Merge lp:~heber013/utah/adding-setup-for-autopkgtest into lp:utah

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
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.
Revision history for this message
Jean-Baptiste Lallement (jibel) wrote :

Approved. Thanks.

review: Approve

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.

Subscribers

People subscribed via source and target branches