Merge lp:~heber013/utah/adding-setup-for-autopkgtest into lp:utah
- adding-setup-for-autopkgtest
- Merge into dev
Proposed by
Heber Parrucci
Status: | Merged |
---|---|
Approved by: | Jean-Baptiste Lallement |
Approved revision: | 1108 |
Merged at revision: | 1108 |
Proposed branch: | lp:~heber013/utah/adding-setup-for-autopkgtest |
Merge into: | lp:utah |
Diff against target: |
454 lines (+288/-4) 10 files modified
examples/run_utah_tests.py (+1/-0) setup.py (+3/-0) templates/casper-preseed-script.jinja2 (+1/-0) templates/utah-latecommand.jinja2 (+11/-0) utah/config.py (+6/-0) utah/parser.py (+2/-0) utah/provisioning/provisioning.py (+33/-4) utah/provisioning/qemu-scripts/qemu-setup.py (+211/-0) utah/provisioning/qemu-scripts/qemu-setup.service (+9/-0) utah/provisioning/vm.py (+11/-0) |
To merge this branch: | bzr merge lp:~heber013/utah/adding-setup-for-autopkgtest |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jean-Baptiste Lallement | Approve | ||
Review via email: mp+326954@code.launchpad.net |
Commit message
* Creating a serial console on ttyS1 for autopkgtest to use it.
* Adding option to poweroff VM after tests have run.
Description of the change
Creating a serial console on ttyS1 for autopkgtest to use it.
Adding option to poweroff VM after tests have run.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'examples/run_utah_tests.py' |
2 | --- examples/run_utah_tests.py 2013-06-20 00:06:02 +0000 |
3 | +++ examples/run_utah_tests.py 2017-07-06 18:08:08 +0000 |
4 | @@ -54,6 +54,7 @@ |
5 | if image is None: |
6 | image = ISO(arch=args.arch, installtype=args.type, series=args.series) |
7 | kw = {'clean': (not args.no_destroy), |
8 | + 'poweroff': args.poweroff, |
9 | 'boot': args.boot, |
10 | 'debug': args.debug, |
11 | 'image': image, |
12 | |
13 | === modified file 'setup.py' |
14 | --- setup.py 2013-06-19 20:30:45 +0000 |
15 | +++ setup.py 2017-07-06 18:08:08 +0000 |
16 | @@ -52,6 +52,9 @@ |
17 | 'utah.client.probe', |
18 | 'utah.provisioning', |
19 | 'utah.provisioning.baremetal'], |
20 | + package_data={ |
21 | + 'utah': ['provisioning/qemu-scripts/*'] |
22 | + }, |
23 | maintainer=maintainer, |
24 | maintainer_email=maintainer_email, |
25 | ) |
26 | |
27 | === modified file 'templates/casper-preseed-script.jinja2' |
28 | --- templates/casper-preseed-script.jinja2 2013-05-22 16:56:32 +0000 |
29 | +++ templates/casper-preseed-script.jinja2 2017-07-06 18:08:08 +0000 |
30 | @@ -6,3 +6,4 @@ |
31 | cp utah-copy-files.sh /root/ |
32 | cp -r utah-copy-files /root/ |
33 | cp -r utah-autorun /root/ |
34 | +cp qemu* /root |
35 | |
36 | === modified file 'templates/utah-latecommand.jinja2' |
37 | --- templates/utah-latecommand.jinja2 2016-06-10 17:40:15 +0000 |
38 | +++ templates/utah-latecommand.jinja2 2017-07-06 18:08:08 +0000 |
39 | @@ -98,9 +98,20 @@ |
40 | fi |
41 | } |
42 | |
43 | +qemu_setup() { |
44 | + if [ -f qemu-setup.py ] ; then |
45 | + cp qemu-setup.py /target/usr/local/bin/qemu-setup.py |
46 | + chmod 755 /target/usr/local/bin/qemu-setup.py |
47 | + cp qemu-setup.service /target/lib/systemd/system/qemu-setup.service |
48 | + chmod 644 /target/lib/systemd/system/qemu-setup.service |
49 | + chroot /target /bin/systemctl enable qemu-setup.service |
50 | + fi |
51 | +} |
52 | + |
53 | copy_ssh_keys |
54 | write_utah_data |
55 | autologin |
56 | autorun |
57 | copy_provisioned_files |
58 | copy_rsyslog_cfg # do this last to avoid getting each syslog msg 2 times |
59 | +qemu_setup |
60 | |
61 | === modified file 'utah/config.py' |
62 | --- utah/config.py 2017-06-06 20:18:58 +0000 |
63 | +++ utah/config.py 2017-07-06 18:08:08 +0000 |
64 | @@ -81,6 +81,12 @@ |
65 | 'type': 'boolean', |
66 | 'default': True, |
67 | }, |
68 | + 'poweroff': { |
69 | + 'description': ( |
70 | + 'Setting for whether to power off VM after a test run'), |
71 | + 'type': 'boolean', |
72 | + 'default': False, |
73 | + }, |
74 | 'client_install_timeout': { |
75 | 'description': ( |
76 | 'Maximum amount of time to wait ' |
77 | |
78 | === modified file 'utah/parser.py' |
79 | --- utah/parser.py 2013-05-28 11:58:47 +0000 |
80 | +++ utah/parser.py 2017-07-06 18:08:08 +0000 |
81 | @@ -110,6 +110,8 @@ |
82 | '(%(choices)s) (Default is %(default)s)') |
83 | parser.add_argument('-n', '--no-destroy', action='store_true', |
84 | help='Preserve VM after tests have run') |
85 | + parser.add_argument('--poweroff', action='store_true', |
86 | + help='Power off VM after tests have run') |
87 | parser.add_argument('-d', '--debug', action='store_true', |
88 | help='Enable debug logging') |
89 | parser.add_argument('-j', '--json', action='store_true', |
90 | |
91 | === modified file 'utah/provisioning/provisioning.py' |
92 | --- utah/provisioning/provisioning.py 2017-03-03 14:35:49 +0000 |
93 | +++ utah/provisioning/provisioning.py 2017-07-06 18:08:08 +0000 |
94 | @@ -24,6 +24,7 @@ |
95 | import pipes |
96 | import re |
97 | import shutil |
98 | +import stat |
99 | import sys |
100 | import urllib |
101 | import uuid |
102 | @@ -63,8 +64,8 @@ |
103 | |
104 | """ |
105 | |
106 | - def __init__(self, boot=config.boot, clean=True, debug=False, |
107 | - image=None, initrd=config.initrd, inventory=None, |
108 | + def __init__(self, boot=config.boot, clean=True, poweroff=False, |
109 | + debug=False, image=None, initrd=config.initrd, inventory=None, |
110 | kernel=config.kernel, machineid=config.machineid, |
111 | machineuuid=config.machineuuid, name=config.name, new=False, |
112 | preseed=config.preseed, rewrite=config.rewrite, |
113 | @@ -110,6 +111,7 @@ |
114 | # TODO: Consider a global temp file creator, maybe as part of install. |
115 | self.boot = boot |
116 | self.clean = clean |
117 | + self.poweroff = poweroff |
118 | self.debug = debug |
119 | self.image = image |
120 | self.inventory = inventory |
121 | @@ -522,11 +524,13 @@ |
122 | if self.clean: |
123 | cleanup.add_path(path) |
124 | |
125 | - def cleanfunction(self, function, *args, **kw): |
126 | + def cleanfunction(self, function, force=False, *args, **kw): |
127 | """Register a function to be run on cleanup. |
128 | |
129 | :param function: A callable that will do some cleanup. |
130 | :type function: callable |
131 | + :param force: whether to force adding the cleanup function. |
132 | + :type force: boolean |
133 | :param args: Positional arguments to the function. |
134 | :type args: Tuple |
135 | :param kw: Keyword arguments to the function. |
136 | @@ -536,7 +540,7 @@ |
137 | |
138 | """ |
139 | # TODO: consider doing this directly instead of through Machine |
140 | - if self.clean: |
141 | + if self.clean or force: |
142 | cleanup.add_function(60, function, *args, **kw) |
143 | |
144 | def cleancommand(self, cmd): |
145 | @@ -703,6 +707,13 @@ |
146 | filename, |
147 | log_file='/var/log/utah-install') |
148 | |
149 | + def _copy_qemu_scripts(self, tmpdir=None): |
150 | + initrd_work_dir = os.path.join(tmpdir, 'initrd') |
151 | + if not os.path.exists(initrd_work_dir): |
152 | + os.makedirs(initrd_work_dir) |
153 | + # copy the qemu scripts into initrd |
154 | + self.copy_qemu_scripts_to_path(initrd_work_dir) |
155 | + |
156 | def _setuppreseed(self, tmpdir=None): |
157 | """Rewrite the preseed to automate installation and access.""" |
158 | # TODO: document this better |
159 | @@ -978,3 +989,21 @@ |
160 | """ |
161 | iface = netifaces.ifaddresses(ifname) |
162 | return iface[netifaces.AF_INET][0]['addr'] |
163 | + |
164 | + def get_qemu_scripts_dir(self): |
165 | + """Return path of the source directory containing qemu setup scripts.""" |
166 | + src_dir = os.path.dirname(os.path.realpath(__file__)) |
167 | + return os.path.join(src_dir, 'qemu-scripts') |
168 | + |
169 | + def copy_qemu_scripts_to_path(self, path): |
170 | + """Copy all qemu scripts to required path and ensure correct permissions. |
171 | + :param path: Target path to copy scripts to. |
172 | + """ |
173 | + src_dir = self.get_qemu_scripts_dir() |
174 | + mask = (stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | |
175 | + stat.S_IXOTH) |
176 | + for src_name in os.listdir(src_dir): |
177 | + src = os.path.join(src_dir, src_name) |
178 | + dst = os.path.join(path, src_name) |
179 | + shutil.copy2(src, dst) |
180 | + os.chmod(dst, mask) |
181 | |
182 | === added directory 'utah/provisioning/qemu-scripts' |
183 | === added file 'utah/provisioning/qemu-scripts/qemu-setup.py' |
184 | --- utah/provisioning/qemu-scripts/qemu-setup.py 1970-01-01 00:00:00 +0000 |
185 | +++ utah/provisioning/qemu-scripts/qemu-setup.py 2017-07-06 18:08:08 +0000 |
186 | @@ -0,0 +1,211 @@ |
187 | +# Ubuntu Testing Automation Harness |
188 | +# Copyright 2017 Canonical Ltd. |
189 | + |
190 | +# This program is free software: you can redistribute it and/or modify it |
191 | +# under the terms of the GNU General Public License version 3, as published |
192 | +# by the Free Software Foundation. |
193 | + |
194 | +# This program is distributed in the hope that it will be useful, but |
195 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
196 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
197 | +# PURPOSE. See the GNU General Public License for more details. |
198 | + |
199 | +# You should have received a copy of the GNU General Public License along |
200 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
201 | + |
202 | +import os |
203 | +import subprocess |
204 | +import time |
205 | + |
206 | +AUTOPKGTEST_NAME = 'autopkgtest.service' |
207 | +AUTOPKGTEST_SERVICE = os.path.join('/lib/systemd/system', AUTOPKGTEST_NAME) |
208 | +AUTOPKGTEST_CFG = '/etc/default/grub.d/90-autopkgtest.cfg' |
209 | + |
210 | +""" This script is run on first boot of the qemu after installation, by the |
211 | + qemu-setup.service. It will prepare the system by: |
212 | + - Creating a serial console on ttyS1 that will be used in future by |
213 | + autopkgtest. The service will be started automatically on all subsequent |
214 | + boots by autopkgtest.service. |
215 | + - Disable qemu-setup.service to stop this script from running on subsequent |
216 | + boots. |
217 | + - Power down the system to complete the setup process. |
218 | +""" |
219 | + |
220 | + |
221 | +def wait_for_network(): |
222 | + """Wait until network connection is active.""" |
223 | + max_count = 60 |
224 | + for count in range(max_count): |
225 | + status = subprocess.call(['wget', '-q', '--spider', 'launchpad.net']) |
226 | + if status == 0: |
227 | + break |
228 | + elif count >= max_count - 1: |
229 | + raise Exception('No network connection available') |
230 | + else: |
231 | + print('wget attempt {} failed, trying again...'.format(count + 1)) |
232 | + time.sleep(1) |
233 | + |
234 | + |
235 | +def wait_for_locks(): |
236 | + """Wait for file locks to be released for doing install operations.""" |
237 | + max_count = 60 |
238 | + lock_files = [ |
239 | + '/var/lib/dpkg/lock', |
240 | + '/var/cache/apt/archives/lock', |
241 | + '/var/lib/apt/lists/lock', |
242 | + ] |
243 | + for count in range(max_count): |
244 | + status = subprocess.call(['fuser'] + lock_files) |
245 | + if status == 1: |
246 | + break |
247 | + elif count >= max_count - 1: |
248 | + raise RuntimeError('File locks not released') |
249 | + else: |
250 | + print('file locks active, trying again...'.format(count + 1)) |
251 | + time.sleep(1) |
252 | + |
253 | + |
254 | +def get_architecture(): |
255 | + """Return architecture of running system.""" |
256 | + return subprocess.check_output( |
257 | + ['dpkg', '--print-architecture']).decode().strip() |
258 | + |
259 | + |
260 | +def install_packages(packages): |
261 | + """Install specfied list of packages.""" |
262 | + wait_for_locks() |
263 | + subprocess.check_call(['dpkg', '--configure', '-a']) |
264 | + subprocess.check_call(['apt-get', 'install', '-y'] + packages) |
265 | + |
266 | + |
267 | +def create_file(path, content): |
268 | + """Create a file with specified path and content.""" |
269 | + with open(path, 'w') as f: |
270 | + f.write(content) |
271 | + |
272 | + |
273 | +def setup_root_shell_service(): |
274 | + """Setup root shell service on ttyS1 for use by autopkgtest.""" |
275 | + create_file( |
276 | + AUTOPKGTEST_SERVICE, |
277 | + '[Unit]\n' |
278 | + 'Description=autopkgtest root shell on ttyS1\n' |
279 | + 'ConditionPathExists=/dev/ttyS1\n\n' |
280 | + '[Service]\n' |
281 | + 'ExecStart=/bin/sh\n' |
282 | + 'StandardInput=tty-fail\n' |
283 | + 'StandardOutput=tty\n' |
284 | + 'StandardError=tty\n' |
285 | + 'TTYPath=/dev/ttyS1\n' |
286 | + 'SendSIGHUP=yes\n' |
287 | + 'SuccessExitStatus=0 208 SIGHUP SIGINT SIGTERM SIGPIPE\n\n' |
288 | + '[Install]\n' |
289 | + 'WantedBy=multi-user.target\n' |
290 | + ) |
291 | + os.chmod(AUTOPKGTEST_SERVICE, 0o644) |
292 | + subprocess.check_call(['systemctl', 'enable', AUTOPKGTEST_NAME]) |
293 | + |
294 | + |
295 | +def _grubd_autopkgtest_setup(architecture): |
296 | + """Add autopkgtest config to grub.d directory. |
297 | + :param architecture: Required architecture. |
298 | + :return: True if changes made, False otherwise. |
299 | + """ |
300 | + if architecture == 'i386': |
301 | + create_file( |
302 | + AUTOPKGTEST_CFG, |
303 | + 'GRUB_CMDLINE_LINUX_DEFAULT="console=ttyS0 vmalloc=512M"' |
304 | + ) |
305 | + return True |
306 | + elif architecture == 'amd64': |
307 | + create_file( |
308 | + AUTOPKGTEST_CFG, |
309 | + 'GRUB_CMDLINE_LINUX_DEFAULT="console=ttyS0"' |
310 | + ) |
311 | + return True |
312 | + return False |
313 | + |
314 | + |
315 | +def _grub_autopkgtest_setup(architecture): |
316 | + """Add autopkgtest config to grub directory. |
317 | + :param architecture: Required architecture. |
318 | + :return: True if changes made, False otherwise. |
319 | + """ |
320 | + changed = False |
321 | + if architecture == 'i386': |
322 | + subprocess.check_call( |
323 | + ['sed', '-i', |
324 | + '/CMDLINE_LINUX_DEFAULT/ s/"$/ console=ttyS0 ' |
325 | + 'vmalloc=512M"/', '/etc/default/grub'] |
326 | + ) |
327 | + changed = True |
328 | + elif architecture == 'amd64': |
329 | + subprocess.check_call( |
330 | + ['sed', '-i', |
331 | + '/CMDLINE_LINUX_DEFAULT/ s/"$/ console=ttyS0"/', |
332 | + '/etc/default/grub'] |
333 | + ) |
334 | + changed = True |
335 | + |
336 | + zero_timeout = subprocess.check_output( |
337 | + ['grep', '-q', 'GRUB_HIDDEN_TIMEOUT=0', '/etc/default/grub'] |
338 | + ) == 0 |
339 | + |
340 | + if zero_timeout: |
341 | + subprocess.check_output( |
342 | + ['sed', '-i', '/^GRUB_TIMEOUT=/ s/=.*$/=1/', |
343 | + '/etc/default/grub'] |
344 | + ) |
345 | + changed = True |
346 | + |
347 | + return changed |
348 | + |
349 | + |
350 | +def setup_serial_console(): |
351 | + """Setup a serial console for autopkgtest to use.""" |
352 | + # This method is derived from: |
353 | + # /usr/share/autopkgtest/setup-commands/setup-testbed |
354 | + setup_root_shell_service() |
355 | + grub = subprocess.call(['which', 'update-grub']) == 0 |
356 | + if not os.path.isfile(AUTOPKGTEST_CFG) and grub: |
357 | + arch = get_architecture() |
358 | + if os.path.isdir('/etc/default/grub.d'): |
359 | + changed = _grubd_autopkgtest_setup(arch) |
360 | + else: |
361 | + changed = _grub_autopkgtest_setup(arch) |
362 | + if changed: |
363 | + subprocess.check_call(['update-grub']) |
364 | + |
365 | + |
366 | +def disable_qemu_setup_on_boot(): |
367 | + """Disable this setup script from running again on future boot up.""" |
368 | + subprocess.call(['systemctl', 'disable', 'qemu-setup.service']) |
369 | + |
370 | + |
371 | +def system_shutdown(): |
372 | + """Shutdown the system to complete first boot setup.""" |
373 | + subprocess.call(['shutdown', '--poweroff', '0']) |
374 | + |
375 | + |
376 | +def do_setup(): |
377 | + """Complete first boot setup actions.""" |
378 | + wait_for_network() |
379 | + setup_serial_console() |
380 | + install_packages(['openssh-server']) |
381 | + |
382 | + |
383 | +def do_teardown(): |
384 | + """Cleanup actions to disable this from running after first boot.""" |
385 | + disable_qemu_setup_on_boot() |
386 | + system_shutdown() |
387 | + |
388 | + |
389 | +def main(): |
390 | + try: |
391 | + do_setup() |
392 | + finally: |
393 | + do_teardown() |
394 | + |
395 | + |
396 | +if __name__ == '__main__': |
397 | + main() |
398 | |
399 | === added file 'utah/provisioning/qemu-scripts/qemu-setup.service' |
400 | --- utah/provisioning/qemu-scripts/qemu-setup.service 1970-01-01 00:00:00 +0000 |
401 | +++ utah/provisioning/qemu-scripts/qemu-setup.service 2017-07-06 18:08:08 +0000 |
402 | @@ -0,0 +1,9 @@ |
403 | +[Unit] |
404 | +Description=Ubuntu System Tests QEMU setup autostart script |
405 | + |
406 | +[Service] |
407 | +Type=oneshot |
408 | +ExecStart=/bin/sh -c "/usr/bin/python3 /usr/local/bin/qemu-setup.py > /dev/ttyS0 2>&1" |
409 | + |
410 | +[Install] |
411 | +WantedBy=multi-user.target |
412 | |
413 | === modified file 'utah/provisioning/vm.py' |
414 | --- utah/provisioning/vm.py 2016-12-23 15:01:36 +0000 |
415 | +++ utah/provisioning/vm.py 2017-07-06 18:08:08 +0000 |
416 | @@ -27,6 +27,7 @@ |
417 | |
418 | from xml.etree import ElementTree |
419 | |
420 | +from utah.cleanup import cleanup |
421 | from utah.config import config |
422 | from utah.process import ProcessChecker, ProcessRunner |
423 | from utah.provisioning.exceptions import UTAHProvisioningException |
424 | @@ -518,6 +519,8 @@ |
425 | initrd_dir = os.path.join(tmpdir, 'initrd.d') |
426 | provision_data.update_initrd(initrd_dir) |
427 | |
428 | + self._copy_qemu_scripts(tmpdir=tmpdir) |
429 | + |
430 | self._setuplatecommand(tmpdir=tmpdir) |
431 | |
432 | self._setuppreseed(tmpdir=tmpdir) |
433 | @@ -546,6 +549,8 @@ |
434 | self.vm = self.lv.defineXML(ElementTree.tostring(xml.getroot())) |
435 | self.cleanfunction(self.vm.destroy) |
436 | self.cleanfunction(self.vm.undefine) |
437 | + if self.poweroff: |
438 | + self.cleanfunction(self.vm.shutdown, force=True) |
439 | |
440 | def _start(self): |
441 | """Start the VM.""" |
442 | @@ -620,6 +625,12 @@ |
443 | "UPDATE machines SET state='destroyed' ""WHERE machineid=?", |
444 | [machineid]) |
445 | |
446 | + def shut_off(self, machineid): |
447 | + """Update the database to indicate a machine is shut off.""" |
448 | + self.execute( |
449 | + "UPDATE machines SET state='shut off' ""WHERE machineid=?", |
450 | + [machineid]) |
451 | + |
452 | |
453 | def get_vm(**kw): |
454 | """Return a Machine object for a VM with the passed in arguments. |
Approved. Thanks.