Merge lp:~nuclearbob/utah/arm into lp:utah

Proposed by Max Brustkern
Status: Merged
Merged at revision: 731
Proposed branch: lp:~nuclearbob/utah/arm
Merge into: lp:utah
Diff against target: 979 lines (+705/-44)
13 files modified
debian/control (+2/-1)
examples/run_install_test.py (+7/-6)
examples/run_test_bamboo_feeder.py (+104/-0)
examples/run_test_cobbler.py (+2/-1)
examples/run_test_vm.py (+2/-1)
examples/run_utah_tests.py (+12/-15)
utah/config.py (+3/-1)
utah/orderedcollections.py (+75/-0)
utah/provisioning/baremetal/bamboofeeder.py (+304/-0)
utah/provisioning/baremetal/power.py (+60/-0)
utah/provisioning/inventory/sqlite.py (+3/-2)
utah/provisioning/provisioning.py (+129/-11)
utah/provisioning/vm/libvirtvm.py (+2/-6)
To merge this branch: bzr merge lp:~nuclearbob/utah/arm
Reviewer Review Type Date Requested Status
Joe Talbott (community) Approve
Max Brustkern (community) Needs Resubmitting
Review via email: mp+130646@code.launchpad.net

Description of the change

This branch adds support for provisioning panda boards in a bamboo-feeder setup, such as the magners lab. Notable additions include:
bamboofeeder.py: provides the BambooFeederMachine class
power.py: provides power commands for non-cobbler physical machines using an arbitrary command or fence_cdu.
run_test_bamboo_feeder.py: script to run the new provisioning method. Also incorporated into run_utah_test.py if '-a arm' is specifried.
Notable changes include:
provisioning.py: Machine class now supports cleanup functions. Subclasses other than BambooFeederMachine have not been updated to use them, but I intend to propose a later merge for that. Image handling has been slightly altered to support ARM images, which are not ISOs, and thus currently will not work with the ISO class. The ISO class could be updated for this later, or an alternate solution developed. Default preseed support has moved into Machine from CustomVM.
sqlite.py: ManualCobblerSQLiteInventory class now supports other machine types. It should be renamed, and 'cargs' (cobbler arguments) should be genericized, possibly into machineinfo. I intend to propose a later merge for that as well.
libvirtvm.py: CustomVM now uses the default preseed support in Machine.

There may be more changes I'm forgetting, so I'll check over that and comment once I have the diffs. I merged the latest dev branch before this proposal, so new things should be incorporated.

To post a comment you must log in.
lp:~nuclearbob/utah/arm updated
731. By Max Brustkern

Removed duplicate class definition

Revision history for this message
Max Brustkern (nuclearbob) wrote :

Other changes:
u-boot-tools added to Suggests. cobbler and vm-tools demoted there as well, since they're only needed for specific integrations. Maybe at some point those integrations should be separate packages. Getting the main test runner script to respond to that and output good, useful error would be a chunk of work, but probably a good thing to do in the long run.
test running scripts were updated to pass through the clean parameter, based on the --no-destroy option.
powertimeout in config is now 15 by default to support the pandas better.
pxedir and wwwdir are added to config to control file locations. These could be used for further pxe integration later.
OrderedSet and HashableDict were added to facilitate cleanup functionality.
Machine class got proper destroy and depcheck methods to support Super calls from subclasses.
depcheck is used in BambooFeederMachine to make sure u-boot-tools is available. I think CobblerMachine and VMToolsVM should move to a similar system in a future merge.

Revision history for this message
Max Brustkern (nuclearbob) wrote :

More about cleanup:
Machine subclasses can now run cleanfile, cleanfunction, and cleancommand. When cleanup is invoked, typically as part of the Machine class's new destroy function, which is also called by the default destructor, those OrderedSets are iterated over and executed. OrderedSets are used to prevent duplicate registrations. First, everything in cleanfiles is removed. Links and files are set to be writeable and unlinked. Directories are recursively set to be writeable and recursively unlinked, similar to how they were handled in the CobblerMachine cleanup, which should eventually be altered to use this routine. Functions in cleanfunctions are run, which any arguments passed in, and a timeout of 60 seconds. This could be altered or removed in the future. Finally, comamnds in cleancommands are run through the machine's _runargs method.

Revision history for this message
Joe Talbott (joetalbott) wrote :

line 10: one too many 'o's in vm-tools
753: 'clearn' should probably be 'clean'.

Other than those looks good to me.

review: Needs Fixing
lp:~nuclearbob/utah/arm updated
732. By Max Brustkern

vm-toools had tooo many ooos

733. By Max Brustkern

clearn -> clean

Revision history for this message
Max Brustkern (nuclearbob) wrote :

Fixed those.

review: Needs Resubmitting
Revision history for this message
Joe Talbott (joetalbott) wrote :

