Skip to content

Commit 8b2a70d

Browse files
committed
Updated Vagrant docker to only use one VM and related updates to save on time, bandwidth and space.
1 parent 2769a70 commit 8b2a70d

File tree

3 files changed

+205
-102
lines changed

3 files changed

+205
-102
lines changed

arca/backend/docker.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ class DockerBackend(BaseBackend):
6969
"""
7070

7171
def __init__(self, **kwargs):
72+
""" Initializes the instance and checks that the docker package is installed.
73+
"""
7274
super().__init__(**kwargs)
7375

7476
if docker is None:

arca/backend/vagrant.py

Lines changed: 154 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
import shutil
23
import subprocess
34
from pathlib import Path
45
from textwrap import dedent
@@ -19,17 +20,19 @@ class VagrantBackend(DockerBackend):
1920
Inherits settings from :class:`DockerBackend`:
2021
2122
* **python_version**
22-
* **apk_dependencies**
23+
* **apt_dependencies**
2324
* **disable_pull**
2425
* **inherit_image**
2526
* **use_registry_name**
27+
* **keep_containers_running** - applies for containers inside the VM, default is ``True`` here.
2628
2729
Adds new settings:
2830
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
3336
3437
"""
3538

@@ -38,9 +41,13 @@ class VagrantBackend(DockerBackend):
3841
box = LazySettingProperty(default="ailispaw/barge")
3942
provider = LazySettingProperty(default="virtualbox")
4043
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)
4247

4348
def __init__(self, **kwargs):
49+
""" Initializes the instance and checks that docker and vagrant are installed.
50+
"""
4451
super().__init__(**kwargs)
4552

4653
try:
@@ -61,17 +68,32 @@ def __init__(self, **kwargs):
6168
if vagrant.get_vagrant_executable() is None:
6269
raise ArcaMisconfigured("Vagrant executable is not accessible!")
6370

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+
6486
def validate_settings(self):
6587
""" Runs :meth:`arca.DockerBackend.validate_settings` and checks extra:
6688
6789
* ``box`` format
6890
* ``provider`` format
69-
* ``use_registry_name`` is set
91+
* ``use_registry_name`` is set and ``registry_pull_only`` is not enabled.
7092
"""
7193
super().validate_settings()
7294

7395
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")
7597

7698
if not re.match(r"^[a-z]+/[a-zA-Z0-9\-_]+$", self.box):
7799
raise ArcaMisconfigured("Provided Vagrant box is not valid")
@@ -82,41 +104,25 @@ def validate_settings(self):
82104
if self.registry_pull_only:
83105
raise ArcaMisconfigured("Push must be enabled for VagrantBackend")
84106

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.
97109
"""
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"
108111

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())
110121

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)
117123

124+
repos_dir = (Path(self._arca.base_dir) / 'repos').resolve()
118125
vagrant_file.parent.mkdir(exist_ok=True, parents=True)
119-
120126
vagrant_file.write_text(dedent(f"""
121127
# -*- mode: ruby -*-
122128
# vi: set ft=ruby :
@@ -126,14 +132,10 @@ def create_vagrant_file(self, repo: str, branch: str, git_repo: Repo, repo_path:
126132
config.ssh.insert_key = true
127133
config.vm.provision "docker" do |d|
128134
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"
133135
end
134136
135137
config.vm.synced_folder ".", "/vagrant"
136-
config.vm.synced_folder "{repo_path}", "/srv/data"
138+
config.vm.synced_folder "{repos_dir}", "/srv/repos"
137139
config.vm.provider "{self.provider}"
138140
139141
end
@@ -148,59 +150,102 @@ def fabric_task(self):
148150
from fabric import api
149151

150152
@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.
153159
"""
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+
157184
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,
160188
"python", "/srv/scripts/runner.py", f"/srv/scripts/{definition_filename}",
161189
]))
162190

191+
if not self.keep_container_running:
192+
api.run(f"docker kill {container_name}")
193+
194+
return output
195+
163196
return run_script
164197

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.
167200
"""
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
171202

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)
173207

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)
177213

178-
logger.info("Vagrantfile in folder %s", vagrant_file)
214+
status = [x for x in self.vagrant.status() if x.name == "default"][0]
179215

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
181227

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)
183232

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)
189236

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
194240

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)
201245

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()
204249
api.env.disable_known_hosts = True # useful for when the vagrant box ip changes.
205250
api.env.abort_exception = BuildError # raises SystemExit otherwise
206251
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
209254
else:
210255
api.output.everything = True
211256

257+
# executes the task
212258
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)
216267
except BuildError: # can be raised by :meth:`Result.__init__`
217268
raise
218269
except Exception as e:
@@ -221,7 +272,24 @@ def run(self, repo: str, branch: str, task: Task, git_repo: Repo, repo_path: Pat
221272
"exception": e
222273
})
223274
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

Comments
 (0)