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
=== modified file 'debian/control'
--- debian/control 2012-10-05 20:22:37 +0000
+++ debian/control 2012-10-25 20:09:20 +0000
@@ -17,7 +17,8 @@
17 python-apt, python-libvirt,17 python-apt, python-libvirt,
18 python-netifaces, python-paramiko, python-psutil,18 python-netifaces, python-paramiko, python-psutil,
19 utah-client (=${binary:Version})19 utah-client (=${binary:Version})
20Recommends: cobbler, kvm, vm-tools20Recommends: kvm
21Suggests: cobbler, u-boot-tools, vm-tools
21Description: Ubuntu Test Automation Harness22Description: Ubuntu Test Automation Harness
22 Automation framework for testing in Ubuntu23 Automation framework for testing in Ubuntu
2324
2425
=== modified file 'examples/run_install_test.py'
--- examples/run_install_test.py 2012-10-04 16:11:18 +0000
+++ examples/run_install_test.py 2012-10-25 20:09:20 +0000
@@ -90,12 +90,13 @@
90 try:90 try:
91 inventory = TinySQLiteInventory()91 inventory = TinySQLiteInventory()
92 machine = inventory.request(CustomVM,92 machine = inventory.request(CustomVM,
93 arch=args.arch, boot=args.boot, debug=args.debug,93 arch=args.arch, boot=args.boot, clean=(not args.no_destroy),
94 diskbus=args.diskbus, disksizes=args.gigabytes,94 debug=args.debug, diskbus=args.diskbus,
95 dlpercentincrement=10, emulator=args.emulator,95 disksizes=args.gigabytes, dlpercentincrement=10,
96 image=args.image, initrd=args.initrd, installtype=args.type,96 emulator=args.emulator, image=args.image, initrd=args.initrd,
97 kernel=args.kernel, new=True, preseed=args.preseed,97 installtype=args.type, kernel=args.kernel, new=True,
98 rewrite=args.rewrite, series=args.series, xml=args.xml)98 preseed=args.preseed, rewrite=args.rewrite,
99 series=args.series, xml=args.xml)
99 exitstatus, locallogs = run_tests(args, machine)100 exitstatus, locallogs = run_tests(args, machine)
100101
101 except UTAHException as error:102 except UTAHException as error:
102103
=== added file 'examples/run_test_bamboo_feeder.py'
--- examples/run_test_bamboo_feeder.py 1970-01-01 00:00:00 +0000
+++ examples/run_test_bamboo_feeder.py 2012-10-25 20:09:20 +0000
@@ -0,0 +1,104 @@
1#!/usr/bin/python
2
3import argparse
4import os
5import sys
6
7from utah.exceptions import UTAHException
8from utah.group import check_user_group, print_group_error_message
9from utah.provisioning.baremetal.bamboofeeder import BambooFeederMachine
10from utah.provisioning.inventory.sqlite import ManualCobblerSQLiteInventory
11from utah.run import run_tests
12from utah.url import url_argument
13
14
15def get_parser():
16 parser = argparse.ArgumentParser(
17 description=('Provision a pandaboard in a bamboo-feeder setup '
18 'and run one or more UTAH runlists there.'),
19 epilog=("For example:\n"
20 "Provision a machine using a precise server image "
21 "with i386 architecture and run the two given runlists\n"
22 "\t%(prog)s -s precise -t server -a i386 \\\n"
23 "\t\t/usr/share/utah/client/examples/master.run \\\n"
24 "\t\t'http://people.canonical.com/~max/max_test.run'"),
25 formatter_class=argparse.RawDescriptionHelpFormatter)
26 parser.add_argument('runlists', metavar='runlist', nargs='+',
27 help='URLs of runlist files to run')
28 parser.add_argument('-s', '--series', metavar='SERIES',
29 choices=('hardy', 'lucid', 'natty',
30 'oneiric', 'precise', 'quantal'),
31 help='Series to use for VM creation (%(choices)s)')
32 parser.add_argument('-t', '--type', metavar='TYPE',
33 choices=('desktop', 'server', 'mini', 'alternate'),
34 help=('Install type to use for VM creation '
35 '(%(choices)s)'))
36 parser.add_argument('-a', '--arch', metavar='ARCH',
37 choices=('i386', 'amd64'),
38 help=('Architecture to use for VM creation '
39 '(%(choices)s)'))
40 parser.add_argument('--name', help='Name of machine to provision')
41 parser.add_argument('-i', '--image', type=url_argument,
42 help='Image/ISO file to use for installation')
43 parser.add_argument('-p', '--preseed', type=url_argument,
44 help='Preseed file to use for installation')
45 parser.add_argument('-n', '--no-destroy', action='store_true',
46 help='Preserve VM after tests have run')
47 parser.add_argument('-d', '--debug', action='store_true',
48 help='Enable debug logging')
49 parser.add_argument('-j', '--json', action='store_true',
50 help='Enable json logging (default is YAML)')
51 return parser
52
53
54def run_test_bamboo_feeder(args=None):
55 if not check_user_group():
56 print_group_error_message(__file__)
57 sys.exit(3)
58
59 if args is None:
60 args = get_parser().parse_args()
61
62 locallogs = []
63 exitstatus = 0
64 machine = None
65
66 try:
67 inventory = ManualCobblerSQLiteInventory(
68 db=os.path.join('~', '.utah-bamboofeeder-inventory'),
69 lockfile=os.path.join('~', '.utah-bamboofeeder-lock'))
70 kw = {}
71 for arg in ['image', 'preseed']:
72 value = vars(args)[arg]
73 if value is not None:
74 kw.update([(arg, value)])
75 machine = inventory.request(machinetype=BambooFeederMachine,
76 clean=(not args.no_destroy),
77 debug=args.debug, new=True,
78 dlpercentincrement=10, **kw)
79 exitstatus, locallogs = run_tests(args, machine)
80
81 except UTAHException as error:
82 mesg = 'Exception: ' + str(error)
83 try:
84 machine.logger.error(mesg)
85 except (AttributeError, NameError):
86 sys.stderr.write(mesg)
87 exitstatus = 2
88
89 finally:
90 if machine is not None:
91 try:
92 machine.destroy()
93 except UTAHException as error:
94 sys.stderr.write('Failed to destroy machine: ' + str(error))
95
96 if len(locallogs) != 0:
97 print('Test logs copied to the following files:')
98 print("\t" + "\n\t".join(locallogs))
99
100 sys.exit(exitstatus)
101
102
103if __name__ == '__main__':
104 run_test_bamboo_feeder()
0105
=== modified file 'examples/run_test_cobbler.py'
--- examples/run_test_cobbler.py 2012-10-04 16:11:18 +0000
+++ examples/run_test_cobbler.py 2012-10-25 20:09:20 +0000
@@ -76,7 +76,8 @@
76 value = vars(args)[arg]76 value = vars(args)[arg]
77 if value is not None:77 if value is not None:
78 kw.update([(arg, value)])78 kw.update([(arg, value)])
79 machine = inventory.request(debug=args.debug, new=True,79 machine = inventory.request(clean=(not args.no_destroy),
80 debug=args.debug, new=True,
80 dlpercentincrement=10, **kw)81 dlpercentincrement=10, **kw)
81 exitstatus, locallogs = run_tests(args, machine)82 exitstatus, locallogs = run_tests(args, machine)
8283
8384
=== modified file 'examples/run_test_vm.py'
--- examples/run_test_vm.py 2012-09-05 12:06:10 +0000
+++ examples/run_test_vm.py 2012-10-25 20:09:20 +0000
@@ -64,7 +64,8 @@
64 kw.update([(arg, value)])64 kw.update([(arg, value)])
65 if args.type is not None:65 if args.type is not None:
66 kw.update([('installtype', args.type)])66 kw.update([('installtype', args.type)])
67 machine = inventory.request(debug=args.debug, new=True,67 machine = inventory.request(clean=(not args.no_destroy),
68 debug=args.debug, new=True,
68 dlpercentincrement=10, **kw)69 dlpercentincrement=10, **kw)
69 exitstatus, locallogs = run_tests(args, machine)70 exitstatus, locallogs = run_tests(args, machine)
7071
7172
=== modified file 'examples/run_utah_tests.py'
--- examples/run_utah_tests.py 2012-10-04 16:11:18 +0000
+++ examples/run_utah_tests.py 2012-10-25 20:09:20 +0000
@@ -88,24 +88,21 @@
88 args = get_parser().parse_args()88 args = get_parser().parse_args()
8989
90 if args.arch is not None and 'arm' in args.arch:90 if args.arch is not None and 'arm' in args.arch:
91 sys.stderr.write("ARM support is not included in this release.\n")91 from run_test_bamboo_feeder import run_test_bamboo_feeder
92 sys.stderr.write("Please check the roadmap, "92 function = run_test_bamboo_feeder
93 "as it will be included in a future version.\n")93 elif args.machinetype == 'physical':
94 sys.exit(4)
95
96 function = run_test_vm
97
98 for option in [args.boot, args.emulator, args.image, args.initrd,
99 args.kernel, args.preseed, args.xml]:
100 if option is not None:
101 from run_install_test import run_install_test
102 function = run_install_test
103 break
104
105 if args.machinetype == 'physical':
106 from run_test_cobbler import run_test_cobbler94 from run_test_cobbler import run_test_cobbler
107 function = run_test_cobbler95 function = run_test_cobbler
96 else:
97 function = run_test_vm
10898
99 for option in [args.boot, args.emulator, args.image, args.initrd,
100 args.kernel, args.preseed, args.xml]:
101 if option is not None:
102 from run_install_test import run_install_test
103 function = run_install_test
104 break
105
109 function(args=args)106 function(args=args)
110107
111if __name__ == '__main__':108if __name__ == '__main__':
112109
=== modified file 'utah/config.py' (properties changed: +x to -x)
--- utah/config.py 2012-10-16 10:46:10 +0000
+++ utah/config.py 2012-10-25 20:09:20 +0000
@@ -46,12 +46,14 @@
46 nfscommand=['sudo', os.path.join('/', 'etc', 'init.d', 'nfs-kernel-server'), 'reload'],46 nfscommand=['sudo', os.path.join('/', 'etc', 'init.d', 'nfs-kernel-server'), 'reload'],
47 nfsconfigfile=os.path.join('/', 'etc', 'exports'),47 nfsconfigfile=os.path.join('/', 'etc', 'exports'),
48 nfsoptions='*(ro,async,no_root_squash,no_subtree_check)',48 nfsoptions='*(ro,async,no_root_squash,no_subtree_check)',
49 powertimeout=5,49 powertimeout=15,
50 preseed=None,50 preseed=None,
51 pxedir=os.path.join('/', 'var', 'lib', 'tftpboot', 'pxelinux.cfg'),
51 qemupath='qemu:///system',52 qemupath='qemu:///system',
52 rewrite='all',53 rewrite='all',
53 series='precise',54 series='precise',
54 template=None,55 template=None,
56 wwwdir=os.path.join('/', 'var', 'www', 'utah'),
55)57)
5658
57# These depend on the local user/path, and need to be filtered out59# These depend on the local user/path, and need to be filtered out
5860
=== added file 'utah/orderedcollections.py'
--- utah/orderedcollections.py 1970-01-01 00:00:00 +0000
+++ utah/orderedcollections.py 2012-10-25 20:09:20 +0000
@@ -0,0 +1,75 @@
1"""
2Provide additional collections to support cleanup functions.
3"""
4
5
6import collections
7
8KEY, PREV, NEXT = range(3)
9
10
11class OrderedSet(collections.MutableSet):
12
13 def __init__(self, iterable=None):
14 self.end = end = []
15 end += [None, end, end] # sentinel node for doubly linked list
16 self.map = {} # key --> [key, prev, next]
17 if iterable is not None:
18 self |= iterable
19
20 def __len__(self):
21 return len(self.map)
22
23 def __contains__(self, key):
24 return key in self.map
25
26 def add(self, key):
27 if key not in self.map:
28 end = self.end
29 curr = end[PREV]
30 curr[NEXT] = end[PREV] = self.map[key] = [key, curr, end]
31
32 def discard(self, key):
33 if key in self.map:
34 key, prev, next = self.map.pop(key)
35 prev[NEXT] = next
36 next[PREV] = prev
37
38 def __iter__(self):
39 end = self.end
40 curr = end[NEXT]
41 while curr is not end:
42 yield curr[KEY]
43 curr = curr[NEXT]
44
45 def __reversed__(self):
46 end = self.end
47 curr = end[PREV]
48 while curr is not end:
49 yield curr[KEY]
50 curr = curr[PREV]
51
52 def pop(self, last=True):
53 if not self:
54 raise KeyError('set is empty')
55 key = next(reversed(self)) if last else next(iter(self))
56 self.discard(key)
57 return key
58
59 def __repr__(self):
60 if not self:
61 return '%s()' % (self.__class__.__name__,)
62 return '%s(%r)' % (self.__class__.__name__, list(self))
63
64 def __eq__(self, other):
65 if isinstance(other, OrderedSet):
66 return len(self) == len(other) and list(self) == list(other)
67 return set(self) == set(other)
68
69 def __del__(self):
70 self.clear() # remove circular references
71
72
73class HashableDict(dict):
74 def __hash__(self):
75 return hash(tuple(sorted(self.items())))
076
=== added file 'utah/provisioning/baremetal/bamboofeeder.py'
--- utah/provisioning/baremetal/bamboofeeder.py 1970-01-01 00:00:00 +0000
+++ utah/provisioning/baremetal/bamboofeeder.py 2012-10-25 20:09:20 +0000
@@ -0,0 +1,304 @@
1"""
2Support bare-metal provisioning of bamboo-feeder-based systems.
3"""
4
5import netifaces
6import os
7import pipes
8import shutil
9import subprocess
10import tempfile
11
12from utah import config
13from utah.provisioning.provisioning import (
14 CustomInstallMixin,
15 Machine,
16 SSHMixin,
17 )
18from utah.provisioning.baremetal.power import PowerMixin
19from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException
20from utah.retry import retry
21
22
23class BambooFeederMachine(CustomInstallMixin, SSHMixin, PowerMixin, Machine):
24 """
25 Provide a class to provision an ARM board from a bamboo-feeder setup.
26 """
27 def __init__(self, boot=None, cargs=None, inventory=None,
28 name=None, powercmd=None, preboot=None, *args, **kw):
29 # TODO: change the name of cargs here and elsewhere
30 # TODO: respect rewrite setting
31 if name is None:
32 raise UTAHBMProvisioningException(
33 'Machine name reqired for bamboo-feeder machine')
34 if powercmd is not None:
35 self.powercmd = powercmd
36 elif cargs is not None:
37 self.power = {}
38 for item in cargs:
39 if 'power' in item:
40 self.power[item] = cargs[item]
41 else:
42 raise UTAHBMProvisioningException(
43 'No power control information specified')
44 try:
45 self.macaddress = cargs['mac-address']
46 except AttributeError:
47 raise UTAHBMProvisioningException(
48 'No MAC address specified')
49 self.inventory = inventory
50 super(BambooFeederMachine, self).__init__(*args, name=name, **kw)
51 if self.inventory is not None:
52 self.cleanfunction(self.inventory.release, machine=self)
53 self._depcheck()
54 if self.image is None:
55 raise UTAHBMProvisioningException(
56 'Image file required for bamboo-feeder installation')
57 try:
58 iface = config.wwwiface
59 except AttributeError:
60 iface = config.bridge
61 self.ip = netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['addr']
62 self.logger.debug('Configuring for ' + iface + ' with IP ' + self.ip)
63 self._cmdlinesetup()
64 imageurl = 'http://' + self.ip + '/utah/' + self.name + '.img'
65 preenvurl = 'http://' + self.ip + '/utah/' + self.name + '.preEnv'
66 if preboot is None:
67 self.preboot = ('console=ttyO2,115200n8 imageurl={imageurl} '
68 'bootcfg={preenvurl}'.format(
69 imageurl=imageurl, preenvurl=preenvurl))
70 else:
71 # TODO: maybe make this automatically add needed options
72 self.preboot = preboot
73 self.logger.debug('Preboot setup:')
74 self.logger.debug(self.preboot)
75 self.logger.debug('BambooFeederMachine init finished')
76
77 def _prepareimage(self):
78 """
79 Make a copy of the image shared via wwa so we can edit it.
80 """
81 self.logger.info('Making copy of install image')
82 self.wwwimage = os.path.join(config.wwwdir, self.name + '.img')
83 self.logger.debug('Copying ' + self.image + ' to ' + self.wwwimage)
84 self.cleanfile(self.wwwimage)
85 shutil.copyfile(self.image, self.wwwimage)
86 return self.wwwimage
87
88 def _mountimage(self):
89 """
90 Mount an ARM boot image so we can edit it.
91 """
92 self.logger.info('Mounting install image')
93 self.imagedir = os.path.join(self.tmpdir, 'image.d')
94 if not os.path.isdir(self.imagedir):
95 os.makedirs(self.imagedir)
96 self.sector = int(subprocess.check_output(['file',
97 self.wwwimage]
98 ).strip().split(';')[1].split(',')[3].split()[1])
99 self.logger.debug('Image start sector is ' + str(self.sector))
100 self.cleanfunction(self._umountimage)
101 self.logger.debug('Mounting ' + self.wwwimage + ' at ' +
102 self.imagedir)
103 self._runargs(['sudo', 'mount', self.wwwimage, self.imagedir,
104 '-o', 'loop', '-o', 'offset=' + str(self.sector*512),
105 '-o', 'uid=' + config.user])
106
107 def _setupconsole(self):
108 """
109 Setup the install to use a serial console."
110 """
111 self.logger.info('Setting up the install to use the serial console')
112 preenvfile = os.path.join(self.imagedir, 'preEnv.txt')
113 serialpreenvfile = os.path.join(self.imagedir, 'preEnv.txt-serial')
114 self.logger.debug('Copying ' + serialpreenvfile + ' to ' + preenvfile)
115 shutil.copyfile(serialpreenvfile, preenvfile)
116
117 def _unpackinitrd(self):
118 """
119 Unpack the uInitrd file into a directory so we can modify it.
120 """
121 self.logger.info('Unpacking uInitrd')
122 if not os.path.isdir(os.path.join(self.tmpdir, 'initrd.d')):
123 os.makedirs(os.path.join(self.tmpdir, 'initrd.d'))
124 os.chdir(os.path.join(self.tmpdir, 'initrd.d'))
125 filesize = os.path.getsize(self.initrd)
126 for line in subprocess.check_output(
127 ['mkimage', '-l', self.initrd]).splitlines():
128 if 'Data Size:' in line:
129 datasize = int(line.split()[2])
130 break
131 headersize = filesize - datasize
132 self.logger.debug('uInitrd header size is ' + str(headersize))
133 pipe = pipes.Template()
134 pipe.prepend('dd if=$IN bs=1 skip=' + str(headersize), 'f-')
135 pipe.append('gunzip', '--')
136 pipe.append('cpio -ivd 2>/dev/null', '-.')
137 # TODO: suppress dd output
138 pipe.copy(self.initrd, '/dev/null')
139
140 def _repackinitrd(self):
141 """
142 Repack the uInitrd file from the initrd.d directory.
143 """
144 self.logger.info('Repacking uInitrd')
145 os.chdir(os.path.join(self.tmpdir, 'initrd.d'))
146 pipe = pipes.Template()
147 pipe.prepend('find .', '.-')
148 pipe.append('cpio --quiet -o -H newc', '--')
149 pipe.append('gzip -9fc', '--')
150 initrd = os.path.join(self.tmpdir, 'initrd.gz')
151 self.logger.debug('Repacking compressed initrd')
152 pipe.copy('/dev/null', initrd)
153 self.logger.debug('Creating uInitrd with mkimage')
154 self._runargs(['mkimage',
155 '-A', 'arm',
156 '-O', 'linux',
157 '-T', 'ramdisk',
158 '-C', 'gzip',
159 '-a', '0',
160 '-e', '0',
161 '-n', 'initramfs',
162 '-d', initrd,
163 self.initrd])
164
165 def _umountimage(self):
166 """
167 Unmount the image after we're done with it."
168 """
169 self.logger.info('Unmounting image')
170 self._runargs(['sudo', 'umount', self.imagedir])
171
172 def _cmdlinesetup(self, boot=None):
173 """
174 Add the needed options to the command line for an automatic install.
175 """
176 if boot is None:
177 boot = ''
178 super(BambooFeederMachine, self)._cmdlinesetup(boot=boot)
179 # TODO: minimize these
180 for option in ('auto', 'ro', 'text'):
181 if option not in self.cmdline:
182 self.logger.info('Adding boot option: ' + option)
183 self.cmdline += ' ' + option
184 for parameter in (('cdrom-detect/try_usb', 'true'),
185 ('console', 'ttyO2,115200n8'),
186 ('country', 'US'),
187 ('hostname', self.name),
188 ('language', 'en'),
189 ('locale', 'en_US'),
190 ('loghost', self.ip),
191 ('log_port', '10514'),
192 ('netcfg/choose_interface', 'auto'),
193 ('url', 'http://' + self.ip + '/utah/' + self.name +
194 '.cfg'),
195 ):
196 if parameter[0] not in self.cmdline:
197 self.logger.info('Adding boot option: ' + '='.join(parameter))
198 self.cmdline += ' ' + '='.join(parameter)
199 self.cmdline.strip()
200
201 def _configurepxe(self):
202 """
203 Setup PXE configuration to boot remote image.
204 """
205 # TODO: Maybe move this into pxe.py
206 # TODO: look into cutting out the middleman/
207 # booting straight into the installer? (maybe nfs?)
208 self.logger.info('Configuring PXE')
209 pxeconfig = """default utah-bamboofeeder
210prompt 0
211timeout 3
212
213label utah/bamboofeeder
214kernel panda/uImage
215append {preboot}
216initrd panda/uInitrd
217""".format(preboot=self.preboot)
218 tmppxefile = os.path.join(self.tmpdir, 'pxe')
219 open(tmppxefile, 'w').write(pxeconfig)
220 self.logger.debug('PXE info written to ' + tmppxefile)
221 pxefile = os.path.join(config.pxedir,
222 '01-' + self.macaddress.replace(':', '-'))
223 self.cleancommand(('sudo', 'rm', '-f', pxefile))
224 self.logger.debug('Copying ' + tmppxefile + ' to ' + pxefile)
225 self._runargs(['sudo', 'cp', tmppxefile, pxefile])
226 preenvfile = os.path.join(config.wwwdir, self.name + '.preEnv')
227 # TODO: sort this out with self.boot
228 # figure out which one should be what and customizable
229 self.preenv = 'bootargs=' + self.cmdline
230 self.logger.debug('Preenv setup:')
231 self.logger.debug(self.preenv)
232 self.cleanfile(preenvfile)
233 self.logger.debug('Writing preenv setup to ' + preenvfile)
234 open(preenvfile, 'w').write(self.preenv)
235
236 def _create(self):
237 """
238 Install the OS on the system.
239 """
240 # TODO: more checks and exceptions for failures
241 self.logger.info('Preparing system install')
242 self.tmpdir = tempfile.mkdtemp(prefix='/tmp/' + self.name + '_')
243 self.cleanfile(self.tmpdir)
244 self.logger.debug('Using ' + self.tmpdir + ' as temp dir')
245 os.chdir(self.tmpdir)
246 self._prepareimage()
247 self._mountimage()
248 self._setupconsole()
249 self.initrd = os.path.join(self.imagedir, 'uInitrd')
250 self.logger.debug('uInitrd is ' + self.initrd)
251 self._unpackinitrd()
252 self._setuplatecommand()
253 # TODO: check if this is still needed
254 if self.installtype == 'desktop':
255 self.logger.info('Configuring latecommand for desktop')
256 myfile = open(os.path.join(self.tmpdir, 'initrd.d',
257 'utah-latecommand'), 'a')
258 myfile.write(
259"""
260chroot /target sh -c 'sed "/eth[0-9]/d" -i /etc/network/interfaces'
261cp /etc/resolv.conf /target/etc/resolv.conf
262""")
263 myfile.close()
264 self._setuppreseed()
265 self.logger.debug('Copying preseed to download location')
266 preseedfile = os.path.join(config.wwwdir, self.name + '.cfg')
267 self.cleanfile(preseedfile)
268 shutil.copyfile(os.path.join(self.tmpdir, 'initrd.d', 'preseed.cfg'),
269 preseedfile)
270 self._repackinitrd()
271 self._configurepxe()
272 self._umountimage()
273 self.restart()
274 self.logger.info('System installing')
275 self.logger.info('Checking every ' + str(config.checktimeout) +
276 ' seconds until system is installed')
277 self.logger.info('Will time out after ' + str(config.installtimeout)
278 + ' seconds')
279 retry(self.sshcheck, config.checktimeout, logmethod=self.logger.info)
280
281 self.provisioned = True
282 self.active = True
283 # TODO: Make this a method that this and CobblerMachine can call
284 uuid_check_command = ('[ "{uuid}" == "$(cat /etc/utah/uuid)" ]'
285 .format(uuid=self.uuid))
286 if self.run(uuid_check_command)[0] != 0:
287 self.provisioned = False
288 raise UTAHBMProvisioningException(
289 'Installed UUID differs from Machine UUID; '
290 'installation probably failed. '
291 'Try restarting cobbler or running cobbler sync')
292 self.logger.info('System installed')
293 self.cleanfunction(self.run, ('dd', 'bs=512k', 'count=10',
294 'if=/dev/zero', 'of=/dev/mmcblk0'), root=True)
295 return True
296
297 def _depcheck(self):
298 """
299 Check for dependencies that are in Recommends or Suggests.
300 """
301 super(BambooFeederMachine, self)._depcheck()
302 cmd = ['which', 'mkimage']
303 if self._runargs(cmd) != 0:
304 raise UTAHBMProvisioningException('u-boot-tools not installed')
0305
=== added file 'utah/provisioning/baremetal/power.py'
--- utah/provisioning/baremetal/power.py 1970-01-01 00:00:00 +0000
+++ utah/provisioning/baremetal/power.py 2012-10-25 20:09:20 +0000
@@ -0,0 +1,60 @@
1"""
2Provide functions for a machine to interface with a power management system.
3Currently only the Sentry CDU in use in the Canonical labs is supported.
4"""
5
6import time
7
8from utah import config
9from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException
10
11
12class PowerMixin(object):
13 """
14 Provide power cycle commands for the Sentry CDU.
15 """
16 def powercommand(self):
17 try:
18 cmd = self.powercmd
19 except AttributeError:
20 try:
21 if self.power['power-type'] == 'sentryswitch_cdu':
22 cmd = ['/usr/sbin/fence_cdu',
23 '-a', self.power['power-address'],
24 '-n', self.power['power-id'],
25 '-l', self.power['power-user'],
26 '-p', self.power['power-pass'],
27 '-o']
28 else:
29 raise UTAHProvisioningException('Power type ' +
30 self.power['power-type'] +
31 ' not supported')
32 except AttributeError:
33 raise UTAHBMProvisioningException('Power commands not defined'
34 ' for this machine')
35 return cmd
36
37 def _start(self):
38 """
39 Use the machine's powercommand to start the machine.
40 """
41 self.logger.debug('Starting system')
42 self._runargs(self.powercommand() + ['on'])
43
44 def stop(self):
45 """
46 Use the machine's powercommand to stop the machine.
47 """
48 self.logger.debug('Stopping system')
49 self._runargs(self.powercommand() + ['off'])
50
51 def restart(self):
52 """
53 Stop the machine, wait powertimeout, then start it.
54 """
55 self.logger.info('Restarting system')
56 self.stop()
57 self.logger.debug('Waiting ' + str(config.powertimeout) + ' seconds')
58 time.sleep(config.powertimeout)
59 self._start()
60
061
=== modified file 'utah/provisioning/inventory/sqlite.py'
--- utah/provisioning/inventory/sqlite.py 2012-08-22 15:20:25 +0000
+++ utah/provisioning/inventory/sqlite.py 2012-10-25 20:09:20 +0000
@@ -83,6 +83,7 @@
83 All columns other than machineid, name, and state are assumed to be83 All columns other than machineid, name, and state are assumed to be
84 arguments for cobbler system creation.84 arguments for cobbler system creation.
85 """85 """
86 # TODO: rename this
86 def __init__(self, db='~/.utah-cobbler-inventory',87 def __init__(self, db='~/.utah-cobbler-inventory',
87 lockfile='~/.utah-cobbler-lock', *args, **kw):88 lockfile='~/.utah-cobbler-lock', *args, **kw):
88 db = os.path.expanduser(db)89 db = os.path.expanduser(db)
@@ -99,7 +100,7 @@
99 raise UTAHProvisioningInventoryException('No machines in database')100 raise UTAHProvisioningInventoryException('No machines in database')
100 self.machines = []101 self.machines = []
101102
102 def request(self, name=None, *args, **kw):103 def request(self, machinetype=CobblerMachine, name=None, *args, **kw):
103 query = 'SELECT * FROM machines'104 query = 'SELECT * FROM machines'
104 queryvars = []105 queryvars = []
105 if name is not None:106 if name is not None:
@@ -121,7 +122,7 @@
121 "WHERE machineid=? AND state='available'",122 "WHERE machineid=? AND state='available'",
122 [machineid]).rowcount123 [machineid]).rowcount
123 if update == 1:124 if update == 1:
124 machine = CobblerMachine(*args, cargs=cargs,125 machine = machinetype(*args, cargs=cargs,
125 inventory=self, name=name, **kw)126 inventory=self, name=name, **kw)
126 self.machines.append(machine)127 self.machines.append(machine)
127 return machine128 return machine
128129
=== modified file 'utah/provisioning/provisioning.py'
--- utah/provisioning/provisioning.py 2012-10-16 10:46:10 +0000
+++ utah/provisioning/provisioning.py 2012-10-25 20:09:20 +0000
@@ -19,7 +19,12 @@
19import apt.cache19import apt.cache
20from glob import glob20from glob import glob
2121
22from utah.commandstr import commandstr
22from utah.iso import ISO23from utah.iso import ISO
24from utah.orderedcollections import (
25 HashableDict,
26 OrderedSet,
27 )
23from utah.preseed import Preseed28from utah.preseed import Preseed
24from utah.provisioning.exceptions import UTAHProvisioningException29from utah.provisioning.exceptions import UTAHProvisioningException
25from utah.retry import retry30from utah.retry import retry
@@ -37,16 +42,17 @@
37 A fully implemented subclass will provide all public methods except read,42 A fully implemented subclass will provide all public methods except read,
38 I.E.:43 I.E.:
3944
40 * provisioncheck, activecheck, getclientdeb, installclient45 * provisioncheck, activecheck, getclientdeb, installclient, destroy
41 (all these can be used from Machine or replaced.)46 (all these can be used from Machine or replaced.)
42 * destroy, stop, uploadfiles, downloadfiles, run47 * stop, uploadfiles, downloadfiles, run
43 (these must be implemented separately.)48 (these must be implemented separately.)
44 """49 """
45 def __init__(self, arch=None, debug=False, directory=None, image=None,50 def __init__(self, arch=None, clean=None, debug=False, directory=None,
46 dlpercentincrement=1, initrd=None, installtype=None,51 image=None, dlpercentincrement=1, initrd=None,
47 kernel=None, machineid=None, machineuuid=None, name=None,52 installtype=None, kernel=None, machineid=None,
48 new=False, prefix='utah', preseed=None, rewrite=None,53 machineuuid=None, name=None, new=False, prefix='utah',
49 series=None, template=None, xml=None):54 preseed=None, rewrite=None, series=None, template=None,
55 xml=None):
50 """56 """
51 Initialize the object representing the machine.57 Initialize the object representing the machine.
52 One of these groups of arguments should be included:58 One of these groups of arguments should be included:
@@ -60,6 +66,7 @@
60 reinstall a specific machine.66 reinstall a specific machine.
61 The subclass is responsible for provisioning with these arguments.67 The subclass is responsible for provisioning with these arguments.
62 Other arguments:68 Other arguments:
69 clean: Enable cleanup functions.
63 debug: Enable debug logging.70 debug: Enable debug logging.
64 directory: Where the machine's files go. Should be persistent for71 directory: Where the machine's files go. Should be persistent for
65 VMs, temporary is fine for installation-only files, like those72 VMs, temporary is fine for installation-only files, like those
@@ -99,6 +106,15 @@
99 self.series = series106 self.series = series
100 self.template = template107 self.template = template
101108
109 if clean is None:
110 self.clean = True
111 else:
112 self.clean = clean
113
114 self.cleanfiles = OrderedSet()
115 self.cleanfunctions = OrderedSet()
116 self.cleancommands = OrderedSet()
117
102 self._namesetup(name)118 self._namesetup(name)
103 self._dirsetup(directory)119 self._dirsetup(directory)
104120
@@ -116,14 +132,21 @@
116 self.active = False132 self.active = False
117 self._loggersetup()133 self._loggersetup()
118134
135 if preseed is None:
136 preseed = '/etc/utah/default-preseed.cfg'
137
138 fileargs = ['preseed', 'xml', 'kernel', 'initrd']
139
119 if image is None:140 if image is None:
120 self.image = None141 self.image = None
121 else:142 elif image.endswith('.iso'):
122 self.image = ISO(image,143 self.image = ISO(image,
123 dlpercentincrement=self.dlpercentincrement,144 dlpercentincrement=self.dlpercentincrement,
124 logger=self.logger)145 logger=self.logger)
146 else:
147 fileargs.append('image')
125148
126 for item in ['preseed', 'xml', 'kernel', 'initrd']:149 for item in fileargs:
127 # Ensure every file/url type argument is available locally150 # Ensure every file/url type argument is available locally
128 arg = locals()[item]151 arg = locals()[item]
129 if arg is None:152 if arg is None:
@@ -159,6 +182,7 @@
159 """182 """
160 Setup the machine's directory, automatically or using a specified one.183 Setup the machine's directory, automatically or using a specified one.
161 """184 """
185 # TODO: Move this to vm
162 if directory is None:186 if directory is None:
163 self.directory = os.path.join(config.vmpath, self.name)187 self.directory = os.path.join(config.vmpath, self.name)
164 else:188 else:
@@ -340,7 +364,15 @@
340 Should generally not be called directly outside of the class;364 Should generally not be called directly outside of the class;
341 provisioncheck() or activecheck() should be used.365 provisioncheck() or activecheck() should be used.
342 """366 """
343 self._unimplemented(sys._getframe().f_code.co_name)367 if not self.new:
368 try:
369 self.logger.debug('Trying to load existing machine')
370 self._load()
371 self.provisioned = True
372 return self.provisioned
373 except Exception as err:
374 self.logger.debug('Failed to load machine: ' + str(err))
375 self._create()
344376
345 def _create(self):377 def _create(self):
346 """378 """
@@ -364,7 +396,8 @@
364 should be powered off if remote power management is available.396 should be powered off if remote power management is available.
365 Now returns False by default to work with multiple inheritance.397 Now returns False by default to work with multiple inheritance.
366 """398 """
367 return False399 self.__del__()
400 del self
368401
369 def _load(self):402 def _load(self):
370 """403 """
@@ -470,11 +503,95 @@
470 line = p.stdout.readline().strip()503 line = p.stdout.readline().strip()
471 self.logger.debug(line)504 self.logger.debug(line)
472 return p.returncode505 return p.returncode
506
507 def _depcheck(self):
508 """
509 Check for dependencies that are in Recommends or Suggests.
510 None exist for the main class, but we implement it here to allow
511 super() to work.
512 """
513 pass
473514
474 def _unimplemented(self, method):515 def _unimplemented(self, method):
475 raise UTAHProvisioningException(self.__class__.__name__ +516 raise UTAHProvisioningException(self.__class__.__name__ +
476 ' attempted to call the ' + method +517 ' attempted to call the ' + method +
477 ' method of the base Machine class, which is not implemented')518 ' method of the base Machine class, which is not implemented')
519
520 def cleanup(self):
521 """
522 Clean up temporary files and tear down other setup (VM, etc.).
523 If a subset is specified, only that subset will be cleaned.
524 This can be used to clean up install files after the install has started.
525 Files are removed first.
526 Functions are run second.
527 Commands are run third.
528 """
529 if self.clean:
530 self.logger.debug('Running cleanup')
531 for path in tuple(self.cleanfiles):
532 if os.path.islink(path):
533 self.logger.debug('Removing link ' + path)
534 os.unlink(path)
535 elif os.path.isfile(path):
536 self.logger.debug('Changing permissions of '+ path)
537 os.chmod(path, 0664)
538 self.logger.debug('Removing file ' + path)
539 os.unlink(path)
540 elif os.path.isdir(path):
541 # Cribbed from http://svn.python.org
542 # /projects/python/trunk/Mac/BuildScript/build-installer.py
543 for dirpath, dirnames, filenames in os.walk(path):
544 for name in (dirnames + filenames):
545 absolute_name = os.path.join(dirpath, name)
546 if not os.path.islink(absolute_name):
547 self.logger.debug('Changing permissions of '
548 + absolute_name)
549 os.chmod(absolute_name, 0775)
550 self.logger.debug('Recursively Removing directory '
551 + path)
552 shutil.rmtree(path)
553 else:
554 self.logger.debug(path + ' is not a link, file, or '
555 'directory; not removing')
556 self.cleanfiles.remove(path)
557 for function in tuple(self.cleanfunctions):
558 timeout, command, args, kw = function
559 self.logger.debug('Running: ' +
560 commandstr(command, *args, **kw))
561 utah.timeout.timeout(timeout, command, *args, **kw)
562 self.cleanfunctions.remove(function)
563 for command in tuple(self.cleancommands):
564 self._runargs(command)
565 self.cleancommands.remove(command)
566 else:
567 self.logger.debug('Not cleaning up')
568
569 def cleanfile(self, path):
570 """
571 Register a link, file, or directory to be cleaned later.
572 Links will be unlinked.
573 Files will have permissions changed to 664 and unlinked.
574 Directories will recursively have permissions changed to 775 (except
575 for links contained within) and then be recursively removed.
576 """
577 self.cleanfiles.add(path)
578
579 def cleanfunction(self, function, *args, **kw):
580 """
581 Register a function to be run on cleanup.
582 Functions are run through utah.timeout.timeout.
583 """
584 self.cleanfunctions.add((60, function, args, HashableDict(kw)))
585
586 def cleancommand(self, cmd):
587 """
588 Register a command to be run on cleanup.
589 Commands are run through Machine._runargs.
590 """
591 self.cleancommands.add(cmd)
592
593 def __del__(self):
594 self.cleanup()
478595
479596
480class SSHMixin(object):597class SSHMixin(object):
@@ -1010,6 +1127,7 @@
1010 else:1127 else:
1011 self.cmdline = boot1128 self.cmdline = boot
1012 self.boottimeout = config.boottimeout1129 self.boottimeout = config.boottimeout
1130 # TODO: Refactor this into lists like BambooFeederMachine
1013 if self.rewrite == 'all':1131 if self.rewrite == 'all':
1014 self.logger.info('Adding needed command line options')1132 self.logger.info('Adding needed command line options')
1015 if self.installtype == 'desktop':1133 if self.installtype == 'desktop':
10161134
=== modified file 'utah/provisioning/vm/libvirtvm.py'
--- utah/provisioning/vm/libvirtvm.py 2012-10-16 10:46:10 +0000
+++ utah/provisioning/vm/libvirtvm.py 2012-10-25 20:09:20 +0000
@@ -349,8 +349,7 @@
349 def __init__(self, arch=None, boot=None, diskbus=None, disksizes=None,349 def __init__(self, arch=None, boot=None, diskbus=None, disksizes=None,
350 emulator=None, image=None, initrd=None, installtype=None,350 emulator=None, image=None, initrd=None, installtype=None,
351 kernel=None, machineid=None, macs=None, name=None,351 kernel=None, machineid=None, macs=None, name=None,
352 prefix='utah', preseed=None, series=None, xml=None,352 prefix='utah', series=None, xml=None, *args, **kw):
353 *args, **kw):
354 # Make sure that no other virtualization solutions are running353 # Make sure that no other virtualization solutions are running
355 # TODO: see if this is needed for qemu or just kvm354 # TODO: see if this is needed for qemu or just kvm
356 process_checker = ProcessChecker()355 process_checker = ProcessChecker()
@@ -373,15 +372,12 @@
373 name = '-'.join([str(prefix), str(machineid)])372 name = '-'.join([str(prefix), str(machineid)])
374 else:373 else:
375 autoname = False374 autoname = False
376 if preseed is None:
377 preseed = '/etc/utah/default-preseed.cfg'
378 if xml is None:375 if xml is None:
379 xml = '/etc/utah/default-vm.xml'376 xml = '/etc/utah/default-vm.xml'
380 super(CustomVM, self).__init__(arch=arch, image=image, initrd=initrd,377 super(CustomVM, self).__init__(arch=arch, image=image, initrd=initrd,
381 installtype=installtype, kernel=kernel,378 installtype=installtype, kernel=kernel,
382 machineid=machineid, name=name,379 machineid=machineid, name=name,
383 preseed=preseed, series=series, xml=xml,380 series=series, xml=xml, *args, **kw)
384 *args, **kw)
385 # TODO: do a better job of separating installation381 # TODO: do a better job of separating installation
386 # into _create rather than __init__382 # into _create rather than __init__
387 if self.image is None:383 if self.image is None:

Subscribers

People subscribed via source and target branches