Awesome. Go for it.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/control'
2--- debian/control 2012-10-05 20:22:37 +0000
3+++ debian/control 2012-10-25 20:09:20 +0000
4@@ -17,7 +17,8 @@
5 python-apt, python-libvirt,
6 python-netifaces, python-paramiko, python-psutil,
7 utah-client (=${binary:Version})
8-Recommends: cobbler, kvm, vm-tools
9+Recommends: kvm
10+Suggests: cobbler, u-boot-tools, vm-tools
11 Description: Ubuntu Test Automation Harness
12 Automation framework for testing in Ubuntu
13
14
15=== modified file 'examples/run_install_test.py'
16--- examples/run_install_test.py 2012-10-04 16:11:18 +0000
17+++ examples/run_install_test.py 2012-10-25 20:09:20 +0000
18@@ -90,12 +90,13 @@
19 try:
20 inventory = TinySQLiteInventory()
21 machine = inventory.request(CustomVM,
22- arch=args.arch, boot=args.boot, debug=args.debug,
23- diskbus=args.diskbus, disksizes=args.gigabytes,
24- dlpercentincrement=10, emulator=args.emulator,
25- image=args.image, initrd=args.initrd, installtype=args.type,
26- kernel=args.kernel, new=True, preseed=args.preseed,
27- rewrite=args.rewrite, series=args.series, xml=args.xml)
28+ arch=args.arch, boot=args.boot, clean=(not args.no_destroy),
29+ debug=args.debug, diskbus=args.diskbus,
30+ disksizes=args.gigabytes, dlpercentincrement=10,
31+ emulator=args.emulator, image=args.image, initrd=args.initrd,
32+ installtype=args.type, kernel=args.kernel, new=True,
33+ preseed=args.preseed, rewrite=args.rewrite,
34+ series=args.series, xml=args.xml)
35 exitstatus, locallogs = run_tests(args, machine)
36
37 except UTAHException as error:
38
39=== added file 'examples/run_test_bamboo_feeder.py'
40--- examples/run_test_bamboo_feeder.py 1970-01-01 00:00:00 +0000
41+++ examples/run_test_bamboo_feeder.py 2012-10-25 20:09:20 +0000
42@@ -0,0 +1,104 @@
43+#!/usr/bin/python
44+
45+import argparse
46+import os
47+import sys
48+
49+from utah.exceptions import UTAHException
50+from utah.group import check_user_group, print_group_error_message
51+from utah.provisioning.baremetal.bamboofeeder import BambooFeederMachine
52+from utah.provisioning.inventory.sqlite import ManualCobblerSQLiteInventory
53+from utah.run import run_tests
54+from utah.url import url_argument
55+
56+
57+def get_parser():
58+ parser = argparse.ArgumentParser(
59+ description=('Provision a pandaboard in a bamboo-feeder setup '
60+ 'and run one or more UTAH runlists there.'),
61+ epilog=("For example:\n"
62+ "Provision a machine using a precise server image "
63+ "with i386 architecture and run the two given runlists\n"
64+ "\t%(prog)s -s precise -t server -a i386 \\\n"
65+ "\t\t/usr/share/utah/client/examples/master.run \\\n"
66+ "\t\t'http://people.canonical.com/~max/max_test.run'"),
67+ formatter_class=argparse.RawDescriptionHelpFormatter)
68+ parser.add_argument('runlists', metavar='runlist', nargs='+',
69+ help='URLs of runlist files to run')
70+ parser.add_argument('-s', '--series', metavar='SERIES',
71+ choices=('hardy', 'lucid', 'natty',
72+ 'oneiric', 'precise', 'quantal'),
73+ help='Series to use for VM creation (%(choices)s)')
74+ parser.add_argument('-t', '--type', metavar='TYPE',
75+ choices=('desktop', 'server', 'mini', 'alternate'),
76+ help=('Install type to use for VM creation '
77+ '(%(choices)s)'))
78+ parser.add_argument('-a', '--arch', metavar='ARCH',
79+ choices=('i386', 'amd64'),
80+ help=('Architecture to use for VM creation '
81+ '(%(choices)s)'))
82+ parser.add_argument('--name', help='Name of machine to provision')
83+ parser.add_argument('-i', '--image', type=url_argument,
84+ help='Image/ISO file to use for installation')
85+ parser.add_argument('-p', '--preseed', type=url_argument,
86+ help='Preseed file to use for installation')
87+ parser.add_argument('-n', '--no-destroy', action='store_true',
88+ help='Preserve VM after tests have run')
89+ parser.add_argument('-d', '--debug', action='store_true',
90+ help='Enable debug logging')
91+ parser.add_argument('-j', '--json', action='store_true',
92+ help='Enable json logging (default is YAML)')
93+ return parser
94+
95+
96+def run_test_bamboo_feeder(args=None):
97+ if not check_user_group():
98+ print_group_error_message(__file__)
99+ sys.exit(3)
100+
101+ if args is None:
102+ args = get_parser().parse_args()
103+
104+ locallogs = []
105+ exitstatus = 0
106+ machine = None
107+
108+ try:
109+ inventory = ManualCobblerSQLiteInventory(
110+ db=os.path.join('~', '.utah-bamboofeeder-inventory'),
111+ lockfile=os.path.join('~', '.utah-bamboofeeder-lock'))
112+ kw = {}
113+ for arg in ['image', 'preseed']:
114+ value = vars(args)[arg]
115+ if value is not None:
116+ kw.update([(arg, value)])
117+ machine = inventory.request(machinetype=BambooFeederMachine,
118+ clean=(not args.no_destroy),
119+ debug=args.debug, new=True,
120+ dlpercentincrement=10, **kw)
121+ exitstatus, locallogs = run_tests(args, machine)
122+
123+ except UTAHException as error:
124+ mesg = 'Exception: ' + str(error)
125+ try:
126+ machine.logger.error(mesg)
127+ except (AttributeError, NameError):
128+ sys.stderr.write(mesg)
129+ exitstatus = 2
130+
131+ finally:
132+ if machine is not None:
133+ try:
134+ machine.destroy()
135+ except UTAHException as error:
136+ sys.stderr.write('Failed to destroy machine: ' + str(error))
137+
138+ if len(locallogs) != 0:
139+ print('Test logs copied to the following files:')
140+ print("\t" + "\n\t".join(locallogs))
141+
142+ sys.exit(exitstatus)
143+
144+
145+if __name__ == '__main__':
146+ run_test_bamboo_feeder()
147
148=== modified file 'examples/run_test_cobbler.py'
149--- examples/run_test_cobbler.py 2012-10-04 16:11:18 +0000
150+++ examples/run_test_cobbler.py 2012-10-25 20:09:20 +0000
151@@ -76,7 +76,8 @@
152 value = vars(args)[arg]
153 if value is not None:
154 kw.update([(arg, value)])
155- machine = inventory.request(debug=args.debug, new=True,
156+ machine = inventory.request(clean=(not args.no_destroy),
157+ debug=args.debug, new=True,
158 dlpercentincrement=10, **kw)
159 exitstatus, locallogs = run_tests(args, machine)
160
161
162=== modified file 'examples/run_test_vm.py'
163--- examples/run_test_vm.py 2012-09-05 12:06:10 +0000
164+++ examples/run_test_vm.py 2012-10-25 20:09:20 +0000
165@@ -64,7 +64,8 @@
166 kw.update([(arg, value)])
167 if args.type is not None:
168 kw.update([('installtype', args.type)])
169- machine = inventory.request(debug=args.debug, new=True,
170+ machine = inventory.request(clean=(not args.no_destroy),
171+ debug=args.debug, new=True,
172 dlpercentincrement=10, **kw)
173 exitstatus, locallogs = run_tests(args, machine)
174
175
176=== modified file 'examples/run_utah_tests.py'
177--- examples/run_utah_tests.py 2012-10-04 16:11:18 +0000
178+++ examples/run_utah_tests.py 2012-10-25 20:09:20 +0000
179@@ -88,24 +88,21 @@
180 args = get_parser().parse_args()
181
182 if args.arch is not None and 'arm' in args.arch:
183- sys.stderr.write("ARM support is not included in this release.\n")
184- sys.stderr.write("Please check the roadmap, "
185- "as it will be included in a future version.\n")
186- sys.exit(4)
187-
188- function = run_test_vm
189-
190- for option in [args.boot, args.emulator, args.image, args.initrd,
191- args.kernel, args.preseed, args.xml]:
192- if option is not None:
193- from run_install_test import run_install_test
194- function = run_install_test
195- break
196-
197- if args.machinetype == 'physical':
198+ from run_test_bamboo_feeder import run_test_bamboo_feeder
199+ function = run_test_bamboo_feeder
200+ elif args.machinetype == 'physical':
201 from run_test_cobbler import run_test_cobbler
202 function = run_test_cobbler
203+ else:
204+ function = run_test_vm
205
206+ for option in [args.boot, args.emulator, args.image, args.initrd,
207+ args.kernel, args.preseed, args.xml]:
208+ if option is not None:
209+ from run_install_test import run_install_test
210+ function = run_install_test
211+ break
212+
213 function(args=args)
214
215 if __name__ == '__main__':
216
217=== modified file 'utah/config.py' (properties changed: +x to -x)
218--- utah/config.py 2012-10-16 10:46:10 +0000
219+++ utah/config.py 2012-10-25 20:09:20 +0000
220@@ -46,12 +46,14 @@
221 nfscommand=['sudo', os.path.join('/', 'etc', 'init.d', 'nfs-kernel-server'), 'reload'],
222 nfsconfigfile=os.path.join('/', 'etc', 'exports'),
223 nfsoptions='*(ro,async,no_root_squash,no_subtree_check)',
224- powertimeout=5,
225+ powertimeout=15,
226 preseed=None,
227+ pxedir=os.path.join('/', 'var', 'lib', 'tftpboot', 'pxelinux.cfg'),
228 qemupath='qemu:///system',
229 rewrite='all',
230 series='precise',
231 template=None,
232+ wwwdir=os.path.join('/', 'var', 'www', 'utah'),
233 )
234
235 # These depend on the local user/path, and need to be filtered out
236
237=== added file 'utah/orderedcollections.py'
238--- utah/orderedcollections.py 1970-01-01 00:00:00 +0000
239+++ utah/orderedcollections.py 2012-10-25 20:09:20 +0000
240@@ -0,0 +1,75 @@
241+"""
242+Provide additional collections to support cleanup functions.
243+"""
244+
245+
246+import collections
247+
248+KEY, PREV, NEXT = range(3)
249+
250+
251+class OrderedSet(collections.MutableSet):
252+
253+ def __init__(self, iterable=None):
254+ self.end = end = []
255+ end += [None, end, end] # sentinel node for doubly linked list
256+ self.map = {} # key --> [key, prev, next]
257+ if iterable is not None:
258+ self |= iterable
259+
260+ def __len__(self):
261+ return len(self.map)
262+
263+ def __contains__(self, key):
264+ return key in self.map
265+
266+ def add(self, key):
267+ if key not in self.map:
268+ end = self.end
269+ curr = end[PREV]
270+ curr[NEXT] = end[PREV] = self.map[key] = [key, curr, end]
271+
272+ def discard(self, key):
273+ if key in self.map:
274+ key, prev, next = self.map.pop(key)
275+ prev[NEXT] = next
276+ next[PREV] = prev
277+
278+ def __iter__(self):
279+ end = self.end
280+ curr = end[NEXT]
281+ while curr is not end:
282+ yield curr[KEY]
283+ curr = curr[NEXT]
284+
285+ def __reversed__(self):
286+ end = self.end
287+ curr = end[PREV]
288+ while curr is not end:
289+ yield curr[KEY]
290+ curr = curr[PREV]
291+
292+ def pop(self, last=True):
293+ if not self:
294+ raise KeyError('set is empty')
295+ key = next(reversed(self)) if last else next(iter(self))
296+ self.discard(key)
297+ return key
298+
299+ def __repr__(self):
300+ if not self:
301+ return '%s()' % (self.__class__.__name__,)
302+ return '%s(%r)' % (self.__class__.__name__, list(self))
303+
304+ def __eq__(self, other):
305+ if isinstance(other, OrderedSet):
306+ return len(self) == len(other) and list(self) == list(other)
307+ return set(self) == set(other)
308+
309+ def __del__(self):
310+ self.clear() # remove circular references
311+
312+
313+class HashableDict(dict):
314+ def __hash__(self):
315+ return hash(tuple(sorted(self.items())))
316
317=== added file 'utah/provisioning/baremetal/bamboofeeder.py'
318--- utah/provisioning/baremetal/bamboofeeder.py 1970-01-01 00:00:00 +0000
319+++ utah/provisioning/baremetal/bamboofeeder.py 2012-10-25 20:09:20 +0000
320@@ -0,0 +1,304 @@
321+"""
322+Support bare-metal provisioning of bamboo-feeder-based systems.
323+"""
324+
325+import netifaces
326+import os
327+import pipes
328+import shutil
329+import subprocess
330+import tempfile
331+
332+from utah import config
333+from utah.provisioning.provisioning import (
334+ CustomInstallMixin,
335+ Machine,
336+ SSHMixin,
337+ )
338+from utah.provisioning.baremetal.power import PowerMixin
339+from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException
340+from utah.retry import retry
341+
342+
343+class BambooFeederMachine(CustomInstallMixin, SSHMixin, PowerMixin, Machine):
344+ """
345+ Provide a class to provision an ARM board from a bamboo-feeder setup.
346+ """
347+ def __init__(self, boot=None, cargs=None, inventory=None,
348+ name=None, powercmd=None, preboot=None, *args, **kw):
349+ # TODO: change the name of cargs here and elsewhere
350+ # TODO: respect rewrite setting
351+ if name is None:
352+ raise UTAHBMProvisioningException(
353+ 'Machine name reqired for bamboo-feeder machine')
354+ if powercmd is not None:
355+ self.powercmd = powercmd
356+ elif cargs is not None:
357+ self.power = {}
358+ for item in cargs:
359+ if 'power' in item:
360+ self.power[item] = cargs[item]
361+ else:
362+ raise UTAHBMProvisioningException(
363+ 'No power control information specified')
364+ try:
365+ self.macaddress = cargs['mac-address']
366+ except AttributeError:
367+ raise UTAHBMProvisioningException(
368+ 'No MAC address specified')
369+ self.inventory = inventory
370+ super(BambooFeederMachine, self).__init__(*args, name=name, **kw)
371+ if self.inventory is not None:
372+ self.cleanfunction(self.inventory.release, machine=self)
373+ self._depcheck()
374+ if self.image is None:
375+ raise UTAHBMProvisioningException(
376+ 'Image file required for bamboo-feeder installation')
377+ try:
378+ iface = config.wwwiface
379+ except AttributeError:
380+ iface = config.bridge
381+ self.ip = netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['addr']
382+ self.logger.debug('Configuring for ' + iface + ' with IP ' + self.ip)
383+ self._cmdlinesetup()
384+ imageurl = 'http://' + self.ip + '/utah/' + self.name + '.img'
385+ preenvurl = 'http://' + self.ip + '/utah/' + self.name + '.preEnv'
386+ if preboot is None:
387+ self.preboot = ('console=ttyO2,115200n8 imageurl={imageurl} '
388+ 'bootcfg={preenvurl}'.format(
389+ imageurl=imageurl, preenvurl=preenvurl))
390+ else:
391+ # TODO: maybe make this automatically add needed options
392+ self.preboot = preboot
393+ self.logger.debug('Preboot setup:')
394+ self.logger.debug(self.preboot)
395+ self.logger.debug('BambooFeederMachine init finished')
396+
397+ def _prepareimage(self):
398+ """
399+ Make a copy of the image shared via wwa so we can edit it.
400+ """
401+ self.logger.info('Making copy of install image')
402+ self.wwwimage = os.path.join(config.wwwdir, self.name + '.img')
403+ self.logger.debug('Copying ' + self.image + ' to ' + self.wwwimage)
404+ self.cleanfile(self.wwwimage)
405+ shutil.copyfile(self.image, self.wwwimage)
406+ return self.wwwimage
407+
408+ def _mountimage(self):
409+ """
410+ Mount an ARM boot image so we can edit it.
411+ """
412+ self.logger.info('Mounting install image')
413+ self.imagedir = os.path.join(self.tmpdir, 'image.d')
414+ if not os.path.isdir(self.imagedir):
415+ os.makedirs(self.imagedir)
416+ self.sector = int(subprocess.check_output(['file',
417+ self.wwwimage]
418+ ).strip().split(';')[1].split(',')[3].split()[1])
419+ self.logger.debug('Image start sector is ' + str(self.sector))
420+ self.cleanfunction(self._umountimage)
421+ self.logger.debug('Mounting ' + self.wwwimage + ' at ' +
422+ self.imagedir)
423+ self._runargs(['sudo', 'mount', self.wwwimage, self.imagedir,
424+ '-o', 'loop', '-o', 'offset=' + str(self.sector*512),
425+ '-o', 'uid=' + config.user])
426+
427+ def _setupconsole(self):
428+ """
429+ Setup the install to use a serial console."
430+ """
431+ self.logger.info('Setting up the install to use the serial console')
432+ preenvfile = os.path.join(self.imagedir, 'preEnv.txt')
433+ serialpreenvfile = os.path.join(self.imagedir, 'preEnv.txt-serial')
434+ self.logger.debug('Copying ' + serialpreenvfile + ' to ' + preenvfile)
435+ shutil.copyfile(serialpreenvfile, preenvfile)
436+
437+ def _unpackinitrd(self):
438+ """
439+ Unpack the uInitrd file into a directory so we can modify it.
440+ """
441+ self.logger.info('Unpacking uInitrd')
442+ if not os.path.isdir(os.path.join(self.tmpdir, 'initrd.d')):
443+ os.makedirs(os.path.join(self.tmpdir, 'initrd.d'))
444+ os.chdir(os.path.join(self.tmpdir, 'initrd.d'))
445+ filesize = os.path.getsize(self.initrd)
446+ for line in subprocess.check_output(
447+ ['mkimage', '-l', self.initrd]).splitlines():
448+ if 'Data Size:' in line:
449+ datasize = int(line.split()[2])
450+ break
451+ headersize = filesize - datasize
452+ self.logger.debug('uInitrd header size is ' + str(headersize))
453+ pipe = pipes.Template()
454+ pipe.prepend('dd if=$IN bs=1 skip=' + str(headersize), 'f-')
455+ pipe.append('gunzip', '--')
456+ pipe.append('cpio -ivd 2>/dev/null', '-.')
457+ # TODO: suppress dd output
458+ pipe.copy(self.initrd, '/dev/null')
459+
460+ def _repackinitrd(self):
461+ """
462+ Repack the uInitrd file from the initrd.d directory.
463+ """
464+ self.logger.info('Repacking uInitrd')
465+ os.chdir(os.path.join(self.tmpdir, 'initrd.d'))
466+ pipe = pipes.Template()
467+ pipe.prepend('find .', '.-')
468+ pipe.append('cpio --quiet -o -H newc', '--')
469+ pipe.append('gzip -9fc', '--')
470+ initrd = os.path.join(self.tmpdir, 'initrd.gz')
471+ self.logger.debug('Repacking compressed initrd')
472+ pipe.copy('/dev/null', initrd)
473+ self.logger.debug('Creating uInitrd with mkimage')
474+ self._runargs(['mkimage',
475+ '-A', 'arm',
476+ '-O', 'linux',
477+ '-T', 'ramdisk',
478+ '-C', 'gzip',
479+ '-a', '0',
480+ '-e', '0',
481+ '-n', 'initramfs',
482+ '-d', initrd,
483+ self.initrd])
484+
485+ def _umountimage(self):
486+ """
487+ Unmount the image after we're done with it."
488+ """
489+ self.logger.info('Unmounting image')
490+ self._runargs(['sudo', 'umount', self.imagedir])
491+
492+ def _cmdlinesetup(self, boot=None):
493+ """
494+ Add the needed options to the command line for an automatic install.
495+ """
496+ if boot is None:
497+ boot = ''
498+ super(BambooFeederMachine, self)._cmdlinesetup(boot=boot)
499+ # TODO: minimize these
500+ for option in ('auto', 'ro', 'text'):
501+ if option not in self.cmdline:
502+ self.logger.info('Adding boot option: ' + option)
503+ self.cmdline += ' ' + option
504+ for parameter in (('cdrom-detect/try_usb', 'true'),
505+ ('console', 'ttyO2,115200n8'),
506+ ('country', 'US'),
507+ ('hostname', self.name),
508+ ('language', 'en'),
509+ ('locale', 'en_US'),
510+ ('loghost', self.ip),
511+ ('log_port', '10514'),
512+ ('netcfg/choose_interface', 'auto'),
513+ ('url', 'http://' + self.ip + '/utah/' + self.name +
514+ '.cfg'),
515+ ):
516+ if parameter[0] not in self.cmdline:
517+ self.logger.info('Adding boot option: ' + '='.join(parameter))
518+ self.cmdline += ' ' + '='.join(parameter)
519+ self.cmdline.strip()
520+
521+ def _configurepxe(self):
522+ """
523+ Setup PXE configuration to boot remote image.
524+ """
525+ # TODO: Maybe move this into pxe.py
526+ # TODO: look into cutting out the middleman/
527+ # booting straight into the installer? (maybe nfs?)
528+ self.logger.info('Configuring PXE')
529+ pxeconfig = """default utah-bamboofeeder
530+prompt 0
531+timeout 3
532+
533+label utah/bamboofeeder
534+kernel panda/uImage
535+append {preboot}
536+initrd panda/uInitrd
537+""".format(preboot=self.preboot)
538+ tmppxefile = os.path.join(self.tmpdir, 'pxe')
539+ open(tmppxefile, 'w').write(pxeconfig)
540+ self.logger.debug('PXE info written to ' + tmppxefile)
541+ pxefile = os.path.join(config.pxedir,
542+ '01-' + self.macaddress.replace(':', '-'))
543+ self.cleancommand(('sudo', 'rm', '-f', pxefile))
544+ self.logger.debug('Copying ' + tmppxefile + ' to ' + pxefile)
545+ self._runargs(['sudo', 'cp', tmppxefile, pxefile])
546+ preenvfile = os.path.join(config.wwwdir, self.name + '.preEnv')
547+ # TODO: sort this out with self.boot
548+ # figure out which one should be what and customizable
549+ self.preenv = 'bootargs=' + self.cmdline
550+ self.logger.debug('Preenv setup:')
551+ self.logger.debug(self.preenv)
552+ self.cleanfile(preenvfile)
553+ self.logger.debug('Writing preenv setup to ' + preenvfile)
554+ open(preenvfile, 'w').write(self.preenv)
555+
556+ def _create(self):
557+ """
558+ Install the OS on the system.
559+ """
560+ # TODO: more checks and exceptions for failures
561+ self.logger.info('Preparing system install')
562+ self.tmpdir = tempfile.mkdtemp(prefix='/tmp/' + self.name + '_')
563+ self.cleanfile(self.tmpdir)
564+ self.logger.debug('Using ' + self.tmpdir + ' as temp dir')
565+ os.chdir(self.tmpdir)
566+ self._prepareimage()
567+ self._mountimage()
568+ self._setupconsole()
569+ self.initrd = os.path.join(self.imagedir, 'uInitrd')
570+ self.logger.debug('uInitrd is ' + self.initrd)
571+ self._unpackinitrd()
572+ self._setuplatecommand()
573+ # TODO: check if this is still needed
574+ if self.installtype == 'desktop':
575+ self.logger.info('Configuring latecommand for desktop')
576+ myfile = open(os.path.join(self.tmpdir, 'initrd.d',
577+ 'utah-latecommand'), 'a')
578+ myfile.write(
579+"""
580+chroot /target sh -c 'sed "/eth[0-9]/d" -i /etc/network/interfaces'
581+cp /etc/resolv.conf /target/etc/resolv.conf
582+""")
583+ myfile.close()
584+ self._setuppreseed()
585+ self.logger.debug('Copying preseed to download location')
586+ preseedfile = os.path.join(config.wwwdir, self.name + '.cfg')
587+ self.cleanfile(preseedfile)
588+ shutil.copyfile(os.path.join(self.tmpdir, 'initrd.d', 'preseed.cfg'),
589+ preseedfile)
590+ self._repackinitrd()
591+ self._configurepxe()
592+ self._umountimage()
593+ self.restart()
594+ self.logger.info('System installing')
595+ self.logger.info('Checking every ' + str(config.checktimeout) +
596+ ' seconds until system is installed')
597+ self.logger.info('Will time out after ' + str(config.installtimeout)
598+ + ' seconds')
599+ retry(self.sshcheck, config.checktimeout, logmethod=self.logger.info)
600+
601+ self.provisioned = True
602+ self.active = True
603+ # TODO: Make this a method that this and CobblerMachine can call
604+ uuid_check_command = ('[ "{uuid}" == "$(cat /etc/utah/uuid)" ]'
605+ .format(uuid=self.uuid))
606+ if self.run(uuid_check_command)[0] != 0:
607+ self.provisioned = False
608+ raise UTAHBMProvisioningException(
609+ 'Installed UUID differs from Machine UUID; '
610+ 'installation probably failed. '
611+ 'Try restarting cobbler or running cobbler sync')
612+ self.logger.info('System installed')
613+ self.cleanfunction(self.run, ('dd', 'bs=512k', 'count=10',
614+ 'if=/dev/zero', 'of=/dev/mmcblk0'), root=True)
615+ return True
616+
617+ def _depcheck(self):
618+ """
619+ Check for dependencies that are in Recommends or Suggests.
620+ """
621+ super(BambooFeederMachine, self)._depcheck()
622+ cmd = ['which', 'mkimage']
623+ if self._runargs(cmd) != 0:
624+ raise UTAHBMProvisioningException('u-boot-tools not installed')
625
626=== added file 'utah/provisioning/baremetal/power.py'
627--- utah/provisioning/baremetal/power.py 1970-01-01 00:00:00 +0000
628+++ utah/provisioning/baremetal/power.py 2012-10-25 20:09:20 +0000
629@@ -0,0 +1,60 @@
630+"""
631+Provide functions for a machine to interface with a power management system.
632+Currently only the Sentry CDU in use in the Canonical labs is supported.
633+"""
634+
635+import time
636+
637+from utah import config
638+from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException
639+
640+
641+class PowerMixin(object):
642+ """
643+ Provide power cycle commands for the Sentry CDU.
644+ """
645+ def powercommand(self):
646+ try:
647+ cmd = self.powercmd
648+ except AttributeError:
649+ try:
650+ if self.power['power-type'] == 'sentryswitch_cdu':
651+ cmd = ['/usr/sbin/fence_cdu',
652+ '-a', self.power['power-address'],
653+ '-n', self.power['power-id'],
654+ '-l', self.power['power-user'],
655+ '-p', self.power['power-pass'],
656+ '-o']
657+ else:
658+ raise UTAHProvisioningException('Power type ' +
659+ self.power['power-type'] +
660+ ' not supported')
661+ except AttributeError:
662+ raise UTAHBMProvisioningException('Power commands not defined'
663+ ' for this machine')
664+ return cmd
665+
666+ def _start(self):
667+ """
668+ Use the machine's powercommand to start the machine.
669+ """
670+ self.logger.debug('Starting system')
671+ self._runargs(self.powercommand() + ['on'])
672+
673+ def stop(self):
674+ """
675+ Use the machine's powercommand to stop the machine.
676+ """
677+ self.logger.debug('Stopping system')
678+ self._runargs(self.powercommand() + ['off'])
679+
680+ def restart(self):
681+ """
682+ Stop the machine, wait powertimeout, then start it.
683+ """
684+ self.logger.info('Restarting system')
685+ self.stop()
686+ self.logger.debug('Waiting ' + str(config.powertimeout) + ' seconds')
687+ time.sleep(config.powertimeout)
688+ self._start()
689+
690
691=== modified file 'utah/provisioning/inventory/sqlite.py'
692--- utah/provisioning/inventory/sqlite.py 2012-08-22 15:20:25 +0000
693+++ utah/provisioning/inventory/sqlite.py 2012-10-25 20:09:20 +0000
694@@ -83,6 +83,7 @@
695 All columns other than machineid, name, and state are assumed to be
696 arguments for cobbler system creation.
697 """
698+ # TODO: rename this
699 def __init__(self, db='~/.utah-cobbler-inventory',
700 lockfile='~/.utah-cobbler-lock', *args, **kw):
701 db = os.path.expanduser(db)
702@@ -99,7 +100,7 @@
703 raise UTAHProvisioningInventoryException('No machines in database')
704 self.machines = []
705
706- def request(self, name=None, *args, **kw):
707+ def request(self, machinetype=CobblerMachine, name=None, *args, **kw):
708 query = 'SELECT * FROM machines'
709 queryvars = []
710 if name is not None:
711@@ -121,7 +122,7 @@
712 "WHERE machineid=? AND state='available'",
713 [machineid]).rowcount
714 if update == 1:
715- machine = CobblerMachine(*args, cargs=cargs,
716+ machine = machinetype(*args, cargs=cargs,
717 inventory=self, name=name, **kw)
718 self.machines.append(machine)
719 return machine
720
721=== modified file 'utah/provisioning/provisioning.py'
722--- utah/provisioning/provisioning.py 2012-10-16 10:46:10 +0000
723+++ utah/provisioning/provisioning.py 2012-10-25 20:09:20 +0000
724@@ -19,7 +19,12 @@
725 import apt.cache
726 from glob import glob
727
728+from utah.commandstr import commandstr
729 from utah.iso import ISO
730+from utah.orderedcollections import (
731+ HashableDict,
732+ OrderedSet,
733+ )
734 from utah.preseed import Preseed
735 from utah.provisioning.exceptions import UTAHProvisioningException
736 from utah.retry import retry
737@@ -37,16 +42,17 @@
738 A fully implemented subclass will provide all public methods except read,
739 I.E.:
740
741- * provisioncheck, activecheck, getclientdeb, installclient
742+ * provisioncheck, activecheck, getclientdeb, installclient, destroy
743 (all these can be used from Machine or replaced.)
744- * destroy, stop, uploadfiles, downloadfiles, run
745+ * stop, uploadfiles, downloadfiles, run
746 (these must be implemented separately.)
747 """
748- def __init__(self, arch=None, debug=False, directory=None, image=None,
749- dlpercentincrement=1, initrd=None, installtype=None,
750- kernel=None, machineid=None, machineuuid=None, name=None,
751- new=False, prefix='utah', preseed=None, rewrite=None,
752- series=None, template=None, xml=None):
753+ def __init__(self, arch=None, clean=None, debug=False, directory=None,
754+ image=None, dlpercentincrement=1, initrd=None,
755+ installtype=None, kernel=None, machineid=None,
756+ machineuuid=None, name=None, new=False, prefix='utah',
757+ preseed=None, rewrite=None, series=None, template=None,
758+ xml=None):
759 """
760 Initialize the object representing the machine.
761 One of these groups of arguments should be included:
762@@ -60,6 +66,7 @@
763 reinstall a specific machine.
764 The subclass is responsible for provisioning with these arguments.
765 Other arguments:
766+ clean: Enable cleanup functions.
767 debug: Enable debug logging.
768 directory: Where the machine's files go. Should be persistent for
769 VMs, temporary is fine for installation-only files, like those
770@@ -99,6 +106,15 @@
771 self.series = series
772 self.template = template
773
774+ if clean is None:
775+ self.clean = True
776+ else:
777+ self.clean = clean
778+
779+ self.cleanfiles = OrderedSet()
780+ self.cleanfunctions = OrderedSet()
781+ self.cleancommands = OrderedSet()
782+
783 self._namesetup(name)
784 self._dirsetup(directory)
785
786@@ -116,14 +132,21 @@
787 self.active = False
788 self._loggersetup()
789
790+ if preseed is None:
791+ preseed = '/etc/utah/default-preseed.cfg'
792+
793+ fileargs = ['preseed', 'xml', 'kernel', 'initrd']
794+
795 if image is None:
796 self.image = None
797- else:
798+ elif image.endswith('.iso'):
799 self.image = ISO(image,
800 dlpercentincrement=self.dlpercentincrement,
801 logger=self.logger)
802+ else:
803+ fileargs.append('image')
804
805- for item in ['preseed', 'xml', 'kernel', 'initrd']:
806+ for item in fileargs:
807 # Ensure every file/url type argument is available locally
808 arg = locals()[item]
809 if arg is None:
810@@ -159,6 +182,7 @@
811 """
812 Setup the machine's directory, automatically or using a specified one.
813 """
814+ # TODO: Move this to vm
815 if directory is None:
816 self.directory = os.path.join(config.vmpath, self.name)
817 else:
818@@ -340,7 +364,15 @@
819 Should generally not be called directly outside of the class;
820 provisioncheck() or activecheck() should be used.
821 """
822- self._unimplemented(sys._getframe().f_code.co_name)
823+ if not self.new:
824+ try:
825+ self.logger.debug('Trying to load existing machine')
826+ self._load()
827+ self.provisioned = True
828+ return self.provisioned
829+ except Exception as err:
830+ self.logger.debug('Failed to load machine: ' + str(err))
831+ self._create()
832
833 def _create(self):
834 """
835@@ -364,7 +396,8 @@
836 should be powered off if remote power management is available.
837 Now returns False by default to work with multiple inheritance.
838 """
839- return False
840+ self.__del__()
841+ del self
842
843 def _load(self):
844 """
845@@ -470,11 +503,95 @@
846 line = p.stdout.readline().strip()
847 self.logger.debug(line)
848 return p.returncode
849+
850+ def _depcheck(self):
851+ """
852+ Check for dependencies that are in Recommends or Suggests.
853+ None exist for the main class, but we implement it here to allow
854+ super() to work.
855+ """
856+ pass
857
858 def _unimplemented(self, method):
859 raise UTAHProvisioningException(self.__class__.__name__ +
860 ' attempted to call the ' + method +
861 ' method of the base Machine class, which is not implemented')
862+
863+ def cleanup(self):
864+ """
865+ Clean up temporary files and tear down other setup (VM, etc.).
866+ If a subset is specified, only that subset will be cleaned.
867+ This can be used to clean up install files after the install has started.
868+ Files are removed first.
869+ Functions are run second.
870+ Commands are run third.
871+ """
872+ if self.clean:
873+ self.logger.debug('Running cleanup')
874+ for path in tuple(self.cleanfiles):
875+ if os.path.islink(path):
876+ self.logger.debug('Removing link ' + path)
877+ os.unlink(path)
878+ elif os.path.isfile(path):
879+ self.logger.debug('Changing permissions of '+ path)
880+ os.chmod(path, 0664)
881+ self.logger.debug('Removing file ' + path)
882+ os.unlink(path)
883+ elif os.path.isdir(path):
884+ # Cribbed from http://svn.python.org
885+ # /projects/python/trunk/Mac/BuildScript/build-installer.py
886+ for dirpath, dirnames, filenames in os.walk(path):
887+ for name in (dirnames + filenames):
888+ absolute_name = os.path.join(dirpath, name)
889+ if not os.path.islink(absolute_name):
890+ self.logger.debug('Changing permissions of '
891+ + absolute_name)
892+ os.chmod(absolute_name, 0775)
893+ self.logger.debug('Recursively Removing directory '
894+ + path)
895+ shutil.rmtree(path)
896+ else:
897+ self.logger.debug(path + ' is not a link, file, or '
898+ 'directory; not removing')
899+ self.cleanfiles.remove(path)
900+ for function in tuple(self.cleanfunctions):
901+ timeout, command, args, kw = function
902+ self.logger.debug('Running: ' +
903+ commandstr(command, *args, **kw))
904+ utah.timeout.timeout(timeout, command, *args, **kw)
905+ self.cleanfunctions.remove(function)
906+ for command in tuple(self.cleancommands):
907+ self._runargs(command)
908+ self.cleancommands.remove(command)
909+ else:
910+ self.logger.debug('Not cleaning up')
911+
912+ def cleanfile(self, path):
913+ """
914+ Register a link, file, or directory to be cleaned later.
915+ Links will be unlinked.
916+ Files will have permissions changed to 664 and unlinked.
917+ Directories will recursively have permissions changed to 775 (except
918+ for links contained within) and then be recursively removed.
919+ """
920+ self.cleanfiles.add(path)
921+
922+ def cleanfunction(self, function, *args, **kw):
923+ """
924+ Register a function to be run on cleanup.
925+ Functions are run through utah.timeout.timeout.
926+ """
927+ self.cleanfunctions.add((60, function, args, HashableDict(kw)))
928+
929+ def cleancommand(self, cmd):
930+ """
931+ Register a command to be run on cleanup.
932+ Commands are run through Machine._runargs.
933+ """
934+ self.cleancommands.add(cmd)
935+
936+ def __del__(self):
937+ self.cleanup()
938
939
940 class SSHMixin(object):
941@@ -1010,6 +1127,7 @@
942 else:
943 self.cmdline = boot
944 self.boottimeout = config.boottimeout
945+ # TODO: Refactor this into lists like BambooFeederMachine
946 if self.rewrite == 'all':
947 self.logger.info('Adding needed command line options')
948 if self.installtype == 'desktop':
949
950=== modified file 'utah/provisioning/vm/libvirtvm.py'
951--- utah/provisioning/vm/libvirtvm.py 2012-10-16 10:46:10 +0000
952+++ utah/provisioning/vm/libvirtvm.py 2012-10-25 20:09:20 +0000
953@@ -349,8 +349,7 @@
954 def __init__(self, arch=None, boot=None, diskbus=None, disksizes=None,
955 emulator=None, image=None, initrd=None, installtype=None,
956 kernel=None, machineid=None, macs=None, name=None,
957- prefix='utah', preseed=None, series=None, xml=None,
958- *args, **kw):
959+ prefix='utah', series=None, xml=None, *args, **kw):
960 # Make sure that no other virtualization solutions are running
961 # TODO: see if this is needed for qemu or just kvm
962 process_checker = ProcessChecker()
963@@ -373,15 +372,12 @@
964 name = '-'.join([str(prefix), str(machineid)])
965 else:
966 autoname = False
967- if preseed is None:
968- preseed = '/etc/utah/default-preseed.cfg'
969 if xml is None:
970 xml = '/etc/utah/default-vm.xml'
971 super(CustomVM, self).__init__(arch=arch, image=image, initrd=initrd,
972 installtype=installtype, kernel=kernel,
973 machineid=machineid, name=name,
974- preseed=preseed, series=series, xml=xml,
975- *args, **kw)
976+ series=series, xml=xml, *args, **kw)
977 # TODO: do a better job of separating installation
978 # into _create rather than __init__
979 if self.image is None:

Subscribers

People subscribed via source and target branches