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

Proposed by Max Brustkern
Status: Merged
Approved by: Javier Collado
Approved revision: 659
Merged at revision: 663
Proposed branch: lp:~nuclearbob/utah/isoclass
Merge into: lp:utah
Diff against target: 929 lines (+258/-257)
17 files modified
utah/config.py (+3/-1)
utah/exceptions.py (+3/-1)
utah/iso.py (+139/-3)
utah/provisioning/baremetal/cobbler.py (+27/-12)
utah/provisioning/baremetal/exceptions.py (+3/-1)
utah/provisioning/exceptions.py (+3/-2)
utah/provisioning/inventory/bootspeed.py (+0/-116)
utah/provisioning/inventory/exceptions.py (+3/-1)
utah/provisioning/inventory/inventory.py (+1/-1)
utah/provisioning/inventory/sqlite.py (+1/-1)
utah/provisioning/provisioning.py (+22/-89)
utah/provisioning/vm/exceptions.py (+3/-1)
utah/provisioning/vm/isotest.py (+0/-15)
utah/provisioning/vm/libvirtvm.py (+38/-8)
utah/provisioning/vm/vm.py (+5/-4)
utah/run.py (+3/-0)
utah/timeout.py (+4/-1)
To merge this branch: bzr merge lp:~nuclearbob/utah/isoclass
Reviewer Review Type Date Requested Status
Javier Collado (community) Approve
Review via email: mp+121279@code.launchpad.net

Description of the change

I created an ISO class and moved many of the existing utah.iso calls into it. I haven't tried to convert the stuff in isotesting yet, but the stuff in provisioning should be moved over.

To post a comment you must log in.
Revision history for this message
Javier Collado (javier.collado) wrote :

Looked at the changes and they look nice. Also provisioned a VM from scratch using this branch and worked fine. Thanks.

My only comment is that please try to avoid unrelated changes in merge requests that fix a bug. I small refactoring is nice, but I think that removing a whole file and adding a lot of documentation strings should be part of a separate merge request.

review: Approve
Revision history for this message
Javier Collado (javier.collado) wrote :

Anyway, I've to say that changes were properly separated in different commits, so it wasn't so difficult to review them (maybe I was a little bit picky in my comment above).

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

