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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Joe Talbott (community) | Approve | ||
Max Brustkern (community) | Needs Resubmitting | ||
Review via email:
|
Commit message
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_
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: ManualCobblerSQ
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.
- 731. By Max Brustkern
-
Removed duplicate class definition
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
- 732. By Max Brustkern
-
vm-toools had tooo many ooos
- 733. By Max Brustkern
-
clearn -> clean
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
Fixed those.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Joe Talbott (joetalbott) wrote : | # |
Awesome. Go for it.
Preview Diff
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: |
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.