1
1
import re
2
+ import shutil
2
3
import subprocess
3
4
from pathlib import Path
4
5
from textwrap import dedent
@@ -19,17 +20,19 @@ class VagrantBackend(DockerBackend):
19
20
Inherits settings from :class:`DockerBackend`:
20
21
21
22
* **python_version**
22
- * **apk_dependencies **
23
+ * **apt_dependencies **
23
24
* **disable_pull**
24
25
* **inherit_image**
25
26
* **use_registry_name**
27
+ * **keep_containers_running** - applies for containers inside the VM, default is ``True`` here.
26
28
27
29
Adds new settings:
28
30
29
- * **box** - what Vagrant box to use (must include docker >= 1.8 or no docker)
30
- * **provider** - what provider should Vagrant user
31
- * **quiet** - Keeps the extra vagrant logs quiet.
32
- * **destroy** - Destroy or just halt the VMs (default is ``True``)
31
+ * **box** - what Vagrant box to use (must include docker >= 1.8 or no docker), ``ailispaw/barge`` being the default
32
+ * **provider** - what provider should Vagrant user, ``virtualbox`` being the default
33
+ * **quiet** - Keeps the extra vagrant logs quiet, ``True`` being the default
34
+ * **keep_vm_running** - Keeps the VM up until ``stop_vm`` is called, ``False`` being the default
35
+ * **destroy** - Destroy the VM (instead of halt) when stopping it, ``False`` being the default
33
36
34
37
"""
35
38
@@ -38,9 +41,13 @@ class VagrantBackend(DockerBackend):
38
41
box = LazySettingProperty (default = "ailispaw/barge" )
39
42
provider = LazySettingProperty (default = "virtualbox" )
40
43
quiet = LazySettingProperty (default = True , convert = bool )
41
- destroy = LazySettingProperty (default = True , convert = bool )
44
+ keep_container_running = LazySettingProperty (default = True , convert = bool )
45
+ keep_vm_running = LazySettingProperty (default = False , convert = bool )
46
+ destroy = LazySettingProperty (default = False , convert = bool )
42
47
43
48
def __init__ (self , ** kwargs ):
49
+ """ Initializes the instance and checks that docker and vagrant are installed.
50
+ """
44
51
super ().__init__ (** kwargs )
45
52
46
53
try :
@@ -61,17 +68,32 @@ def __init__(self, **kwargs):
61
68
if vagrant .get_vagrant_executable () is None :
62
69
raise ArcaMisconfigured ("Vagrant executable is not accessible!" )
63
70
71
+ self .vagrant : vagrant .Vagrant = None
72
+
73
+ def inject_arca (self , arca ):
74
+ """ Creates log file for this instance.
75
+ """
76
+ super ().inject_arca (arca )
77
+
78
+ import vagrant
79
+
80
+ self .log_path = Path (self ._arca .base_dir ) / "logs" / (str (uuid4 ()) + ".log" )
81
+ self .log_path .parent .mkdir (exist_ok = True , parents = True )
82
+ logger .info ("Storing vagrant log in %s" , self .log_path )
83
+
84
+ self .log_cm = vagrant .make_file_cm (self .log_path )
85
+
64
86
def validate_settings (self ):
65
87
""" Runs :meth:`arca.DockerBackend.validate_settings` and checks extra:
66
88
67
89
* ``box`` format
68
90
* ``provider`` format
69
- * ``use_registry_name`` is set
91
+ * ``use_registry_name`` is set and ``registry_pull_only`` is not enabled.
70
92
"""
71
93
super ().validate_settings ()
72
94
73
95
if self .use_registry_name is None :
74
- raise ArcaMisconfigured ("Push to registry setting is required for VagrantBackend" )
96
+ raise ArcaMisconfigured ("Use registry name setting is required for VagrantBackend" )
75
97
76
98
if not re .match (r"^[a-z]+/[a-zA-Z0-9\-_]+$" , self .box ):
77
99
raise ArcaMisconfigured ("Provided Vagrant box is not valid" )
@@ -82,41 +104,25 @@ def validate_settings(self):
82
104
if self .registry_pull_only :
83
105
raise ArcaMisconfigured ("Push must be enabled for VagrantBackend" )
84
106
85
- def get_vagrant_file_location (self , repo : str , branch : str , git_repo : Repo , repo_path : Path ) -> Path :
86
- """ Returns a directory where Vagrantfile should be. Based on repo, branch and tag of the used docker image.
87
- """
88
- path = Path (self ._arca .base_dir ) / "vagrant"
89
- path /= self ._arca .repo_id (repo )
90
- path /= branch
91
- path /= self .get_image_tag (self .get_requirements_file (repo_path ), self .get_dependencies ())
92
- return path
93
-
94
- def create_vagrant_file (self , repo : str , branch : str , git_repo : Repo , repo_path : Path ):
95
- """ Creates a Vagrantfile in the target dir with the required settings and the required docker image.
96
- The image is built locally if not already pushed.
107
+ def get_vm_location (self ) -> Path :
108
+ """ Returns a directory where a Vagrantfile should be - folder called ``vagrant`` in the Arca base dir.
97
109
"""
98
- vagrant_file = self .get_vagrant_file_location (repo , branch , git_repo , repo_path ) / "Vagrantfile"
99
-
100
- self .check_docker_access ()
101
-
102
- self .get_image_for_repo (repo , branch , git_repo , repo_path )
103
-
104
- requirements_file = self .get_requirements_file (repo_path )
105
- dependencies = self .get_dependencies ()
106
- image_tag = self .get_image_tag (requirements_file , dependencies )
107
- image_name = self .use_registry_name
110
+ return Path (self ._arca .base_dir ) / "vagrant"
108
111
109
- logger .info ("Creating Vagrantfile with image %s:%s" , image_name , image_tag )
112
+ def init_vagrant (self , vagrant_file ):
113
+ """ Creates a Vagrantfile in the target dir, with only the base image pulled.
114
+ Copies the runner script to the directory so it's accessible from the VM.
115
+ """
116
+ if self .inherit_image :
117
+ image_name , image_tag = str (self .inherit_image ).split (":" )
118
+ else :
119
+ image_name = self .get_arca_base_name ()
120
+ image_tag = self .get_python_base_tag (self .get_python_version ())
110
121
111
- container_name = "arca_{}_{}_{}" .format (
112
- self ._arca .repo_id (repo ),
113
- branch ,
114
- self ._arca .current_git_hash (repo , branch , git_repo , short = True )
115
- )
116
- workdir = str ((Path ("/srv/data" ) / self .cwd ))
122
+ logger .info ("Creating Vagrantfile located in %s, base image %s:%s" , vagrant_file , image_name , image_tag )
117
123
124
+ repos_dir = (Path (self ._arca .base_dir ) / 'repos' ).resolve ()
118
125
vagrant_file .parent .mkdir (exist_ok = True , parents = True )
119
-
120
126
vagrant_file .write_text (dedent (f"""
121
127
# -*- mode: ruby -*-
122
128
# vi: set ft=ruby :
@@ -126,14 +132,10 @@ def create_vagrant_file(self, repo: str, branch: str, git_repo: Repo, repo_path:
126
132
config.ssh.insert_key = true
127
133
config.vm.provision "docker" do |d|
128
134
d.pull_images "{ image_name } :{ image_tag } "
129
- d.run "{ image_name } :{ image_tag } ",
130
- name: "{ container_name } ",
131
- args: "-t -w { workdir } ",
132
- cmd: "bash -i"
133
135
end
134
136
135
137
config.vm.synced_folder ".", "/vagrant"
136
- config.vm.synced_folder "{ repo_path } ", "/srv/data "
138
+ config.vm.synced_folder "{ repos_dir } ", "/srv/repos "
137
139
config.vm.provider "{ self .provider } "
138
140
139
141
end
@@ -148,59 +150,102 @@ def fabric_task(self):
148
150
from fabric import api
149
151
150
152
@api .task
151
- def run_script (container_name , definition_filename ):
152
- """ Sequence to run inside the VM, copies data and script to the container and runs the script.
153
+ def run_script (container_name , definition_filename , image_name , image_tag , repository ):
154
+ """ Sequence to run inside the VM.
155
+ Starts up the container if the container is not running
156
+ (and copies over the data and the runner script)
157
+ Then the definition is copied over and the script launched.
158
+ If the VM is gonna be shut down then kills the container as well.
153
159
"""
154
- api .run (f"docker exec { container_name } mkdir -p /srv/scripts" )
155
- api .run (f"docker cp /srv/data { container_name } :/srv" )
156
- api .run (f"docker cp /vagrant/runner.py { container_name } :/srv/scripts/" )
160
+ workdir = str ((Path ("/srv/data" ) / self .cwd ).resolve ())
161
+ cmd = "sh" if self .inherit_image else "bash"
162
+
163
+ api .run (f"docker pull { image_name } :{ image_tag } " )
164
+
165
+ container_running = int (api .run (f"docker ps --format '{{.Names}}' -f name={ container_name } | wc -l" ))
166
+ container_stopped = int (api .run (f"docker ps -a --format '{{.Names}}' -f name={ container_name } | wc -l" ))
167
+
168
+ if container_running == 0 :
169
+ if container_stopped :
170
+ api .run (f"docker rm -f { container_name } " )
171
+
172
+ api .run (f"docker run "
173
+ f"--name { container_name } "
174
+ f"--workdir \" { workdir } \" "
175
+ f"-dt { image_name } :{ image_tag } "
176
+ f"{ cmd } -i" )
177
+
178
+ api .run (f"docker exec { container_name } mkdir -p /srv/scripts" )
179
+ api .run (f"docker cp /srv/repos/{ repository } { container_name } :/srv/branch" )
180
+ api .run (f"docker exec --user root { container_name } bash -c 'mv /srv/branch/* /srv/data'" )
181
+ api .run (f"docker exec --user root { container_name } rm -rf /srv/branch" )
182
+ api .run (f"docker cp /vagrant/runner.py { container_name } :/srv/scripts/" )
183
+
157
184
api .run (f"docker cp /vagrant/{ definition_filename } { container_name } :/srv/scripts/" )
158
- return api .run (" " .join ([
159
- "docker" , "exec" , "-t" , container_name ,
185
+
186
+ output = api .run (" " .join ([
187
+ "docker" , "exec" , container_name ,
160
188
"python" , "/srv/scripts/runner.py" , f"/srv/scripts/{ definition_filename } " ,
161
189
]))
162
190
191
+ if not self .keep_container_running :
192
+ api .run (f"docker kill { container_name } " )
193
+
194
+ return output
195
+
163
196
return run_script
164
197
165
- def run (self , repo : str , branch : str , task : Task , git_repo : Repo , repo_path : Path ):
166
- """ Gets or creates Vagrantfile, starts up a VM with it, executes Fabric script over SSH, returns result .
198
+ def ensure_vm_running (self , vm_location ):
199
+ """ Gets or creates a Vagrantfile in ``vm_location`` and calls ``vagrant up`` if the VM is not running .
167
200
"""
168
- # importing here, prints out warning when vagrant is missing even when the backend is not used otherwise
169
- from vagrant import Vagrant , make_file_cm
170
- from fabric import api
201
+ import vagrant
171
202
172
- vagrant_file = self .get_vagrant_file_location (repo , branch , git_repo , repo_path )
203
+ if self .vagrant is None :
204
+ vagrant_file = vm_location / "Vagrantfile"
205
+ if not vagrant_file .exists ():
206
+ self .init_vagrant (vagrant_file )
173
207
174
- if not vagrant_file .exists ():
175
- logger .info ("Vagrantfile doesn't exist, creating" )
176
- self .create_vagrant_file (repo , branch , git_repo , repo_path )
208
+ self .vagrant = vagrant .Vagrant (vm_location ,
209
+ quiet_stdout = self .quiet ,
210
+ quiet_stderr = self .quiet ,
211
+ out_cm = self .log_cm ,
212
+ err_cm = self .log_cm )
177
213
178
- logger . info ( "Vagrantfile in folder %s" , vagrant_file )
214
+ status = [ x for x in self . vagrant . status () if x . name == "default" ][ 0 ]
179
215
180
- task_filename , task_json = self .serialized_task (task )
216
+ if status .state != "running" :
217
+ try :
218
+ self .vagrant .up ()
219
+ except subprocess .CalledProcessError :
220
+ raise BuildError ("Vagrant VM couldn't up launched. See output for details." )
221
+
222
+ def run (self , repo : str , branch : str , task : Task , git_repo : Repo , repo_path : Path ):
223
+ """ Starts up a VM, builds an docker image and gets it to the VM, runs the script over SSH, returns result.
224
+ Stops the VM if ``keep_vm_running`` is not set.
225
+ """
226
+ from fabric import api
181
227
182
- (vagrant_file / task_filename ).write_text (task_json )
228
+ # start up or get running VM
229
+ vm_location = self .get_vm_location ()
230
+ self .ensure_vm_running (vm_location )
231
+ logger .info ("Running with VM located at %s" , vm_location )
183
232
184
- container_name = "arca_{}_{}_{}" .format (
185
- self ._arca .repo_id (repo ),
186
- branch ,
187
- self ._arca .current_git_hash (repo , branch , git_repo , short = True )
188
- )
233
+ # pushes the image to the registry so it can be pulled in the VM
234
+ self .check_docker_access () # init client
235
+ self .get_image_for_repo (repo , branch , git_repo , repo_path )
189
236
190
- log_path = Path (self ._arca .base_dir ) / "logs" / (str (uuid4 ()) + ".log" )
191
- log_path .parent .mkdir (exist_ok = True , parents = True )
192
- log_cm = make_file_cm (log_path )
193
- logger .info ("Storing vagrant log in %s" , log_path )
237
+ # getting things needed for execution over SSH
238
+ image_tag = self .get_image_tag (self .get_requirements_file (repo_path ), self .get_dependencies ())
239
+ image_name = self .use_registry_name
194
240
195
- vagrant = Vagrant (root = vagrant_file , quiet_stdout = self .quiet , quiet_stderr = self .quiet ,
196
- out_cm = log_cm , err_cm = log_cm )
197
- try :
198
- vagrant .up ()
199
- except subprocess .CalledProcessError :
200
- raise BuildError ("Vagrant VM couldn't up launched. See output for details." )
241
+ task_filename , task_json = self .serialized_task (task )
242
+ (vm_location / task_filename ).write_text (task_json )
243
+
244
+ container_name = self .get_container_name (repo , branch , git_repo )
201
245
202
- api .env .hosts = [vagrant .user_hostname_port ()]
203
- api .env .key_filename = vagrant .keyfile ()
246
+ # setting up Fabric
247
+ api .env .hosts = [self .vagrant .user_hostname_port ()]
248
+ api .env .key_filename = self .vagrant .keyfile ()
204
249
api .env .disable_known_hosts = True # useful for when the vagrant box ip changes.
205
250
api .env .abort_exception = BuildError # raises SystemExit otherwise
206
251
api .env .shell = "/bin/sh -l -c"
@@ -209,10 +254,16 @@ def run(self, repo: str, branch: str, task: Task, git_repo: Repo, repo_path: Pat
209
254
else :
210
255
api .output .everything = True
211
256
257
+ # executes the task
212
258
try :
213
- res = api .execute (self .fabric_task , container_name = container_name , definition_filename = task_filename )
214
-
215
- return Result (res [vagrant .user_hostname_port ()].stdout )
259
+ res = api .execute (self .fabric_task ,
260
+ container_name = container_name ,
261
+ definition_filename = task_filename ,
262
+ image_name = image_name ,
263
+ image_tag = image_tag ,
264
+ repository = str (repo_path .relative_to (Path (self ._arca .base_dir ).resolve () / 'repos' )))
265
+
266
+ return Result (res [self .vagrant .user_hostname_port ()].stdout )
216
267
except BuildError : # can be raised by :meth:`Result.__init__`
217
268
raise
218
269
except Exception as e :
@@ -221,7 +272,24 @@ def run(self, repo: str, branch: str, task: Task, git_repo: Repo, repo_path: Pat
221
272
"exception" : e
222
273
})
223
274
finally :
224
- if self .destroy :
225
- vagrant .destroy ()
226
- else :
227
- vagrant .halt ()
275
+ # stops or destroys the VM if it should not be kept running
276
+ if not self .keep_vm_running :
277
+ if self .destroy :
278
+ self .vagrant .destroy ()
279
+ shutil .rmtree (self .vagrant .root , ignore_errors = True )
280
+ self .vagrant = None
281
+ else :
282
+ self .vagrant .halt ()
283
+
284
+ def stop_containers (self ):
285
+ raise ValueError ("Can't be used here, stop the entire VM instead." )
286
+
287
+ def stop_vm (self ):
288
+ """ Stops or destroys the VM used to launch tasks.
289
+ """
290
+ if self .destroy :
291
+ self .vagrant .destroy ()
292
+ shutil .rmtree (self .vagrant .root , ignore_errors = True )
293
+ self .vagrant = None
294
+ else :
295
+ self .vagrant .halt ()
0 commit comments