I meant to do those separately, I must have just based this branch on my working copy or something instead of the branch in launchpad. I'll keep better track of that in the future to keep them separated.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'utah/config.py'
--- utah/config.py 2012-08-13 20:37:09 +0000
+++ utah/config.py 2012-08-24 20:25:19 +0000
@@ -75,8 +75,10 @@
75CONFIG = {}75CONFIG = {}
76CONFIG.update(DEFAULTS)76CONFIG.update(DEFAULTS)
7777
78"""
79Process config files.
80"""
78files = ['/etc/utah/config', '~/.utah/config', os.getenv('UTAH_CONFIG_FILE')]81files = ['/etc/utah/config', '~/.utah/config', os.getenv('UTAH_CONFIG_FILE')]
79
80for conffile in files:82for conffile in files:
81 if conffile is not None:83 if conffile is not None:
82 myfile = os.path.expanduser(conffile)84 myfile = os.path.expanduser(conffile)
8385
=== modified file 'utah/exceptions.py'
--- utah/exceptions.py 2012-07-19 13:25:30 +0000
+++ utah/exceptions.py 2012-08-24 20:25:19 +0000
@@ -1,4 +1,6 @@
1#!/usr/bin/python1"""
2Provide exceptions for the rest of the tool.
3"""
24
35
4class UTAHException(Exception):6class UTAHException(Exception):
57
=== modified file 'utah/iso.py'
--- utah/iso.py 2012-08-13 20:37:09 +0000
+++ utah/iso.py 2012-08-24 20:25:19 +0000
@@ -1,12 +1,17 @@
1"""1"""
2All commands use bsdtar and accept a logmethod to use for logging.2All commands use bsdtar and accept a logmethod to use for logging.
3TODO: Make an iso class that can return things like arch, series, type, etc.
4TODO: Add the existing functions to the iso class.
5TODO: Convert the code to use the class instead of the existing functions.
6"""3"""
4#TODO: Make an iso class that can return things like arch, series, type, etc.
5#TODO: Add the existing functions to the iso class.
6#TODO: Convert the code to use the class instead of the existing functions.
7import logging
8import logging.handlers
7import os9import os
8import subprocess10import subprocess
11import sys
12import urllib
913
14from utah import config
10from utah.exceptions import UTAHException15from utah.exceptions import UTAHException
1116
1217
@@ -104,3 +109,134 @@
104 """109 """
105 cmd = getrealfile(image, filename, logmethod=logmethod)110 cmd = getrealfile(image, filename, logmethod=logmethod)
106 return subprocess.Popen(cmd, stdout=subprocess.PIPE, **kw)111 return subprocess.Popen(cmd, stdout=subprocess.PIPE, **kw)
112
113
114class ISO(object):
115 """
116 Provide a simplified method of interfacing with images.
117 """
118 def __init__(self, image, dlpercentincrement=1, logger=None):
119 self.dlpercentincrement = dlpercentincrement
120 if logger is None:
121 self._loggersetup()
122 else:
123 self.logger = logger
124 if image.startswith('~'):
125 path = os.path.expanduser(image)
126 self.logger.debug('Rewriting ~-based path: ' + image + ' to: ' + path)
127 else:
128 path = image
129
130 self.percent = 0
131 self.logger.info('Preparing image: ' + path)
132 self.image = urllib.urlretrieve(path, reporthook=self.dldisplay)[0]
133 self.logger.debug(path + ' is locally available as ' + self.image)
134 self.installtype = self.getinstalltype()
135 if self.installtype == 'mini':
136 self.logger.debug('Using mini image')
137 self.infofile = '.disk/mini-info'
138 else:
139 self.logger.debug('Using normal image')
140 self.infofile = './.disk/info'
141 # Info file looks like this:
142 # Ubuntu 12.04 LTS "Precise Pangolin" - Release i386 (20120423)
143 # or this:
144 # Ubuntu 12.10 "Quantal Quetzal" - Alpha amd64 (20120820)
145 # i.e. Ubuntu Version (LTS) "Series Codename" - Alpha/Beta/Release
146 # arch (buildnumber)
147 self.arch = self.getarch()
148 self.series = self.getseries()
149
150 def _loggersetup(self):
151 """
152 Initialize the logging for the image.
153 """
154 self.logger = logging.getLogger('utah.iso')
155 self.logger.propagate = False
156 self.logger.setLevel(logging.INFO)
157 for handler in list(self.logger.handlers):
158 self.logger.removeHandler(handler)
159 del handler
160 self.consolehandler = logging.StreamHandler(stream=sys.stderr)
161 console_formatter = logging.Formatter('%(levelname)s: %(message)s')
162 self.consolehandler.setFormatter(console_formatter)
163 self.consolehandler.setLevel(config.consoleloglevel)
164 self.logger.addHandler(self.consolehandler)
165 self.filehandler = logging.handlers.WatchedFileHandler(config.logfile)
166 file_formatter = logging.Formatter('%(asctime)s iso '
167 + ' %(levelname)s: %(message)s')
168 self.filehandler.setFormatter(file_formatter)
169 self.filehandler.setLevel(config.fileloglevel)
170 self.logger.addHandler(self.filehandler)
171 if config.debuglog is not None:
172 self.logger.setLevel(logging.DEBUG)
173 self.debughandler = (
174 logging.handlers.WatchedFileHandler(config.debuglog))
175 self.debughandler.setFormatter(file_formatter)
176 self.debughandler.setLevel(logging.DEBUG)
177 self.logger.addHandler(self.debughandler)
178
179 def getinstalltype(self):
180 """
181 Inspect the image's files to get the image type.
182 If .disk/mini-info exists, it's mini.
183 If the casper directory exists, it's desktop.
184 If ubuntu-server.seed exists in the preseeds directory, it's server.
185 """
186 self.logger.info('Getting image type of ' + self.image)
187 files = self.listfiles(returnlist=True)
188 installtype = 'alternate'
189 if '.disk/mini-info' in files:
190 installtype = 'mini'
191 elif './casper' in files:
192 installtype = 'desktop'
193 elif './preseed/ubuntu-server.seed' in files:
194 installtype = 'server'
195 self.logger.info('Image type is: ' + installtype)
196 return installtype
197
198 def getarch(self):
199 """
200 Unpack the image's info file to get the arch.
201 """
202 stdout = self.dump(self.infofile).communicate()[0]
203 arch = stdout.split()[-2]
204 self.logger.info('Arch is: ' + arch)
205 return arch
206
207 def getseries(self):
208 """
209 Unpack the image's info file to get the series.
210 """
211 stdout = self.dump(self.infofile).communicate()[0]
212 for word in stdout.split():
213 if word.startswith('"'):
214 series = word.strip('"').lower()
215 break
216 self.logger.info('Series is ' + series)
217 return series
218
219 def dldisplay(self, blocks, size, total):
220 read = blocks * size
221 percent = 100 * read / total
222 if percent >= self.percent:
223 self.logger.info('File ' + str(percent) + '% downloaded')
224 self.percent += self.dlpercentincrement
225 self.logger.debug("%s read, %s%% of %s total" % (read, percent, total))
226
227 def listfiles(self, returnlist=False):
228 return listfiles(self.image, self.logger.debug, returnlist=returnlist)
229
230 def getrealfile(self, filename, outdir=None):
231 return getrealfile(self.image, filename, outdir=outdir, logmethod=self.logger.debug)
232
233 def extract(self, filename, outdir=None, outfile=None, **kw):
234 return extract(self.image, filename, outdir, outfile, logmethod=self.logger.debug, **kw)
235
236 def dump(self, filename, **kw):
237 return dump(self.image, filename, logmethod=self.logger.debug, **kw)
238
239 def extractall(self):
240 for myfile in self.listfiles(returnlist=True):
241 self.extract(myfile)
242
107243
=== modified file 'utah/provisioning/baremetal/cobbler.py'
--- utah/provisioning/baremetal/cobbler.py 2012-08-20 10:20:44 +0000
+++ utah/provisioning/baremetal/cobbler.py 2012-08-24 20:25:19 +0000
@@ -1,4 +1,6 @@
1#!/usr/bin/python1"""
2Support bare metal provisioning through cobbler.
3"""
24
3import os5import os
4import shutil6import shutil
@@ -12,23 +14,22 @@
12 Machine,14 Machine,
13 SSHMixin,15 SSHMixin,
14 )16 )
15from utah.provisioning.exceptions import UTAHProvisioningException17from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException
16from utah.retry import retry18from utah.retry import retry
1719
1820
19class CobblerMachine(CustomInstallMixin, SSHMixin, Machine):21class CobblerMachine(CustomInstallMixin, SSHMixin, Machine):
20 """22 """
21 Provide a class to provision a machine via cobbler.23 Provide a class to provision a machine via cobbler.
22
23 """24 """
24 # TODO: may need to fix DHCP/hostname issues, may just be magners25 # TODO: may need to fix DHCP/hostname issues, may just be magners
25 def __init__(self, boot=None, cargs=None, inventory=None,26 def __init__(self, boot=None, cargs=None, inventory=None,
26 name=None, preseed=None, *args, **kw):27 name=None, preseed=None, *args, **kw):
27 if name is None:28 if name is None:
28 raise UTAHProvisioningException(29 raise UTAHBMProvisioningException(
29 'Machine name reqired for cobbler machine')30 'Machine name reqired for cobbler machine')
30 if cargs is None:31 if cargs is None:
31 raise UTAHProvisioningException(32 raise UTAHBMProvisioningException(
32 'No cobbler arguments given for machine creation')33 'No cobbler arguments given for machine creation')
33 else:34 else:
34 self.cargs = cargs35 self.cargs = cargs
@@ -38,7 +39,7 @@
38 super(CobblerMachine, self).__init__(*args, name=name,39 super(CobblerMachine, self).__init__(*args, name=name,
39 preseed=preseed, **kw)40 preseed=preseed, **kw)
40 if self.image is None:41 if self.image is None:
41 raise UTAHProvisioningException(42 raise UTAHBMProvisioningException(
42 'Image file required for cobbler installation')43 'Image file required for cobbler installation')
43 self._custominit(arch=self.arch, boot=boot,44 self._custominit(arch=self.arch, boot=boot,
44 installtype=self.installtype, series=self.series)45 installtype=self.installtype, series=self.series)
@@ -50,7 +51,7 @@
50 'ubuntu-installer', self.arch,51 'ubuntu-installer', self.arch,
51 'initrd.gz')52 'initrd.gz')
52 else:53 else:
53 raise UTAHProvisioningException(54 raise UTAHBMProvisioningException(
54 'Only alternate and server images currently supported '55 'Only alternate and server images currently supported '
55 'for cobbler provisioning')56 'for cobbler provisioning')
56 if self.arch == 'amd64':57 if self.arch == 'amd64':
@@ -63,16 +64,17 @@
6364
64 def _provision(self, checktimeout=config.checktimeout,65 def _provision(self, checktimeout=config.checktimeout,
65 installtimeout=config.installtimeout):66 installtimeout=config.installtimeout):
67 """
68 Install a machine.
69 """
70 # TODO: support for reusing existing machines
66 self.tmpdir = tempfile.mkdtemp(prefix='/tmp/' + self.name + '_')71 self.tmpdir = tempfile.mkdtemp(prefix='/tmp/' + self.name + '_')
67 os.chdir(self.tmpdir)72 os.chdir(self.tmpdir)
6873
69 # TODO: try to remove this step, mount the iso and import that74 # TODO: try to remove this step, mount the iso and import that
70 # with a specified kernel/preseed/initrd from the tmpdir75 # with a specified kernel/preseed/initrd from the tmpdir
71 self.logger.info('Extracting image to ' + self.tmpdir)76 self.logger.info('Extracting image to ' + self.tmpdir)
72 files = utah.iso.listfiles(self.image, logmethod=self.logger.debug,77 self.image.extractall()
73 returnlist=True)
74 for myfile in files:
75 utah.iso.extract(self.image, myfile, logmethod=self.logger.debug)
7678
77 self._preparekernel()79 self._preparekernel()
78 cinitrd = os.path.join(self.tmpdir, self.cinitrd)80 cinitrd = os.path.join(self.tmpdir, self.cinitrd)
@@ -149,7 +151,7 @@
149 .format(uuid=self.uuid))151 .format(uuid=self.uuid))
150 if self.run(uuid_check_command)[0] != 0:152 if self.run(uuid_check_command)[0] != 0:
151 self.provisioned = False153 self.provisioned = False
152 raise UTAHProvisioningException(154 raise UTAHBMProvisioningException(
153 'Installed UUID differs from CobblerMachine UUID; '155 'Installed UUID differs from CobblerMachine UUID; '
154 'installation probably failed. '156 'installation probably failed. '
155 'Try restarting cobbler or running cobbler sync')157 'Try restarting cobbler or running cobbler sync')
@@ -157,6 +159,11 @@
157 return True159 return True
158160
159 def destroy(self, *args, **kw):161 def destroy(self, *args, **kw):
162 """
163 Destroy the machine, and call super to release it in the inventory.
164 """
165 # TODO: Remove the machine from cobbler
166 # TODO: Consider actually wrecking the install
160 super(CobblerMachine, self).destroy(*args, **kw)167 super(CobblerMachine, self).destroy(*args, **kw)
161 self.__del__()168 self.__del__()
162 del self169 del self
@@ -185,6 +192,9 @@
185 self.active = False192 self.active = False
186193
187 def restart(self):194 def restart(self):
195 """
196 Stop the machine, wait powertimeout, then start it/
197 """
188 self.logger.info('Restarting system')198 self.logger.info('Restarting system')
189 self.stop()199 self.stop()
190 self.logger.debug('Waiting ' + str(config.powertimeout) + ' seconds')200 self.logger.debug('Waiting ' + str(config.powertimeout) + ' seconds')
@@ -192,6 +202,10 @@
192 self._start()202 self._start()
193203
194 def __del__(self):204 def __del__(self):
205 """
206 If the machine has an inventory, tell the inventory to release the
207 machine when the object is deleted.
208 """
195 if self.inventory is not None:209 if self.inventory is not None:
196 self.inventory.release(machine=self)210 self.inventory.release(machine=self)
197211
@@ -199,6 +213,7 @@
199 """213 """
200 Start the machine if needed, and check for SSH login.214 Start the machine if needed, and check for SSH login.
201 """215 """
216 # TODO: Consider moving this to SSHMixin
202 self.logger.debug('Checking if machine is active')217 self.logger.debug('Checking if machine is active')
203 self.provisioncheck()218 self.provisioncheck()
204 if not self.active:219 if not self.active:
205220
=== modified file 'utah/provisioning/baremetal/exceptions.py'
--- utah/provisioning/baremetal/exceptions.py 2012-07-19 09:36:34 +0000
+++ utah/provisioning/baremetal/exceptions.py 2012-08-24 20:25:19 +0000
@@ -1,4 +1,6 @@
1#!/usr/bin/python1"""
2Provide exceptions specific to bare metal provisioning.
3"""
24
3from utah.provisioning.exceptions import UTAHProvisioningException5from utah.provisioning.exceptions import UTAHProvisioningException
46
57
=== modified file 'utah/provisioning/exceptions.py'
--- utah/provisioning/exceptions.py 2012-07-24 11:08:49 +0000
+++ utah/provisioning/exceptions.py 2012-08-24 20:25:19 +0000
@@ -1,5 +1,6 @@
1#!/usr/bin/python1"""
22Provide exceptions specific to provisioning.
3"""
3from utah.exceptions import UTAHException4from utah.exceptions import UTAHException
45
56
67
=== removed file 'utah/provisioning/inventory/bootspeed.py'
--- utah/provisioning/inventory/bootspeed.py 2012-08-20 10:20:44 +0000
+++ utah/provisioning/inventory/bootspeed.py 1970-01-01 00:00:00 +0000
@@ -1,116 +0,0 @@
1#!/usr/bin/python
2
3import os
4from utah.provisioning.inventory.exceptions import \
5 UTAHProvisioningInventoryException
6from utah.provisioning.inventory.sqlite import SQLiteInventory
7from utah.provisioning.baremetal.cobbler import CobblerMachine
8
9
10class BootspeedInventory(SQLiteInventory):
11 """
12 Keep an inventory of manually entered machines for bootspeed testing.
13 All columns other than machineid, name, and state are assumed to be
14 arguments for cobbler system creation.
15 """
16 def __init__(self, db='~/.utah-bootspeed-inventory',
17 lockfile='~/.utah-bootspeed-lock', *args, **kw):
18 db = os.path.expanduser(db)
19 lockfile = os.path.expanduser(lockfile)
20 if not os.path.isfile(db):
21 raise UTAHProvisioningInventoryException(
22 'No machine database found at ' + db)
23 super(BootspeedInventory, self).__init__(*args, db=db,
24 lockfile=lockfile, **kw)
25 machines_count = (self.connection
26 .execute('SELECT COUNT(*) FROM machines')
27 .fetchall()[0][0])
28 if machines_count == 0:
29 raise UTAHProvisioningInventoryException('No machines in database')
30 self.machines = []
31
32 def request(self, name=None, *args, **kw):
33 query = 'SELECT * FROM machines'
34 queryvars = []
35 if name is not None:
36 query += ' WHERE name=?'
37 queryvars.append(name)
38 result = self.connection.execute(query, queryvars).fetchall()
39 if result is None:
40 raise UTAHProvisioningInventoryException(
41 'No machines meet criteria')
42 else:
43 for machineinfo in result:
44 cargs = dict(machineinfo)
45 machineid = cargs.pop('machineid')
46 name = cargs.pop('name')
47 state = cargs.pop('state')
48 if state == 'available':
49 update = self.connection.execute(
50 "UPDATE machines SET state='provisioned' "
51 "WHERE machineid=? AND state='available'",
52 [machineid]).rowcount
53 if update == 1:
54 machine = CobblerMachine(*args, cargs=cargs,
55 inventory=self, name=name, **kw)
56 self.machines.append(machine)
57 return machine
58 elif update == 0:
59 raise UTAHProvisioningInventoryException(
60 'Machine was requested by another process '
61 'before we could request it')
62 elif update > 1:
63 raise UTAHProvisioningInventoryException(
64 'Multiple machines exist '
65 'matching those criteria; '
66 'database ' + self.db + ' may be corrupt')
67 else:
68 raise UTAHProvisioningInventoryException(
69 'Negative rowcount returned '
70 'when attempting to request machine')
71 raise UTAHProvisioningInventoryException(
72 'All machines meeting criteria are currently unavailable')
73
74 def release(self, machine=None, name=None):
75 if machine is not None:
76 name = machine.name
77 if name is None:
78 raise UTAHProvisioningInventoryException(
79 'name required to release a machine')
80 query = "UPDATE machines SET state='available' WHERE name=?"
81 queryvars = [name]
82 update = self.connection.execute(query, queryvars).rowcount
83 if update == 1:
84 if machine is not None:
85 if machine in self.machines:
86 self.machines.remove(machine)
87 elif update == 0:
88 raise UTAHProvisioningInventoryException(
89 'SERIOUS ERROR: Another process released this machine '
90 'before we could, which means two processes provisioned '
91 'the same machine simultaneously')
92 elif update > 1:
93 raise UTAHProvisioningInventoryException(
94 'Multiple machines exist matching those criteria; '
95 'database ' + self.db + ' may be corrupt')
96 else:
97 raise UTAHProvisioningInventoryException(
98 'Negative rowcount returned '
99 'when attempting to request machine')
100
101# Here is how I currently create the database:
102# CREATE TABLE machines (machineid INTEGER PRIMARY KEY,
103# name TEXT NOT NULL UNIQUE,
104# state TEXT default 'available',
105# [mac-address] TEXT NOT NULL UNIQUE,
106# [power-address] TEXT DEFAULT '10.97.0.13',
107# [power-id] TEXT,
108# [power-user] TEXT DEFAULT 'ubuntu',
109# [power-pass] TEXT DEFAULT 'ubuntu',
110# [power-type] DEFAULT 'sentryswitch_cdu');
111
112# Here is how I currently populate the database:
113# INSERT INTO machines (name, [mac-address], [power-id])
114# VALUES ('acer-veriton-01-Pete', 'd0:27:88:9f:73:ce', 'Veriton_1');
115# INSERT INTO machines (name, [mac-address], [power-id])
116# VALUES ('acer-veriton-02-Pete', 'd0:27:88:9b:84:5b', 'Veriton_2');
1170
=== modified file 'utah/provisioning/inventory/exceptions.py'
--- utah/provisioning/inventory/exceptions.py 2012-07-19 09:36:34 +0000
+++ utah/provisioning/inventory/exceptions.py 2012-08-24 20:25:19 +0000
@@ -1,4 +1,6 @@
1#!/usr/bin/python1"""
2Provide excpetions specific to inventory issues.
3"""
24
3from utah.provisioning.exceptions import UTAHProvisioningException5from utah.provisioning.exceptions import UTAHProvisioningException
46
57
=== modified file 'utah/provisioning/inventory/inventory.py'
--- utah/provisioning/inventory/inventory.py 2012-07-24 14:07:41 +0000
+++ utah/provisioning/inventory/inventory.py 2012-08-24 20:25:19 +0000
@@ -63,7 +63,7 @@
6363
64 def write(self):64 def write(self):
65 """65 """
66 Force a commit all current inventory data in memory to internal or66 Force a commit of all current inventory data in memory to internal or
67 external storage.67 external storage.
68 """68 """
69 return False69 return False
7070
=== modified file 'utah/provisioning/inventory/sqlite.py'
--- utah/provisioning/inventory/sqlite.py 2012-08-20 10:20:44 +0000
+++ utah/provisioning/inventory/sqlite.py 2012-08-24 20:25:19 +0000
@@ -177,7 +177,7 @@
177 # [power-id] TEXT,177 # [power-id] TEXT,
178 # [power-user] TEXT DEFAULT 'ubuntu',178 # [power-user] TEXT DEFAULT 'ubuntu',
179 # [power-pass] TEXT DEFAULT 'ubuntu',179 # [power-pass] TEXT DEFAULT 'ubuntu',
180 # [power-type] DEFAULT 'sentryswitch_cdu');180 # [power-type] TEXT DEFAULT 'sentryswitch_cdu');
181181
182 # Here is how I currently populate the database:182 # Here is how I currently populate the database:
183 # INSERT INTO machines (name, [mac-address], [power-id])183 # INSERT INTO machines (name, [mac-address], [power-id])
184184
=== modified file 'utah/provisioning/provisioning.py'
--- utah/provisioning/provisioning.py 2012-08-20 19:56:21 +0000
+++ utah/provisioning/provisioning.py 2012-08-24 20:25:19 +0000
@@ -18,10 +18,11 @@
18import re18import re
19import apt.cache19import apt.cache
2020
21from utah.iso import ISO
21from utah.provisioning.exceptions import UTAHProvisioningException22from utah.provisioning.exceptions import UTAHProvisioningException
22from utah.retry import retry23from utah.retry import retry
23import utah.timeout24import utah.timeout
24from utah import config, iso25from utah import config
2526
2627
27class Machine(object):28class Machine(object):
@@ -94,7 +95,14 @@
94 self.active = False95 self.active = False
95 self._loggersetup()96 self._loggersetup()
9697
97 for item in ['preseed', 'xml', 'kernel', 'initrd', 'image']:98 if image is None:
99 self.image = None
100 else:
101 self.image = ISO(image,
102 dlpercentincrement=self.dlpercentincrement,
103 logger=self.logger)
104
105 for item in ['preseed', 'xml', 'kernel', 'initrd']:
98 # Ensure every file/url type argument is available locally106 # Ensure every file/url type argument is available locally
99 arg = locals()[item]107 arg = locals()[item]
100 if arg is None:108 if arg is None:
@@ -634,87 +642,12 @@
634 rewriting them to our liking, and repacking them.642 rewriting them to our liking, and repacking them.
635 """643 """
636 # TODO: setup variable(s) to suppress adding and rewriting644 # TODO: setup variable(s) to suppress adding and rewriting
637
638 def _getinstalltypefromimage(self, image=None):
639 """
640 Inspect the image's files to get the image type.
641 If .disk/mini-info exists, it's mini.
642 If the casper directory exists, it's desktop.
643 If ubuntu-server.seed exists in the preseeds directory, it's server.
644 """
645 # TODO: move this into the iso class after it exists
646 if image is None:
647 image = self.image
648 self.logger.info('Getting image type of ' + image)
649 files = iso.listfiles(image,
650 logmethod=self.logger.debug, returnlist=True)
651 installtype = 'alternate'
652 if '.disk/mini-info' in files:
653 installtype = 'mini'
654 elif './casper' in files:
655 installtype = 'desktop'
656 elif './preseed/ubuntu-server.seed' in files:
657 installtype = 'server'
658 self.logger.info('Image type is: ' + installtype)
659 return installtype
660
661 def _getarchfromimage(self, image=None, installtype=None):
662 """
663 Unpack the image's info file to get the arch.
664 Mini images use .disk/mini-info, everything else uses .disk/info.
665 """
666 # TODO: document the format of the info file
667 # TODO: move this into the iso class after it exists
668 if image is None:
669 image = self.image
670 if installtype is None:
671 installtype = self.installtype
672 self.logger.info('Getting arch of ' + image)
673 if installtype == 'mini':
674 self.logger.debug('Checking mini image')
675 infofile = '.disk/mini-info'
676 else:
677 self.logger.debug('Checking normal image')
678 infofile = './.disk/info'
679 stdout = (iso.dump(image, infofile, logmethod=logging.debug)
680 .communicate()[0])
681 arch = stdout.split()[-2]
682 self.logger.info('Arch is: ' + arch)
683 return arch
684
685 def _getseriesfromimage(self, image=None, installtype=None):
686 """
687 Unpack the image's info file to get the series.
688 Mini images use .disk/mini-info, everything else uses .disk/info.
689 """
690 # TODO: document the format of the info file
691 # TODO: move this into the iso class after it exists
692 if image is None:
693 image = self.image
694 if installtype is None:
695 installtype = self.installtype
696 self.logger.info('Getting series of ' + image)
697 if installtype == 'mini':
698 self.logger.debug('Checking mini image')
699 infofile = '.disk/mini-info'
700 else:
701 self.logger.debug('Checking normal image')
702 infofile = './.disk/info'
703 stdout = (iso.dump(image, infofile, logmethod=logging.debug)
704 .communicate()[0])
705 for word in stdout.split():
706 if word.startswith('"'):
707 series = word.strip('"').lower()
708 break
709 self.logger.info('Series is ' + series)
710 return series
711
712 def _preparekernel(self, kernel=None, tmpdir=None):645 def _preparekernel(self, kernel=None, tmpdir=None):
713 """646 """
714 If we haven't been given a kernel file, unpack one from the image.647 If we haven't been given a kernel file, unpack one from the image.
715 """648 """
716 # TODO: document this better649 # TODO: document this better
717 # TODO: consider moving this into the iso class when it exists650 # TODO: consider moving this into the ISO class when it exists
718 self.logger.info('Preparing kernel')651 self.logger.info('Preparing kernel')
719 if kernel is None:652 if kernel is None:
720 kernel = self.kernel653 kernel = self.kernel
@@ -730,8 +663,7 @@
730 self.logger.debug('Desktop image detected')663 self.logger.debug('Desktop image detected')
731 kernelpath = './casper/vmlinuz'664 kernelpath = './casper/vmlinuz'
732 kernel = os.path.join(tmpdir, 'kernel')665 kernel = os.path.join(tmpdir, 'kernel')
733 iso.extract(self.image, kernelpath,666 self.image.extract(kernelpath, outfile=kernel)
734 outfile=kernel, logmethod=logging.debug)
735 else:667 else:
736 self.logger.info('Using local kernel: ' + kernel)668 self.logger.info('Using local kernel: ' + kernel)
737 return kernel669 return kernel
@@ -741,7 +673,7 @@
741 If we haven't been given an initrd file, unpack one from the image.673 If we haven't been given an initrd file, unpack one from the image.
742 """674 """
743 # TODO: document this better675 # TODO: document this better
744 # TODO: consider moving this into the iso class when it exists676 # TODO: consider moving this into the ISO class when it exists
745 self.logger.info('Preparing initrd')677 self.logger.info('Preparing initrd')
746 if initrd is None:678 if initrd is None:
747 initrd = self.initrd679 initrd = self.initrd
@@ -757,8 +689,7 @@
757 self.logger.debug('Desktop image detected')689 self.logger.debug('Desktop image detected')
758 initrdpath = './casper/initrd.lz'690 initrdpath = './casper/initrd.lz'
759 initrd = os.path.join(tmpdir, os.path.basename(initrdpath))691 initrd = os.path.join(tmpdir, os.path.basename(initrdpath))
760 iso.extract(self.image, initrdpath,692 self.image.extract(initrdpath, outfile=initrd)
761 outfile=initrd, logmethod=logging.debug)
762 else:693 else:
763 self.logger.info('Using local initrd: ' + initrd)694 self.logger.info('Using local initrd: ' + initrd)
764 return initrd695 return initrd
@@ -874,7 +805,7 @@
874 .format(pkgname))805 .format(pkgname))
875 line = line.replace('string ',806 line = line.replace('string ',
876 'string {} '.format(pkgname))807 'string {} '.format(pkgname))
877 line = line.rstrip()808 line = line.rstrip() + "\n"
878 if 'netcfg/get_hostname' in line:809 if 'netcfg/get_hostname' in line:
879 self.logger.info('Rewriting hostname to ' + self.name)810 self.logger.info('Rewriting hostname to ' + self.name)
880 line = 'd-i netcfg/get_hostname string ' + self.name + "\n"811 line = 'd-i netcfg/get_hostname string ' + self.name + "\n"
@@ -1018,13 +949,15 @@
1018949
1019 def _custominit(self, arch=None, boot=None, installtype=None, series=None):950 def _custominit(self, arch=None, boot=None, installtype=None, series=None):
1020 """951 """
1021 TODO: Make this a proper __init__ method952 Setup installtype, arch, series and commandline.
1022 by adding super call to Machine.953 Everything but commandline setup may be replaced by ISO class later.
1023 """954 """
955 #TODO: Make this a proper __init__ method
956 #by adding super call to Machine.
1024 if installtype is None:957 if installtype is None:
1025 self.installtype = self._getinstalltypefromimage()958 self.installtype = self.image.installtype
1026 if arch is None:959 if arch is None:
1027 self.arch = self._getarchfromimage()960 self.arch = self.image.arch
1028 if series is None:961 if series is None:
1029 self.series = self._getseriesfromimage()962 self.series = self.image.series
1030 self._cmdlinesetup(boot=boot)963 self._cmdlinesetup(boot=boot)
1031964
=== modified file 'utah/provisioning/vm/exceptions.py'
--- utah/provisioning/vm/exceptions.py 2012-07-19 09:36:34 +0000
+++ utah/provisioning/vm/exceptions.py 2012-08-24 20:25:19 +0000
@@ -1,4 +1,6 @@
1#!/usr/bin/python1"""
2Provide excpetions specific to virtual machines.
3"""
24
3from utah.provisioning.exceptions import UTAHProvisioningException5from utah.provisioning.exceptions import UTAHProvisioningException
46
57
=== removed file 'utah/provisioning/vm/isotest.py'
--- utah/provisioning/vm/isotest.py 2012-07-12 11:06:21 +0000
+++ utah/provisioning/vm/isotest.py 1970-01-01 00:00:00 +0000
@@ -1,15 +0,0 @@
1#!/usr/bin/python
2
3import os
4
5import utah.provisioning
6
7isodir = '/var/cache/uath/iso'
8
9for iso in os.listdir(isodir):
10 isofile = os.path.join(isodir, iso)
11 m = utah.provisioning.vm.CustomVM(image=isofile)
12 print('File: ' + isofile)
13 print('Type: ' + m.installtype)
14 print('Arch: ' + m.arch)
15 print('Series: ' + m.series)
160
=== modified file 'utah/provisioning/vm/libvirtvm.py'
--- utah/provisioning/vm/libvirtvm.py 2012-08-20 10:20:44 +0000
+++ utah/provisioning/vm/libvirtvm.py 2012-08-24 20:25:19 +0000
@@ -1,4 +1,6 @@
1#!/usr/bin/python1"""
2Provide classes to provision libvirt-based virtual machines.
3"""
24
3import os5import os
4import random6import random
@@ -16,7 +18,6 @@
16 SSHMixin,18 SSHMixin,
17 CustomInstallMixin,19 CustomInstallMixin,
18 )20 )
19from utah.provisioning.exceptions import UTAHProvisioningException
20from utah.provisioning.vm.vm import VM21from utah.provisioning.vm.vm import VM
21from utah.provisioning.vm.exceptions import UTAHVMProvisioningException22from utah.provisioning.vm.exceptions import UTAHVMProvisioningException
22from utah import config23from utah import config
@@ -40,7 +41,7 @@
4041
41 def _load(self):42 def _load(self):
42 """43 """
43 Load an existing VM44 Load an existing VM.
44 """45 """
45 self.logger.info('Loading VM')46 self.logger.info('Loading VM')
46 self.vm = self.lv.lookupByName(self.name)47 self.vm = self.lv.lookupByName(self.name)
@@ -121,6 +122,9 @@
121 self.active = False122 self.active = False
122123
123 def libvirterrorhandler(self, context, err):124 def libvirterrorhandler(self, context, err):
125 """
126 Log libvirt errors instead of sending them directly to the console.
127 """
124 errorcode = err.get_error_code()128 errorcode = err.get_error_code()
125 if errorcode in [9, 42]:129 if errorcode in [9, 42]:
126 # We see these as part of normal operations,130 # We see these as part of normal operations,
@@ -236,8 +240,7 @@
236 self.logger.info("\tThe kvm and processor-specific "240 self.logger.info("\tThe kvm and processor-specific "
237 "kvm kernel modules are installed and loaded")241 "kvm kernel modules are installed and loaded")
238 self.logger.error('Software virtual machine support '242 self.logger.error('Software virtual machine support '
239 'will be implemented '243 'is implemented in the CustomVM class')
240 'in a future version of UTAH')
241 raise UTAHVMProvisioningException(244 raise UTAHVMProvisioningException(
242 'No hardware virtual machine support available')245 'No hardware virtual machine support available')
243 if ('Could not find' in line246 if ('Could not find' in line
@@ -259,7 +262,7 @@
259 "'s/^vm_extra_packages=\""262 "'s/^vm_extra_packages=\""
260 "/vm_extra_packages=\"python-yaml /' "263 "/vm_extra_packages=\"python-yaml /' "
261 "-i ~/.uqt_vm_tools.conf")264 "-i ~/.uqt_vm_tools.conf")
262 raise UTAHProvisioningException(265 raise UTAHVMProvisioningException(
263 'No vm-tools config file available; '266 'No vm-tools config file available; '
264 'more info available in '267 'more info available in '
265 + self.filehandler.baseFilename)268 + self.filehandler.baseFilename)
@@ -344,6 +347,9 @@
344347
345348
346class CustomVM(CustomInstallMixin, SSHMixin, LibvirtVM):349class CustomVM(CustomInstallMixin, SSHMixin, LibvirtVM):
350 """
351 Install a VM from an image using libvirt direct kernel booting.
352 """
347 def __init__(self, arch=None, boot=None, diskbus=None, disksizes=None,353 def __init__(self, arch=None, boot=None, diskbus=None, disksizes=None,
348 emulator=None, image=None, initrd=None, installtype=None,354 emulator=None, image=None, initrd=None, installtype=None,
349 kernel=None, machineid=None, macs=[], name=None,355 kernel=None, machineid=None, macs=[], name=None,
@@ -417,6 +423,9 @@
417 self.logger.debug('CustomVM init finished')423 self.logger.debug('CustomVM init finished')
418424
419 def _createdisks(self, disksizes=None):425 def _createdisks(self, disksizes=None):
426 """
427 Create disk files if needed and build a list of them.
428 """
420 self.logger.info('Creating disks')429 self.logger.info('Creating disks')
421 if disksizes is None:430 if disksizes is None:
422 disksizes = self.disksizes431 disksizes = self.disksizes
@@ -440,6 +449,9 @@
440 self.logger.debug('Adding disk to list')449 self.logger.debug('Adding disk to list')
441450
442 def _supportsdomaintype(self, domaintype):451 def _supportsdomaintype(self, domaintype):
452 """
453 Check emulator support in libvirt capabilities.
454 """
443 capabilities = ElementTree.fromstring(self.lv.getCapabilities())455 capabilities = ElementTree.fromstring(self.lv.getCapabilities())
444 for guest in capabilities.iterfind('guest'):456 for guest in capabilities.iterfind('guest'):
445 for arch in guest.iterfind('arch'):457 for arch in guest.iterfind('arch'):
@@ -450,6 +462,9 @@
450462
451 def _installxml(self, cmdline=None, initrd=None,463 def _installxml(self, cmdline=None, initrd=None,
452 kernel=None, tmpdir=None, xml=None):464 kernel=None, tmpdir=None, xml=None):
465 """
466 Return the XML tree to be passed to libvirt for VM installation.
467 """
453 self.logger.info('Creating installation XML')468 self.logger.info('Creating installation XML')
454 if cmdline is None:469 if cmdline is None:
455 cmdline = self.cmdline470 cmdline = self.cmdline
@@ -502,11 +517,11 @@
502 #TODO: Add a cdrom if none exists517 #TODO: Add a cdrom if none exists
503 if disk.get('device') == 'cdrom':518 if disk.get('device') == 'cdrom':
504 if disk.find('source') is not None:519 if disk.find('source') is not None:
505 disk.find('source').set('file', self.image)520 disk.find('source').set('file', self.image.image)
506 self.logger.debug('Rewrote existing CD-ROM')521 self.logger.debug('Rewrote existing CD-ROM')
507 else:522 else:
508 source = ElementTree.Element('source')523 source = ElementTree.Element('source')
509 source.set('file', self.image)524 source.set('file', self.image.image)
510 disk.append(source)525 disk.append(source)
511 self.logger.debug('Added source to existing CD-ROM')526 self.logger.debug('Added source to existing CD-ROM')
512 for disk in self.disks:527 for disk in self.disks:
@@ -559,6 +574,10 @@
559 return xmlt574 return xmlt
560575
561 def _installvm(self, lv=None, tmpdir=None, xml=None):576 def _installvm(self, lv=None, tmpdir=None, xml=None):
577 """
578 Install a VM, then undefine it in libvirt.
579 The final installation will recreate the VM using the existing disks.
580 """
562 self.logger.info('Creating VM')581 self.logger.info('Creating VM')
563 if lv is None:582 if lv is None:
564 lv = self.lv583 lv = self.lv
@@ -581,6 +600,10 @@
581 self.logger.info('Installation complete')600 self.logger.info('Installation complete')
582601
583 def _finalxml(self, tmpdir=None, xml=None):602 def _finalxml(self, tmpdir=None, xml=None):
603 """
604 Create the XML to be used for the post-installation VM.
605 This may be a transformation of the installation XML.
606 """
584 self.logger.info('Creating final VM XML')607 self.logger.info('Creating final VM XML')
585 if xml is None:608 if xml is None:
586 xml = ElementTree.ElementTree(file=self.xml)609 xml = ElementTree.ElementTree(file=self.xml)
@@ -601,6 +624,10 @@
601 return xml624 return xml
602625
603 def _create(self):626 def _create(self):
627 """
628 Create the VM, install the system, and prepare it to boot.
629 This primarily calls functions from CustomInstallMixin and CustomVM.
630 """
604 self.logger.info('Creating custom virtual machine')631 self.logger.info('Creating custom virtual machine')
605632
606 tmpdir = tempfile.mkdtemp(prefix='/tmp/' + self.name + '_')633 tmpdir = tempfile.mkdtemp(prefix='/tmp/' + self.name + '_')
@@ -653,6 +680,9 @@
653 self.active = True680 self.active = True
654681
655 def destroy(self, *args, **kw):682 def destroy(self, *args, **kw):
683 """
684 Remove the machine from libvirt and remove all the disk files.
685 """
656 super(CustomVM, self).destroy(*args, **kw)686 super(CustomVM, self).destroy(*args, **kw)
657 self.stop(force=True)687 self.stop(force=True)
658 self.vm.undefine()688 self.vm.undefine()
659689
=== modified file 'utah/provisioning/vm/vm.py'
--- utah/provisioning/vm/vm.py 2012-08-20 10:20:44 +0000
+++ utah/provisioning/vm/vm.py 2012-08-24 20:25:19 +0000
@@ -1,13 +1,14 @@
1#!/usr/bin/python1"""
2Consolidate functions specific to virtual machine provisioning,
3but not specific to any type of virtual machine.
4"""
25
3from utah.provisioning.provisioning import Machine6from utah.provisioning.provisioning import Machine
47
58
6class VM(Machine):9class VM(Machine):
7 """10 """
8 Provide a generic class to provision an arbitrary VM.11 Provide a class to consolidate methods specific to virtual machines.
9
10 Raise exceptions for most methods.
11 """12 """
12 def __init__(self, *args, **kw):13 def __init__(self, *args, **kw):
13 super(VM, self).__init__(*args, **kw)14 super(VM, self).__init__(*args, **kw)
1415
=== modified file 'utah/run.py'
--- utah/run.py 2012-08-16 13:12:51 +0000
+++ utah/run.py 2012-08-24 20:25:19 +0000
@@ -11,6 +11,9 @@
1111
1212
13def run_tests(args, machine):13def run_tests(args, machine):
14 """
15 Run some tests and retrieve results.
16 """
14 # Since we're running from a script, output INFO17 # Since we're running from a script, output INFO
15 for logger in [machine.logger] + machine.logger.handlers:18 for logger in [machine.logger] + machine.logger.handlers:
16 if logger.level > logging.INFO:19 if logger.level > logging.INFO:
1720
=== modified file 'utah/timeout.py'
--- utah/timeout.py 2012-08-13 20:37:09 +0000
+++ utah/timeout.py 2012-08-24 20:25:19 +0000
@@ -22,8 +22,8 @@
22 """22 """
23 Run command with all other arguments for up to timeout seconds, after with23 Run command with all other arguments for up to timeout seconds, after with
24 a UTAHTimeout exception is returned.24 a UTAHTimeout exception is returned.
25 TODO: Better support for nested timeouts.
26 """25 """
26 #TODO: Better support for nested timeouts.
27 if command is None:27 if command is None:
28 return28 return
2929
@@ -101,6 +101,9 @@
101101
102102
103def get_process_children(pid):103def get_process_children(pid):
104 """
105 Find process children so they can be killed when the timeout expires.
106 """
104 p = subprocess.Popen('ps --no-headers -o pid --ppid %d' % pid, shell=True,107 p = subprocess.Popen('ps --no-headers -o pid --ppid %d' % pid, shell=True,
105 stdout=subprocess.PIPE, stderr=subprocess.PIPE)108 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
106 stdout, stderr = p.communicate()109 stdout, stderr = p.communicate()

Subscribers

People subscribed via source and target branches