-
Notifications
You must be signed in to change notification settings - Fork 56
/
builder.py
506 lines (412 loc) · 19.2 KB
/
builder.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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
import os
import pipes
import socket
from subprocess import Popen
import time
from urlparse import urlparse
from ansible.runner import Runner
from backend.vm_manage import PUBSUB_INTERRUPT_BUILDER
from ..helpers import get_redis_connection, ensure_dir_exists
from ..exceptions import BuilderError, BuilderTimeOutError, AnsibleCallError, AnsibleResponseError, VmError
from ..constants import mockchain, rsync, DEF_BUILD_TIMEOUT
import modulemd
class Builder(object):
def __init__(self, opts, hostname, job, logger):
self.opts = opts
self.hostname = hostname
self.job = job
self.timeout = self.job.timeout or self.opts.timeout
self.repos = []
self.log = logger
self.buildroot_pkgs = self.job.buildroot_pkgs or ""
self._remote_tempdir = self.opts.remote_tempdir
self._remote_basedir = self.opts.remote_basedir
self.remote_pkg_path = None
self.remote_pkg_name = None
# if we're at this point we've connected and done stuff on the host
self.conn = self._create_ans_conn()
self.root_conn = self._create_ans_conn(username="root")
self.module_dist_tag = self._load_module_dist_tag()
def _load_module_dist_tag(self):
module_md_filepath = os.path.join(self.job.destdir, self.job.chroot, "module_md.yaml")
try:
mmd = modulemd.ModuleMetadata()
mmd.load(module_md_filepath)
except IOError as e:
return None
except Exception as e:
log.exception(e)
return None
else:
self.log.info("Loaded {}".format(module_md_filepath))
return ("." + mmd.name + '+' + mmd.version + '+' + mmd.release)
def get_chroot_config_path(self, chroot):
return "{tempdir}/{chroot}.cfg".format(tempdir=self.tempdir, chroot=chroot)
@property
def remote_build_dir(self):
return self.tempdir + "/build/"
@property
def tempdir(self):
if self._remote_tempdir:
return self._remote_tempdir
create_tmpdir_cmd = "/bin/mktemp -d {0}/{1}-XXXXX".format(
self._remote_basedir, "mockremote")
results = self._run_ansible(create_tmpdir_cmd)
tempdir = None
# TODO: use check_for_ans_error
for _, resdict in results["contacted"].items():
tempdir = resdict["stdout"]
# if still nothing then we"ve broken
if not tempdir:
raise BuilderError("Could not make tmpdir on {0}".format(
self.hostname))
self._run_ansible("/bin/chmod 755 {0}".format(tempdir))
self._remote_tempdir = tempdir
return self._remote_tempdir
@tempdir.setter
def tempdir(self, value):
self._remote_tempdir = value
def _create_ans_conn(self, username=None):
ans_conn = Runner(remote_user=username or self.opts.build_user,
host_list=self.hostname + ",",
pattern=self.hostname,
forks=1,
transport=self.opts.ssh.transport,
timeout=self.timeout)
return ans_conn
def run_ansible_with_check(self, cmd, module_name=None, as_root=False,
err_codes=None, success_codes=None):
results = self._run_ansible(cmd, module_name, as_root)
try:
check_for_ans_error(
results, self.hostname, err_codes, success_codes)
except AnsibleResponseError as response_error:
raise AnsibleCallError(
msg="Failed to execute ansible command",
cmd=cmd, module_name=module_name, as_root=as_root,
return_code=response_error.return_code,
stdout=response_error.stdout, stderr=response_error.stderr
)
return results
def _run_ansible(self, cmd, module_name=None, as_root=False):
"""
Executes single ansible module
:param str cmd: module command
:param str module_name: name of the invoked module
:param bool as_root:
:return: ansible command result
"""
if as_root:
conn = self.root_conn
else:
conn = self.conn
conn.module_name = module_name or "shell"
conn.module_args = str(cmd)
return conn.run()
def _get_remote_results_dir(self):
if any(x is None for x in [self.remote_build_dir,
self.remote_pkg_name,
self.job.chroot]):
return None
# the pkg will build into a dir by mockchain named:
# $tempdir/build/results/$chroot/$packagename
return os.path.normpath(os.path.join(
self.remote_build_dir, "results", self.job.chroot, self.remote_pkg_name))
def _get_remote_config_dir(self):
return os.path.normpath(os.path.join(self.remote_build_dir, "configs", self.job.chroot))
def setup_mock_chroot_config(self):
"""
Setup mock config for current chroot.
Packages in buildroot_pkgs are added to minimal buildroot.
"""
cfg_path = self.get_chroot_config_path(self.job.chroot)
try:
copy_cmd = "cp /etc/mock/{chroot}.cfg {dest}".format(chroot=self.job.chroot, dest=cfg_path)
self.run_ansible_with_check(copy_cmd, module_name="shell")
except BuilderError as err:
self.log.exception(err)
raise err
if ("'{0} '".format(self.buildroot_pkgs) !=
pipes.quote(str(self.buildroot_pkgs) + ' ')):
# just different test if it contains only alphanumeric characters
# allowed in packages name
raise BuilderError("Do not try this kind of attack on me")
self.log.info("putting {0} into minimal buildroot of {1}"
.format(self.buildroot_pkgs, self.job.chroot))
kwargs = {
"cfg_path": cfg_path,
"chroot": self.job.chroot,
"pkgs": self.buildroot_pkgs,
"net_enabled": "True" if self.job.enable_net else "False",
}
buildroot_cmd = (
"dest={cfg_path}"
" line=\"config_opts['chroot_setup_cmd'] = 'install \\1 {pkgs}'\""
" regexp=\"^.*chroot_setup_cmd.*(@buildsys-build|buildsys-build buildsys-macros).*$\""
" backrefs=yes"
)
buildroot_custom_cmd = (
"dest={cfg_path}"
" line=\"config_opts['chroot_setup_cmd'] = 'install {pkgs}'\""
" regexp=\"config_opts\\['chroot_setup_cmd'\\] = ''$\""
)
set_networking_cmd = (
"dest={cfg_path}"
" line=\"config_opts['use_host_resolv'] = {net_enabled}\""
" regexp=\"^.*use_host_resolv.*$\""
)
try:
self.run_ansible_with_check(set_networking_cmd.format(**kwargs),
module_name="lineinfile")
if self.buildroot_pkgs:
self.run_ansible_with_check(buildroot_cmd.format(**kwargs),
module_name="lineinfile")
self.run_ansible_with_check(buildroot_custom_cmd.format(**kwargs),
module_name="lineinfile")
if self.module_dist_tag:
set_dist_tag_cmd = (
"dest={cfg_path}"
" line=\"config_opts['macros']['%dist'] = '{dist_tag}'\""
" regexp=\"^.*config_opts['macros']['%dist'].*$\""
)
self.run_ansible_with_check(set_dist_tag_cmd.format(
cfg_path=cfg_path, dist_tag=self.module_dist_tag), module_name="lineinfile")
except Exception as err:
self.log.exception(err)
raise
def collect_built_packages(self):
self.log.info("Listing built binary packages")
results = self._run_ansible(
"cd {0} && "
"for f in `ls *.rpm |grep -v \"src.rpm$\"`; do"
" rpm -qp --qf \"%{{NAME}} %{{VERSION}}\n\" $f; "
"done".format(pipes.quote(self._get_remote_results_dir()))
)
built_packages = list(results["contacted"].values())[0][u"stdout"]
self.log.info("Built packages:\n{}".format(built_packages))
return built_packages
def check_build_success(self):
successfile = os.path.join(self._get_remote_results_dir(), "success")
ansible_test_results = self._run_ansible("/usr/bin/test -f {0}".format(successfile))
check_for_ans_error(ansible_test_results, self.hostname)
def download_job_pkg_to_builder(self):
repo_url = "{}/{}.git".format(self.opts.dist_git_url, self.job.git_repo)
self.log.info("Cloning Dist Git repo {}, branch {}, hash {}".format(
self.job.git_repo, self.job.git_branch, self.job.git_hash))
results = self._run_ansible(
"rm -rf /tmp/build_package_repo && "
"mkdir /tmp/build_package_repo && "
"cd /tmp/build_package_repo && "
"git clone {repo_url} && "
"cd {pkg_name} && "
"git checkout {git_hash} && "
"fedpkg-copr --dist {branch} srpm"
.format(repo_url=repo_url,
pkg_name=self.job.package_name,
git_hash=self.job.git_hash,
branch=self.job.git_branch))
# expected output:
# ...
# Wrote: /tmp/.../copr-ping/copr-ping-1-1.fc21.src.rpm
try:
self.remote_pkg_path = list(results["contacted"].values())[0][u"stdout"].split("Wrote: ")[1]
self.remote_pkg_name = os.path.basename(self.remote_pkg_path).replace(".src.rpm", "")
except Exception:
self.log.exception("Failed to obtain srpm from dist-git")
raise BuilderError("Failed to obtain srpm from dist-git: ansible results {}".format(results))
self.log.info("Got srpm to build: {}".format(self.remote_pkg_path))
def pre_process_repo_url(self, repo_url):
"""
Expands variables and sanitize repo url to be used for mock config
"""
try:
parsed_url = urlparse(repo_url)
if parsed_url.scheme == "copr":
user = parsed_url.netloc
prj = parsed_url.path.split("/")[1]
repo_url = "/".join([self.opts.results_baseurl, user, prj, self.job.chroot])
else:
if "rawhide" in self.job.chroot:
repo_url = repo_url.replace("$releasever", "rawhide")
# custom expand variables
repo_url = repo_url.replace("$chroot", self.job.chroot)
repo_url = repo_url.replace("$distname", self.job.chroot.split("-")[0])
return pipes.quote(repo_url)
except Exception as err:
self.log.exception("Failed to pre-process repo url: {}".format(err))
return None
def gen_mockchain_command(self):
buildcmd = "{} -r {} -l {} ".format(
mockchain, pipes.quote(self.get_chroot_config_path(self.job.chroot)),
pipes.quote(self.remote_build_dir))
for repo in self.job.chroot_repos_extended:
repo = self.pre_process_repo_url(repo)
if repo is not None:
buildcmd += "-a {0} ".format(repo)
for k, v in self.job.mockchain_macros.items():
mock_opt = "--define={} {}".format(k, v)
buildcmd += "-m {} ".format(pipes.quote(mock_opt))
buildcmd += self.remote_pkg_path
return buildcmd
def run_build_and_wait(self, buildcmd):
self.log.info("executing: {0}".format(buildcmd))
self.conn.module_name = "shell"
self.conn.module_args = buildcmd
_, poller = self.conn.run_async(self.timeout)
waited = 0
results = None
# self.setup_pubsub_handler()
while True:
# TODO rework Builder and extrace check_pubsub, add method to interrupt build process from dispatcher
# self.check_pubsub()
results = poller.poll()
if results["contacted"] or results["dark"]:
break
if waited >= self.timeout:
raise BuilderTimeOutError("Build timeout expired. Time limit: {}s, time spent: {}s"
.format(self.timeout, waited))
time.sleep(10)
waited += 10
return results
def setup_pubsub_handler(self):
self.rc = get_redis_connection(self.opts)
self.ps = self.rc.pubsub(ignore_subscribe_messages=True)
channel_name = PUBSUB_INTERRUPT_BUILDER.format(self.hostname)
self.ps.subscribe(channel_name)
self.log.info("Subscribed to vm interruptions channel {}".format(channel_name))
def check_pubsub(self):
# self.log.info("Checking pubsub channel")
msg = self.ps.get_message()
if msg is not None and msg.get("type") == "message":
raise VmError("Build interrupted by msg: {}".format(msg["data"]))
# def start_build(self, pkg):
# # build the pkg passed in
# # add pkg to various lists
# # check for success/failure of build
#
# # build_details = {}
# self.setup_mock_chroot_config()
#
# # check if pkg is local or http
# dest = self.check_if_pkg_local_or_http(pkg)
#
# # srpm version
# self.update_job_pkg_version(pkg)
#
# # construct the mockchain command
# buildcmd = self.gen_mockchain_command(dest)
#
def build(self):
self.setup_mock_chroot_config()
# download the package to the builder
self.download_job_pkg_to_builder()
# construct the mockchain command
buildcmd = self.gen_mockchain_command()
# run the mockchain command async
ansible_build_results = self.run_build_and_wait(buildcmd) # now raises BuildTimeoutError
check_for_ans_error(ansible_build_results, self.hostname) # on error raises AnsibleResponseError
# we know the command ended successfully but not if the pkg built
# successfully
self.check_build_success()
return get_ans_results(ansible_build_results, self.hostname).get("stdout", "")
def rsync_call(self, source_path, target_path):
ensure_dir_exists(target_path, self.log)
# make spaces work w/our rsync command below :(
target_path = "'" + target_path.replace("'", "'\\''") + "'"
ssh_opts = "'ssh -o PasswordAuthentication=no -o StrictHostKeyChecking=no'"
full_source_path = "{}@{}:{}/*".format(self.opts.build_user,
self.hostname,
source_path)
log_filepath = os.path.join(target_path, self.job.rsync_log_name)
command = "{} -rlptDvH -e {} {} {}/ &> {}".format(
rsync, ssh_opts, full_source_path, target_path, log_filepath)
# dirty magic with Popen due to IO buffering
# see http://thraxil.org/users/anders/posts/2008/03/13/Subprocess-Hanging-PIPE-is-your-enemy/
# alternative: use tempfile.Tempfile as Popen stdout/stderr
try:
self.log.info("rsyncing of {0} started for job: {1}".format(full_source_path, self.job))
cmd = Popen(command, shell=True)
cmd.wait()
self.log.info("rsyncing finished.")
except Exception as error:
err_msg = "Failed to download data from builder due to rsync error, see the rsync log file for details. Original error: {}".format(error)
self.log.error(err_msg)
raise BuilderError(err_msg)
if cmd.returncode != 0:
err_msg = "Failed to download data from builder due to rsync error, see the rsync log file for details."
self.log.error(err_msg)
raise BuilderError(err_msg, return_code=cmd.returncode)
def download_results(self, target_path):
if self._get_remote_results_dir():
self.rsync_call(self._get_remote_results_dir(), target_path)
def download_configs(self, target_path):
self.rsync_call(self._get_remote_config_dir(), target_path)
def check(self):
# do check of host
try:
# requires name resolve facility
socket.gethostbyname(self.hostname)
except IOError:
raise BuilderError("{0} could not be resolved".format(self.hostname))
try:
# check_for_ans_error(res, self.hostname)
self.run_ansible_with_check("/bin/rpm -q mock rsync")
except AnsibleCallError:
raise BuilderError(msg="Build host `{0}` does not have mock or rsync installed"
.format(self.hostname))
# test for path existence for mockchain and chroot config for this chroot
try:
self.run_ansible_with_check("/usr/bin/test -f {0}".format(mockchain))
except AnsibleCallError:
raise BuilderError(msg="Build host `{}` missing mockchain binary `{}`"
.format(self.hostname, mockchain))
try:
self.run_ansible_with_check("/usr/bin/test -f /etc/mock/{}.cfg"
.format(self.job.chroot))
except AnsibleCallError:
raise BuilderError(msg="Build host `{}` missing mock config for chroot `{}`"
.format(self.hostname, self.job.chroot))
def get_ans_results(results, hostname):
if hostname in results["dark"]:
return results["dark"][hostname]
if hostname in results["contacted"]:
return results["contacted"][hostname]
return {}
def check_for_ans_error(results, hostname, err_codes=None, success_codes=None):
"""
dict includes 'msg'
may include 'rc', 'stderr', 'stdout' and any other requested result codes
:raises AnsibleResponseError:
:raises VmError:
"""
if err_codes is None:
err_codes = []
if success_codes is None:
success_codes = [0]
if ("dark" in results and hostname in results["dark"]) or \
"contacted" not in results or hostname not in results["contacted"]:
raise VmError(msg="Error: Could not contact/connect to {}. raw results: {}".format(hostname, results))
error = False
err_results = {}
if err_codes or success_codes:
if hostname in results["contacted"]:
if "rc" in results["contacted"][hostname]:
rc = int(results["contacted"][hostname]["rc"])
err_results["return_code"] = rc
# check for err codes first
if rc in err_codes:
error = True
err_results["msg"] = "rc {0} matched err_codes".format(rc)
elif rc not in success_codes:
error = True
err_results["msg"] = "rc {0} not in success_codes".format(rc)
elif ("failed" in results["contacted"][hostname] and
results["contacted"][hostname]["failed"]):
error = True
err_results["msg"] = "results included failed as true"
if error:
for item in ["stdout", "stderr"]:
if item in results["contacted"][hostname]:
err_results[item] = results["contacted"][hostname][item]
if error:
raise AnsibleResponseError(**err_results)