-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvm_control.py
351 lines (313 loc) · 11.8 KB
/
vm_control.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
import argparse
import os
import re
import signal
import subprocess
import sys
import time
import uuid
from difflib import get_close_matches
from io import TextIOWrapper
from typing import List, Optional
from install_test.consts import BUILD_LOGS, FASTAPI
from install_test.utils import notify
from git_scraping import get_repository_language
SETUP_FILE = "resources/setup.sh"
MACHINE_NAME = "ub"
USER_NAME = "machine"
PWD = "123"
HOST_PORT = "3022"
DOCKER_NAME = "lmmilliken"
IMAGE_NAME = "temp_image"
TIMEOUT = 60 * 20
class OutOfStorage(Exception):
def __init__(self, *args: object) -> None:
super().__init__(*args)
class VMController:
def __init__(self, logs: Optional[str] = "STDOUT") -> None:
self.logs = logs
if self.logs is not None:
with open(self.logs, "w") as f:
f.write("")
def log(self, msg, flag="a"):
if self.logs == "STDOUT":
print(msg)
elif self.logs is not None:
with open(self.logs, flag) as f:
f.write(msg + "\n")
def get_dockerfile(self, target_repo: str) -> str:
"""returns path to a preset dockerfile based on the langauge of target repo."""
language = get_repository_language(target_repo).lower()
dockerfile = os.path.abspath(
f"resources/default_dockerfiles/{language}/Dockerfile"
)
if os.path.exists(dockerfile):
return dockerfile
else:
return ValueError(f"No dockerfile found for langauge: {language}")
def open_machine(self):
"""Opens the to use for testing, if the machine is already running, does nothing."""
machines = subprocess.run(
["VBoxManage", "list", "vms"], capture_output=True
).stdout.decode("utf-8")
if '"' + MACHINE_NAME + '"' not in machines:
raise ValueError(f"no machine named {MACHINE_NAME}")
running_machines = str(
subprocess.run(
["VBoxManage", "list", "runningvms"], capture_output=True
).stdout
)
if '"' + MACHINE_NAME + '"' not in running_machines:
response = str(
subprocess.run(
f"VBoxManage startvm {MACHINE_NAME} --type headless".split(" "),
capture_output=True,
).stdout
)
if "successfully started" not in response:
raise ValueError(f"failed to start machine")
else:
self.log("started machine")
else:
self.log("machine already started")
def setup_repo(self, target_repo: str, dockerfile: str, ref: Optional[str] = None):
"""
Clones target repo in a temporary directory within the vm,
then sends the dockerfile via scp.
"""
# make temp directory
cmd = (
f"sshpass -p {PWD} ssh -p {HOST_PORT} {USER_NAME}@localhost "
f"echo $(mktemp -d)"
)
tmp_dir = (
subprocess.run(cmd.split(" "), capture_output=True)
.stdout.decode("utf-8")
.strip()
)
self.log(f"TEMP DIR: {tmp_dir}")
# clone target repo in temp directory
repo_name = target_repo.split("/")[-1][:-4]
cmd = (
f"/usr/bin/sshpass -p {PWD} ssh -T -p {HOST_PORT} {USER_NAME}@localhost "
f"cd {tmp_dir} ; git clone --recursive {target_repo} ; cd {repo_name} ; rm .dockerignore"
)
if ref is not None:
cmd = cmd + f"; git checkout {ref}"
cmd = cmd.split(" ")
try:
resp = subprocess.run(cmd, capture_output=True, timeout=TIMEOUT)
except subprocess.TimeoutExpired:
resp = subprocess.run(cmd, capture_output=True, timeout=TIMEOUT)
self.log(resp.stderr.decode("utf-8").strip())
self.log(resp.stdout.decode("utf-8").strip())
# get name of the directory where the repo was cloned to (-4 to remove '.git')
repo_name = target_repo.split("/")[-1][:-4]
repo_dir = f"{tmp_dir}/{repo_name}"
print(repo_dir)
# send dockerfile to vm
subprocess.run(
(
f"/usr/bin/sshpass -p {PWD} "
f"scp -P {HOST_PORT} "
"-oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null "
f"{dockerfile} {USER_NAME}@localhost:{repo_dir}/Dockerfile"
).split(" ")
)
dockerfile = dockerfile.split("/")[-1]
return tmp_dir, repo_dir
def build_project(self, repo_dir: str, logs: str) -> bool:
"""Run docker build in the virtual machine and stream progress."""
# build dockerfile
cmd = (
f"sshpass -p {PWD} ssh -p {HOST_PORT} "
"-oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null "
f"{USER_NAME}@localhost "
f"cd {repo_dir} ; docker build --no-cache -t {IMAGE_NAME} ."
).split(" ")
with open(logs, "a") as f:
progress, timeout = self.monitor_process(cmd, f, TIMEOUT)
if timeout:
with open(logs, "a") as f:
progress, timeout = self.monitor_process(cmd, f, TIMEOUT)
# progress = subprocess.Popen(cmd, stdout=f, stderr=f)
# progress.wait()
with open(logs, "r") as f:
output = f.readlines()
passed = False
ran = False
for i, line in enumerate(output):
if "fatal" in line and "No space left on device" in line:
raise OutOfStorage()
if (
(
("==" in line and " in " in line)
or "snapshots" in line
or ("tests" in line)
or len(output) - i < 30
)
and "passed" in line
) or (
ran
and (
len(line.split()) > 0
and line.split()[-1].strip() == "OK"
or ("(" in line and line.split("(")[0].split()[-1] == "OK")
)
):
passed = True
elif "Ran" in line and "tests in" in line:
ran = True
if timeout:
msg = (
"process timed out twice! "
f"(took more than {TIMEOUT} seconds) Aborting...\n"
)
f.write(msg)
notify(msg)
return False
if not passed:
try:
err = "Error running docker build on virtual machine:\n" + "\n".join(
[p.decode("utf-8") for p in progress.stderr]
)
self.log(err)
print(err)
except:
pass
return False
else:
succ = (
"At least 1 test passed.\n"
"Docker build completed successfully on virtual machine."
)
self.log(succ)
print(succ)
return True
def monitor_process(self, cmd: List[str], f: TextIOWrapper, timeout_val: int):
progress = subprocess.Popen(cmd, stdout=f, stderr=f)
start_time = time.time()
timeout = False
interrupted = False
try:
while True:
# get latest output
if progress.poll() is not None:
break
elif time.time() - start_time > timeout_val:
if not interrupted:
# First try to cancel the process with an interrupt
# os.killpg(os.getpgid(progress.pid), signal.SIGINT)
os.kill(progress.pid, signal.SIGINT)
notify(f"INTERRUPTING")
start_time = time.time()
timeout_val = 20
interrupted = True
else:
# If the interrupt did not work, raise a timeout
raise subprocess.TimeoutExpired(cmd, timeout_val)
except subprocess.TimeoutExpired:
notify("KILLING PROCESS")
progress.kill()
timeout = True
finally:
progress.wait()
return progress, timeout
def clear_cache(self):
subprocess.run(
(
f"/usr/bin/sshpass -p {PWD} ssh -T -p {HOST_PORT} {USER_NAME}@localhost "
"docker system prune -a -f"
).split(" "),
)
def cleanup(self, tmp_dir: str, keep_image: bool = False, keep_repo: bool = False):
"""Delete docker image and temporary file after execution."""
if not keep_image:
# remove newly created docker image
self.log("removing docker image...")
subprocess.run(
(
f"sshpass -p {PWD} ssh -T -p {HOST_PORT} {USER_NAME}@localhost "
f"docker image rm {IMAGE_NAME}"
).split(" "),
)
if not keep_repo:
# clear temp directory
self.log("clearing temp directory")
subprocess.run(
(
f"/usr/bin/sshpass -p {PWD} ssh -T -p {HOST_PORT} {USER_NAME}@localhost "
f"rm -rf {tmp_dir}"
).split(" "),
)
def test_dockerfile(
self,
target_repo: str,
dockerfile: Optional[str] = None,
keep_image: bool = False,
keep_repo: bool = False,
logs: Optional[str] = None,
ref: Optional[str] = None,
):
"""
Tests a dockerfile by connecting to a virtual machine,
sending the dockerfile to the vm and then building the docker image inside the vm.
"""
if dockerfile is None:
dockerfile = self.get_dockerfile(target_repo)
self.log(f"using dockerfile: {dockerfile}")
with open(dockerfile, "r") as f:
df_contents = f.read()
self.log(df_contents, "w")
self.open_machine()
try:
tmp_dir, repo_dir = self.setup_repo(target_repo, dockerfile, ref=ref)
self.log("setup repo.")
success = self.build_project(repo_dir=repo_dir, logs=logs or self.logs)
except OutOfStorage:
self.cleanup(tmp_dir)
self.clear_cache()
notify("RAN OUT OF STORAGE!! RESTARTING")
tmp_dir, repo_dir = self.setup_repo(target_repo, dockerfile, ref=ref)
self.log("setup repo.")
success = self.build_project(repo_dir=repo_dir, logs=logs or self.logs)
except Exception as e:
success = False
if __name__ == "__main__":
raise e
else:
print(e)
except KeyboardInterrupt as e:
success = False
self.cleanup(tmp_dir, keep_image=keep_image, keep_repo=keep_repo)
return success
def test_dockerfile(
url: str,
dockerfile: str,
repo_name: Optional[str] = None,
vmc: Optional[VMController] = None,
ref: Optional[str] = None,
) -> bool:
dockerfile_path = "logs/dockerfiles/Dockerfile"
name = url.split("/")[-1][:-4]
with open(dockerfile_path, "w") as f:
f.write(dockerfile)
print(dockerfile)
if vmc is None:
logs = f"{BUILD_LOGS}/{repo_name or name}.log"
vmc = VMController(logs)
(f"\nattempting to build using dockerfile, logs written to {vmc.logs}.")
return vmc.test_dockerfile(url, dockerfile_path, ref=ref)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--dockerfile",
help="path to a dockerfile you want to test",
default="resources/working_dockerfiles/20k+/fastapi.dockerfile",
)
parser.add_argument(
"--repo", help="url to a repo you want to test", default=FASTAPI
)
args = parser.parse_args()
controller = VMController()
controller.test_dockerfile(target_repo=args.repo, dockerfile=args.dockerfile)