Merge lp:~justin-fathomdb/nova/virtualbox-support into lp:~hudson-openstack/nova/trunk

Proposed by justinsb
Status: Work in progress
Proposed branch: lp:~justin-fathomdb/nova/virtualbox-support
Merge into: lp:~hudson-openstack/nova/trunk
Prerequisite: lp:~justin-fathomdb/nova/raw-disk-images
Diff against target: 1705 lines (+841/-171)
28 files modified
bin/nova-compute (+5/-6)
dev-scripts/api.sh (+9/-0)
dev-scripts/compute.sh (+19/-0)
dev-scripts/create-image.sh (+17/-0)
dev-scripts/down.sh (+8/-0)
dev-scripts/network.sh (+7/-0)
dev-scripts/objectstore.sh (+15/-0)
dev-scripts/redis.sh (+3/-0)
dev-scripts/restart.sh (+8/-0)
dev-scripts/up.sh (+10/-0)
dev-scripts/upload-image.sh (+13/-0)
dev-scripts/volume.sh (+11/-0)
nova/compute/disk.py (+27/-36)
nova/compute/monitor.py (+1/-1)
nova/compute/service.py (+19/-31)
nova/network/service.py (+1/-1)
nova/process.py (+31/-34)
nova/rpc.py (+113/-13)
nova/server.py (+1/-1)
nova/service.py (+93/-36)
nova/threads.py (+40/-0)
nova/twistd.py (+4/-2)
nova/utils.py (+16/-2)
nova/virt/connection.py (+10/-2)
nova/virt/images.py (+4/-5)
nova/virt/libvirt_conn.py (+23/-0)
nova/virt/virtualbox.py (+332/-0)
nova/volume/service.py (+1/-1)
To merge this branch: bzr merge lp:~justin-fathomdb/nova/virtualbox-support
Reviewer Review Type Date Requested Status
Nova Core security contacts Pending
Review via email: mp+33067@code.launchpad.net

Description of the change

Adds initial experimental support for VirtualBox. Currently doesn't work, because VirtualBox doesn't expose an async API. Will work once the Twisted removal is complete. Won't break anything unless users launch with the (undocumented) connection type 'virtualbox'.

To post a comment you must log in.
166. By justinsb

Merged with trunk & lp:~justin-fathomdb/nova/better-error-messages-on-process-fail

167. By justinsb

Merged with updated kiss-backend (twisted for some services)

168. By justinsb

Merged with latest kiss-backend

169. By justinsb

Merged with latest kiss-backend

170. By justinsb

Merged with latest kiss-backend

171. By justinsb

Merged with latest lp:~justin-fathomdb/nova/better-error-messages-on-process-fail

172. By justinsb

Merged latest kiss-backend

173. By justinsb

Merged latest improved error message

174. By justinsb

Merged latest kiss-backend

175. By justinsb

Adding dev-scripts to make development easier

176. By justinsb

Added network to up/down helper scripts

177. By justinsb

Merged with latest ~justin-fathomdb/nova/kiss-backend/

178. By justinsb

Built disk and launched VM

179. By justinsb

Don't partition the disk if using raw images

180. By justinsb

Merged with latest lp:~justin-fathomdb/nova/raw-disk-images (though I'm fighting a mini merge-battle with myself because the code actually depends on the twisted removal)

181. By justinsb

Actually, it looks like the twisted removal & virtualbox support may be inextricably linked...

Revision history for this message
Soren Hansen (soren) wrote :

Twisted has no particular problem talking to a synchronous API. It can simply defer the call to a thread and get a response back as a callback once the call returns. What /is/ difficult is calling Twisted from a strictly synchronous application, but I don't see that as an issue here.

With that in mind, how is it that Twisted removal and VirtualBox support are related?

Revision history for this message
justinsb (justin-fathomdb) wrote :

The relational is the 'viral' nature of Twisted's async calls... when implementing an interface method that is currently synchronous (get_info, as I recall), that method needs to be made asynchronous if the implementation is now async (in this case, all VirtualBox methods must spawn the VirtualBoxManage command.) But then all its callers need to be made async, and then their callers etc. When I asked about this on the mailing list, it seems that the only thing to do was essentially to make _every_ method async, in case it ever called in future an interface method which had an async implementation. That's a pretty big lock-in to Twisted, and it seems that the momentum was moving against Twisted.

If this patch continued to work with Twisted, I think it counted it would need a dozen additional changes propagating new deferreds up the call chains.

Whatever the decision on Twisted, I'll work with it, but I do think we need _a_ decision. If it's to keep Twisted then I'll have to rework this patch, but that's OK... My vote is to go with Threads/Eventlet, because then the whole code isn't covered in Twisted-specifics and we have more options in future (Python Threads, Jython, Eventlet, source code translators) vs just Twisted.

Unmerged revisions

181. By justinsb

Actually, it looks like the twisted removal & virtualbox support may be inextricably linked...

180. By justinsb

Merged with latest lp:~justin-fathomdb/nova/raw-disk-images (though I'm fighting a mini merge-battle with myself because the code actually depends on the twisted removal)

179. By justinsb

Don't partition the disk if using raw images

178. By justinsb

Built disk and launched VM

177. By justinsb

Merged with latest ~justin-fathomdb/nova/kiss-backend/

176. By justinsb

Added network to up/down helper scripts

175. By justinsb

Adding dev-scripts to make development easier

174. By justinsb

Merged latest kiss-backend

173. By justinsb

Merged latest improved error message

172. By justinsb

Merged latest kiss-backend

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/nova-compute'
2--- bin/nova-compute 2010-08-19 00:39:12 +0000
3+++ bin/nova-compute 2010-08-21 14:09:42 +0000
4@@ -24,9 +24,8 @@
5 from nova import twistd
6 from nova.compute import service
7
8-
9-if __name__ == '__main__':
10- twistd.serve(__file__)
11-
12-if __name__ == '__builtin__':
13- application = service.ComputeService.create() # pylint: disable-msg=C0103
14+twistd.serve(__file__, twisted=False)
15+
16+service.ComputeService.create()
17+
18+service.ComputeService.main_loop()
19
20=== added directory 'dev-scripts'
21=== added file 'dev-scripts/api.sh'
22--- dev-scripts/api.sh 1970-01-01 00:00:00 +0000
23+++ dev-scripts/api.sh 2010-08-21 14:09:42 +0000
24@@ -0,0 +1,9 @@
25+#!/bin/bash
26+
27+. ~/openstack/conf
28+cd ${NOVA_SRC_BASE}
29+
30+NOVA_API_ARGS="--ca_path=${CA_DIR} --keys_path=${KEYS_PATH} ${NETWORK_ARGS}" # --datastore_path=/var/lib/nova/keeper"
31+echo NOVA_API_ARGS=${NOVA_API_ARGS}
32+bin/nova-api ${NOVA_API_ARGS} --nodaemonize --verbose start
33+
34
35=== added file 'dev-scripts/compute.sh'
36--- dev-scripts/compute.sh 1970-01-01 00:00:00 +0000
37+++ dev-scripts/compute.sh 2010-08-21 14:09:42 +0000
38@@ -0,0 +1,19 @@
39+#!/bin/bash
40+
41+. ~/openstack/conf
42+cd ${NOVA_SRC_BASE}
43+
44+mkdir -p ${INSTANCES_PATH}
45+mkdir -p ${NETWORKS_PATH}
46+NOVA_COMPUTE_ARGS="--ca_path=${CA_DIR} --keys_path=${KEYS_PATH} ${NETWORK_ARGS} --images_path=${IMAGES_PATH} --use_s3=false --instances_path=${INSTANCES_PATH} --networks_path=${NETWORKS_PATH}" # --libvirt_xml_template=nova/compute/libvirt.xml.template --vpn_client_template=nova/cloudpipe/client.ovpn.template --credentials_template=nova/auth/novarc.template"
47+# --datastore_path=/var/lib/nova/keeper"
48+
49+if [ -n $CONNECTION_TYPE ]; then
50+ NOVA_COMPUTE_ARGS="${NOVA_COMPUTE_ARGS} --connection_type=${CONNECTION_TYPE}"
51+fi
52+
53+echo NOVA_COMPUTE_ARGS=${NOVA_COMPUTE_ARGS}
54+
55+bin/nova-compute ${NOVA_COMPUTE_ARGS} --nodaemon --verbose start
56+
57+
58
59=== added file 'dev-scripts/create-image.sh'
60--- dev-scripts/create-image.sh 1970-01-01 00:00:00 +0000
61+++ dev-scripts/create-image.sh 2010-08-21 14:09:42 +0000
62@@ -0,0 +1,17 @@
63+#!/bin/bash
64+
65+apt-get --yes install apt-proxy ubuntu-vm-builder
66+
67+mkdir -p ~/openstack/images
68+cd ~/openstack/images
69+# If you're on 32 bit CPU, or if you don't have HVM (?), use 32 bit...
70+# If you're not running a big-memory machine, you should probably be using 32 bit anyway...
71+ARCH=i386
72+sudo vmbuilder kvm ubuntu --addpkg openssh-server --install-mirror=http://127.0.0.1:9999/ubuntu --install-security-mirror=http://127.0.0.1:9999/ubuntu-security --debug --arch ${ARCH}
73+cd ubuntu-kvm
74+# Sneaky link to get around random naming
75+ln -s *.qcow2 root.qcow2
76+qemu-img info root.qcow2
77+qemu-img convert -O vmdk root.qcow2 root.vmdk
78+qemu-img info root.vmdk
79+
80
81=== added file 'dev-scripts/down.sh'
82--- dev-scripts/down.sh 1970-01-01 00:00:00 +0000
83+++ dev-scripts/down.sh 2010-08-21 14:09:42 +0000
84@@ -0,0 +1,8 @@
85+#!/bin/bash
86+
87+pkill -f nova-api
88+pkill -f nova-compute
89+pkill -f nova-volume
90+pkill -f nova-objectstore
91+pkill -f nova-network
92+pkill -f redis-server
93
94=== added file 'dev-scripts/network.sh'
95--- dev-scripts/network.sh 1970-01-01 00:00:00 +0000
96+++ dev-scripts/network.sh 2010-08-21 14:09:42 +0000
97@@ -0,0 +1,7 @@
98+#!/bin/bash
99+
100+. ~/openstack/conf
101+cd ${NOVA_SRC_BASE}
102+
103+bin/nova-network --nodaemon --verbose
104+
105
106=== added file 'dev-scripts/objectstore.sh'
107--- dev-scripts/objectstore.sh 1970-01-01 00:00:00 +0000
108+++ dev-scripts/objectstore.sh 2010-08-21 14:09:42 +0000
109@@ -0,0 +1,15 @@
110+#!/bin/bash
111+
112+. ~/openstack/conf
113+cd ${NOVA_SRC_BASE}
114+
115+mkdir -p ${BUCKETS_PATH}
116+mkdir -p ${IMAGES_PATH}
117+NOVA_OBJECTSTORE_ARGS="--ca_path=${CA_DIR} --keys_path=${KEYS_PATH} ${NETWORK_ARGS} --images_path=${IMAGES_PATH} --buckets_path=${BUCKETS_PATH}" # --datastore_path=/var/lib/nova/keeper"
118+# This hack is because otherwise the compute deamon also writes to twistd.log
119+mkdir -p objectstorelogs
120+cd objectstorelogs
121+# PYTHONPATH needs to be set to '..' to find the modules when running this way.. (overriding the 'conf' file at the top) -- comstud
122+PYTHONPATH=".."
123+../bin/nova-objectstore ${NOVA_OBJECTSTORE_ARGS} --nodaemon --verbose start
124+
125
126=== added file 'dev-scripts/redis.sh'
127--- dev-scripts/redis.sh 1970-01-01 00:00:00 +0000
128+++ dev-scripts/redis.sh 2010-08-21 14:09:42 +0000
129@@ -0,0 +1,3 @@
130+#!/bin/bash
131+
132+~/openstack/src/redis-latest/redis-server
133
134=== added file 'dev-scripts/restart.sh'
135--- dev-scripts/restart.sh 1970-01-01 00:00:00 +0000
136+++ dev-scripts/restart.sh 2010-08-21 14:09:42 +0000
137@@ -0,0 +1,8 @@
138+#!/bin/bash
139+
140+BASE=`dirname $0`
141+
142+${BASE}/down.sh
143+sleep 1
144+${BASE}/up.sh
145+
146
147=== added file 'dev-scripts/up.sh'
148--- dev-scripts/up.sh 1970-01-01 00:00:00 +0000
149+++ dev-scripts/up.sh 2010-08-21 14:09:42 +0000
150@@ -0,0 +1,10 @@
151+#!/bin/bash
152+
153+BASE=`dirname $0`
154+screen -X screen -t redis ${BASE}/redis.sh
155+screen -X screen -t api ${BASE}/api.sh
156+screen -X screen -t object ${BASE}/objectstore.sh
157+screen -X screen -t compute ${BASE}/compute.sh
158+screen -X screen -t volume ${BASE}/volume.sh
159+screen -X screen -t network ${BASE}/network.sh
160+
161
162=== added file 'dev-scripts/upload-image.sh'
163--- dev-scripts/upload-image.sh 1970-01-01 00:00:00 +0000
164+++ dev-scripts/upload-image.sh 2010-08-21 14:09:42 +0000
165@@ -0,0 +1,13 @@
166+#!/bin/bash
167+
168+cd ~/openstack/images
169+cd ubuntu-kvm
170+
171+mkdir -p image
172+. ${NOVA_SRC_BASE}/me/novarc
173+euca-bundle-image --image root.vmdk --destination ./image
174+euca-upload-bundle --bucket image --manifest ./image/root.vmdk.manifest.xml
175+EMI=`euca-register image/root.img.manifest.xml | awk '{print $2}'`
176+echo $EMI
177+
178+
179
180=== added file 'dev-scripts/volume.sh'
181--- dev-scripts/volume.sh 1970-01-01 00:00:00 +0000
182+++ dev-scripts/volume.sh 2010-08-21 14:09:42 +0000
183@@ -0,0 +1,11 @@
184+#!/bin/bash
185+
186+. ~/openstack/conf
187+cd ${NOVA_SRC_BASE}
188+
189+NOVA_VOLUME_ARGS="--storage_dev=${LOOPBACK_MOUNT}"
190+
191+#are you running from iSCSI? if not, comment out 2nd line and run first line:
192+#bin/nova-volume ${NOVA_VOLUME_ARGS} --nodaemon --verbose start
193+bin/nova-volume ${NOVA_VOLUME_ARGS} --nodaemon --iscsi_storage=true --verbose start
194+
195
196=== modified file 'nova/compute/disk.py'
197--- nova/compute/disk.py 2010-08-21 14:09:42 +0000
198+++ nova/compute/disk.py 2010-08-21 14:09:42 +0000
199@@ -25,12 +25,9 @@
200 import os
201 import tempfile
202
203-from twisted.internet import defer
204-
205 from nova import exception
206
207
208-@defer.inlineCallbacks
209 def partition(infile, outfile, local_bytes=0, local_type='ext2', execute=None):
210 """Takes a single partition represented by infile and writes a bootable
211 drive image into outfile.
212@@ -66,27 +63,26 @@
213 last_sector = local_last # e
214
215 # create an empty file
216- yield execute('dd if=/dev/zero of=%s count=1 seek=%d bs=%d'
217+ execute('dd if=/dev/zero of=%s count=1 seek=%d bs=%d'
218 % (outfile, last_sector, sector_size))
219
220 # make mbr partition
221- yield execute('parted --script %s mklabel msdos' % outfile)
222+ execute('parted --script %s mklabel msdos' % outfile)
223
224 # make primary partition
225- yield execute('parted --script %s mkpart primary %ds %ds'
226+ execute('parted --script %s mkpart primary %ds %ds'
227 % (outfile, primary_first, primary_last))
228
229 # make local partition
230 if local_bytes > 0:
231- yield execute('parted --script %s mkpartfs primary %s %ds %ds'
232+ execute('parted --script %s mkpartfs primary %s %ds %ds'
233 % (outfile, local_type, local_first, local_last))
234
235 # copy file into partition
236- yield execute('dd if=%s of=%s bs=%d seek=%d conv=notrunc,fsync'
237+ execute('dd if=%s of=%s bs=%d seek=%d conv=notrunc,fsync'
238 % (infile, outfile, sector_size, primary_first))
239
240
241-@defer.inlineCallbacks
242 def inject_data( image, key=None, net=None, dns=None,
243 remove_network_udev=False,
244 partition=None, execute=None):
245@@ -98,14 +94,14 @@
246 If partition is not specified it mounts the image as a single partition.
247
248 """
249- out, err = yield execute('sudo losetup --find --show %s' % image)
250+ out, err = execute('sudo losetup --find --show %s' % image)
251 if err:
252 raise exception.Error('Could not attach image to loopback: %s' % err)
253 device = out.strip()
254 try:
255 if not partition is None:
256 # create partition
257- out, err = yield execute('sudo kpartx -a %s' % device)
258+ out, err = execute('sudo kpartx -a %s' % device)
259 if err:
260 raise exception.Error('Failed to load partition: %s' % err)
261 mapped_device = '/dev/mapper/%sp%s' % (device.split('/')[-1],
262@@ -121,12 +117,12 @@
263 % mapped_device)
264
265 # Configure ext2fs so that it doesn't auto-check every N boots
266- out, err = yield execute('sudo tune2fs -c 0 -i 0 %s' % mapped_device)
267+ out, err = execute('sudo tune2fs -c 0 -i 0 %s' % mapped_device)
268
269 tmpdir = tempfile.mkdtemp()
270 try:
271 # mount loopback to dir
272- out, err = yield execute(
273+ out, err = execute(
274 'sudo mount %s %s' % (mapped_device, tmpdir))
275 if err:
276 raise exception.Error('Failed to mount filesystem: %s' % err)
277@@ -134,53 +130,48 @@
278 try:
279 if key:
280 # inject key file
281- yield _inject_key_into_fs(key, tmpdir, execute=execute)
282+ _inject_key_into_fs(key, tmpdir, execute=execute)
283 if net:
284- yield _inject_net_into_fs(net, tmpdir, execute=execute)
285+ _inject_net_into_fs(net, tmpdir, execute=execute)
286 if dns:
287- yield _inject_dns_into_fs(dns, tmpdir, execute=execute)
288+ _inject_dns_into_fs(dns, tmpdir, execute=execute)
289 if remove_network_udev:
290- yield _remove_network_udev(tmpdir, execute=execute)
291+ _remove_network_udev(tmpdir, execute=execute)
292
293 finally:
294 # unmount device
295- yield execute('sudo umount %s' % mapped_device)
296+ execute('sudo umount %s' % mapped_device)
297 finally:
298 # remove temporary directory
299- yield execute('rmdir %s' % tmpdir)
300+ execute('rmdir %s' % tmpdir)
301 if not partition is None:
302 # remove partitions
303- yield execute('sudo kpartx -d %s' % device)
304+ execute('sudo kpartx -d %s' % device)
305 finally:
306 # remove loopback
307- yield execute('sudo losetup --detach %s' % device)
308-
309-
310-@defer.inlineCallbacks
311+ execute('sudo losetup --detach %s' % device)
312+
313 def _inject_key_into_fs(key, fs, execute=None):
314 sshdir = os.path.join(fs, 'root', '.ssh')
315- yield execute('sudo mkdir -p %s' % sshdir) # existing dir doesn't matter
316- yield execute('sudo chown root %s' % sshdir)
317- yield execute('sudo chmod 700 %s' % sshdir)
318+ execute('sudo mkdir -p %s' % sshdir) # existing dir doesn't matter
319+ execute('sudo chown root %s' % sshdir)
320+ execute('sudo chmod 700 %s' % sshdir)
321 keyfile = os.path.join(sshdir, 'authorized_keys')
322- yield execute('sudo tee -a %s' % keyfile, '\n' + key.strip() + '\n')
323-
324-
325-@defer.inlineCallbacks
326+ execute('sudo tee -a %s' % keyfile, '\n' + key.strip() + '\n')
327+
328+
329 def _inject_net_into_fs(net, fs, execute=None):
330 netfile = os.path.join(fs, 'etc', 'network', 'interfaces')
331- yield execute('sudo tee %s' % netfile, net)
332+ execute('sudo tee %s' % netfile, net)
333
334-@defer.inlineCallbacks
335 def _inject_dns_into_fs(dns, fs, execute=None):
336 dnsfile = os.path.join(fs, 'etc', 'resolv.conf')
337- yield execute('sudo tee %s' % dnsfile, dns)
338+ execute('sudo tee %s' % dnsfile, dns)
339
340-@defer.inlineCallbacks
341 def _remove_network_udev(fs, execute=None):
342 # TODO(justinsb): This is correct for Ubuntu, but might not be right for
343 # other distros. There is a much bigger discussion to be had about what
344 # we inject and how we inject it.
345 rulesfile = os.path.join(fs, 'etc', 'udev', 'rules.d', '70-persistent-net.rules')
346- yield execute('rm -f %s' % rulesfile)
347+ execute('rm -f %s' % rulesfile)
348
349
350=== modified file 'nova/compute/monitor.py'
351--- nova/compute/monitor.py 2010-08-16 12:16:21 +0000
352+++ nova/compute/monitor.py 2010-08-21 14:09:42 +0000
353@@ -389,7 +389,7 @@
354 return '%d:%d' % (rx, tx)
355
356
357-class InstanceMonitor(object, service.Service):
358+class InstanceMonitor(object, service.ThreadedService):
359 """
360 Monitors the running instances of the current machine.
361 """
362
363=== modified file 'nova/compute/service.py'
364--- nova/compute/service.py 2010-08-16 12:16:21 +0000
365+++ nova/compute/service.py 2010-08-21 14:09:42 +0000
366@@ -30,12 +30,8 @@
367 import os
368 import sys
369
370-from twisted.internet import defer
371-from twisted.internet import task
372-
373 from nova import exception
374 from nova import flags
375-from nova import process
376 from nova import service
377 from nova import utils
378 from nova.compute import disk
379@@ -53,7 +49,7 @@
380 'where instances are stored on disk')
381
382
383-class ComputeService(service.Service):
384+class ComputeService(service.ThreadedService):
385 """
386 Manages the running instances.
387 """
388@@ -67,7 +63,7 @@
389
390 def noop(self):
391 """ simple test of an AMQP message call """
392- return defer.succeed('PONG')
393+ return 'PONG'
394
395 def get_instance(self, instance_id):
396 # inst = self.instdir.get(instance_id)
397@@ -79,7 +75,7 @@
398 @exception.wrap_exception
399 def adopt_instances(self):
400 """ if there are instances already running, adopt them """
401- return defer.succeed(0)
402+ return 0
403 instance_names = self._conn.list_instances()
404 for name in instance_names:
405 try:
406@@ -87,7 +83,7 @@
407 new_inst.update_state()
408 except:
409 pass
410- return defer.succeed(len(self._instances))
411+ return len(self._instances)
412
413 @exception.wrap_exception
414 def describe_instances(self):
415@@ -97,7 +93,6 @@
416 Instance.fromName(self._conn, inst['instance_id']))
417 return retval
418
419- @defer.inlineCallbacks
420 def report_state(self, nodename, daemon):
421 # TODO(termie): make this pattern be more elegant. -todd
422 try:
423@@ -111,7 +106,7 @@
424 if not getattr(self, "model_disconnected", False):
425 self.model_disconnected = True
426 logging.exception("model server went away")
427- yield
428+
429
430 @exception.wrap_exception
431 def run_instance(self, instance_id, **_kwargs):
432@@ -161,7 +156,6 @@
433 'trying to reboot unknown instance: %s' % instance_id)
434 return instance.reboot()
435
436- @defer.inlineCallbacks
437 @exception.wrap_exception
438 def get_console_output(self, instance_id):
439 """ send the console output for an instance """
440@@ -171,34 +165,31 @@
441 if not instance:
442 raise exception.Error(
443 'trying to get console log for unknown: %s' % instance_id)
444- rv = yield instance.console_output()
445+ rv = instance.console_output()
446 # TODO(termie): this stuff belongs in the API layer, no need to
447 # munge the data we send to ourselves
448 output = {"InstanceId" : instance_id,
449 "Timestamp" : "2",
450 "output" : base64.b64encode(rv)}
451- defer.returnValue(output)
452+ return output
453
454- @defer.inlineCallbacks
455 @exception.wrap_exception
456 def attach_volume(self, instance_id = None,
457 volume_id = None, mountpoint = None):
458 volume = volume_service.get_volume(volume_id)
459- yield self._init_aoe()
460- yield process.simple_execute(
461+ self._init_aoe()
462+ utils.execute(
463 "sudo virsh attach-disk %s /dev/etherd/%s %s" %
464 (instance_id,
465 volume['aoe_device'],
466 mountpoint.rpartition('/dev/')[2]))
467 volume.finish_attach()
468- defer.returnValue(True)
469+ return True
470
471- @defer.inlineCallbacks
472 def _init_aoe(self):
473- yield process.simple_execute("sudo aoe-discover")
474- yield process.simple_execute("sudo aoe-stat")
475+ utils.execute("sudo aoe-discover")
476+ utils.execute("sudo aoe-stat")
477
478- @defer.inlineCallbacks
479 @exception.wrap_exception
480 def detach_volume(self, instance_id, volume_id):
481 """ detach a volume from an instance """
482@@ -206,10 +197,10 @@
483 # name without the leading /dev/
484 volume = volume_service.get_volume(volume_id)
485 target = volume['mountpoint'].rpartition('/dev/')[2]
486- yield process.simple_execute(
487+ utils.execute(
488 "sudo virsh detach-disk %s %s " % (instance_id, target))
489 volume.finish_detach()
490- defer.returnValue(True)
491+ return True
492
493
494 class Group(object):
495@@ -317,7 +308,6 @@
496 self.set_state(self.state)
497 self.datamodel.save() # Extra, but harmless
498
499- @defer.inlineCallbacks
500 @exception.wrap_exception
501 def destroy(self):
502 if self.is_destroyed():
503@@ -326,10 +316,9 @@
504 ' instance: %s' % self.name)
505
506 self.set_state(power_state.NOSTATE, 'shutting_down')
507- yield self._conn.destroy(self)
508+ self._conn.destroy(self)
509 self.datamodel.destroy()
510
511- @defer.inlineCallbacks
512 @exception.wrap_exception
513 def reboot(self):
514 if not self.is_running():
515@@ -339,18 +328,17 @@
516
517 logging.debug('rebooting instance %s' % self.name)
518 self.set_state(power_state.NOSTATE, 'rebooting')
519- yield self._conn.reboot(self)
520+ self._conn.reboot(self)
521 self.update_state()
522
523- @defer.inlineCallbacks
524 @exception.wrap_exception
525 def spawn(self):
526 self.set_state(power_state.NOSTATE, 'spawning')
527 logging.debug("Starting spawn in Instance")
528 try:
529- yield self._conn.spawn(self)
530+ self._conn.spawn(self)
531 except Exception, ex:
532- logging.debug(ex)
533+ logging.warn(ex)
534 self.set_state(power_state.SHUTDOWN)
535 self.update_state()
536
537@@ -364,4 +352,4 @@
538 console = f.read()
539 else:
540 console = 'FAKE CONSOLE OUTPUT'
541- return defer.succeed(console)
542+ return console
543
544=== modified file 'nova/network/service.py'
545--- nova/network/service.py 2010-08-18 15:44:24 +0000
546+++ nova/network/service.py 2010-08-21 14:09:42 +0000
547@@ -81,7 +81,7 @@
548 return "networkhost:%s" % project_id
549
550
551-class BaseNetworkService(service.Service):
552+class BaseNetworkService(service.TwistedService):
553 """Implements common network service functionality
554
555 This class must be subclassed.
556
557=== modified file 'nova/process.py'
558--- nova/process.py 2010-08-18 21:14:24 +0000
559+++ nova/process.py 2010-08-21 14:09:42 +0000
560@@ -29,28 +29,12 @@
561 from twisted.internet import reactor
562
563 from nova import flags
564+from nova.utils import ProcessExecutionError
565
566 FLAGS = flags.FLAGS
567 flags.DEFINE_integer('process_pool_size', 4,
568 'Number of processes to use in the process pool')
569
570-
571-# NOTE(termie): this is copied from twisted.internet.utils but since
572-# they don't export it I've copied and modified
573-class UnexpectedErrorOutput(IOError):
574- """
575- Standard error data was received where it was not expected. This is a
576- subclass of L{IOError} to preserve backward compatibility with the previous
577- error behavior of L{getProcessOutput}.
578-
579- @ivar processEnded: A L{Deferred} which will fire when the process which
580- produced the data on stderr has ended (exited and all file descriptors
581- closed).
582- """
583- def __init__(self, stdout=None, stderr=None):
584- IOError.__init__(self, "got stdout: %r\nstderr: %r" % (stdout, stderr))
585-
586-
587 # This is based on _BackRelay from twister.internal.utils, but modified to
588 # capture both stdout and stderr, without odd stderr handling, and also to
589 # handle stdin
590@@ -62,22 +46,23 @@
591 @ivar deferred: A L{Deferred} which will be called back with all of stdout
592 and all of stderr as well (as a tuple). C{terminate_on_stderr} is true
593 and any bytes are received over stderr, this will fire with an
594- L{_UnexpectedErrorOutput} instance and the attribute will be set to
595+ L{_ProcessExecutionError} instance and the attribute will be set to
596 C{None}.
597
598 @ivar onProcessEnded: If C{terminate_on_stderr} is false and bytes are
599 received over stderr, this attribute will refer to a L{Deferred} which
600 will be called back when the process ends. This C{Deferred} is also
601- associated with the L{_UnexpectedErrorOutput} which C{deferred} fires
602+ associated with the L{_ProcessExecutionError} which C{deferred} fires
603 with earlier in this case so that users can determine when the process
604 has actually ended, in addition to knowing when bytes have been received
605 via stderr.
606 """
607
608- def __init__(self, deferred, started_deferred=None,
609+ def __init__(self, deferred, cmd, started_deferred=None,
610 terminate_on_stderr=False, check_exit_code=True,
611 process_input=None):
612 self.deferred = deferred
613+ self.cmd = cmd
614 self.stdout = StringIO.StringIO()
615 self.stderr = StringIO.StringIO()
616 self.started_deferred = started_deferred
617@@ -85,14 +70,18 @@
618 self.check_exit_code = check_exit_code
619 self.process_input = process_input
620 self.on_process_ended = None
621-
622+
623+ def _build_execution_error(self, exit_code=None):
624+ return ProcessExecutionError( cmd=self.cmd,
625+ exit_code=exit_code,
626+ stdout=self.stdout.getvalue(),
627+ stderr=self.stderr.getvalue())
628+
629 def errReceived(self, text):
630 self.stderr.write(text)
631 if self.terminate_on_stderr and (self.deferred is not None):
632 self.on_process_ended = defer.Deferred()
633- self.deferred.errback(UnexpectedErrorOutput(
634- stdout=self.stdout.getvalue(),
635- stderr=self.stderr.getvalue()))
636+ self.deferred.errback(self._build_execution_error())
637 self.deferred = None
638 self.transport.loseConnection()
639
640@@ -102,15 +91,19 @@
641 def processEnded(self, reason):
642 if self.deferred is not None:
643 stdout, stderr = self.stdout.getvalue(), self.stderr.getvalue()
644- try:
645- if self.check_exit_code:
646- reason.trap(error.ProcessDone)
647- self.deferred.callback((stdout, stderr))
648- except:
649- # NOTE(justinsb): This logic is a little suspicious to me...
650- # If the callback throws an exception, then errback will be
651- # called also. However, this is what the unit tests test for...
652- self.deferred.errback(UnexpectedErrorOutput(stdout, stderr))
653+ exit_code = reason.value.exitCode
654+ if self.check_exit_code and exit_code <> 0:
655+ self.deferred.errback(self._build_execution_error(exit_code))
656+ else:
657+ try:
658+ if self.check_exit_code:
659+ reason.trap(error.ProcessDone)
660+ self.deferred.callback((stdout, stderr))
661+ except:
662+ # NOTE(justinsb): This logic is a little suspicious to me...
663+ # If the callback throws an exception, then errback will be
664+ # called also. However, this is what the unit tests test for...
665+ self.deferred.errback(self._build_execution_error(exit_code))
666 elif self.on_process_ended is not None:
667 self.on_process_ended.errback(reason)
668
669@@ -131,8 +124,12 @@
670 args = args and args or ()
671 env = env and env and {}
672 deferred = defer.Deferred()
673+ cmd = executable
674+ if args:
675+ cmd = cmd + " " + ' '.join(args)
676 process_handler = BackRelayWithInput(
677- deferred,
678+ deferred,
679+ cmd,
680 started_deferred=started_deferred,
681 check_exit_code=check_exit_code,
682 process_input=process_input,
683
684=== modified file 'nova/rpc.py'
685--- nova/rpc.py 2010-08-18 15:44:24 +0000
686+++ nova/rpc.py 2010-08-21 14:09:42 +0000
687@@ -1,8 +1,12 @@
688 # vim: tabstop=4 shiftwidth=4 softtabstop=4
689+from nova.threads import PollingThread
690+from threading import Thread
691+import threading
692
693 # Copyright 2010 United States Government as represented by the
694 # Administrator of the National Aeronautics and Space Administration.
695 # All Rights Reserved.
696+# Copyright 2010 FathomDB Inc. All rights reserved.
697 #
698 # Licensed under the Apache License, Version 2.0 (the "License"); you may
699 # not use this file except in compliance with the License. You may obtain
700@@ -71,6 +75,14 @@
701 del cls._instance
702 return cls.instance()
703
704+# Carrot is seriously FUBAR when it comes to threads
705+# (1990 called and wants its libraries back)
706+# It looks like it we simultaneously issue fetches from two threads, carrot crashes.
707+# Also, we can't open a queue connection on one thread and then call from another.
708+# We'll probably have to introduce a message queueing library that works
709+# (or just wrap carrot)
710+# Finding all the problems first, then going to decide...
711+queue_lock = threading.Lock()
712
713 class Consumer(messaging.Consumer):
714 """Consumer base class
715@@ -100,6 +112,7 @@
716 """Wraps the parent fetch with some logic for failed connections"""
717 # TODO(vish): the logic for failed connections and logging should be
718 # refactored into some sort of connection manager object
719+ LOG.debug("Doing fetch from queue")
720 try:
721 if self.failed_connection:
722 # NOTE(vish): conn is defined in the parent class, we can
723@@ -107,16 +120,17 @@
724 # pylint: disable-msg=W0201
725 self.conn = Connection.recreate()
726 self.backend = self.conn.create_backend()
727- super(Consumer, self).fetch(no_ack, auto_ack, enable_callbacks)
728+ with queue_lock:
729+ super(Consumer, self).fetch(no_ack, auto_ack, enable_callbacks)
730 if self.failed_connection:
731- logging.error("Reconnected to queue")
732+ LOG.error("Reconnected to queue")
733 self.failed_connection = False
734 # NOTE(vish): This is catching all errors because we really don't
735 # exceptions to be logged 10 times a second if some
736 # persistent failure occurs.
737 except Exception: # pylint: disable-msg=W0703
738 if not self.failed_connection:
739- logging.exception("Failed to fetch message from queue")
740+ LOG.exception("Failed to fetch message from queue")
741 self.failed_connection = True
742
743 def attach_to_twisted(self):
744@@ -124,7 +138,44 @@
745 loop = task.LoopingCall(self.fetch, enable_callbacks=True)
746 loop.start(interval=0.1)
747
748-
749+ def start_background_thread(self):
750+ consumer_thread = ConsumeQueueThread()
751+ consumer_thread.setName('Queue consumer thread')
752+ consumer_thread.task_interval = 0.1
753+ consumer_thread.consumer = self
754+ consumer_thread.setDaemon(True)
755+ consumer_thread.start()
756+ return consumer_thread
757+
758+class ConsumeQueueThread(PollingThread):
759+ """ Thread that waits for queue messages """
760+ # It would be very easy to convert this to be non-polling
761+ # (i.e. just call consumer.wait(), but we're cloning existing behaviour before improving it
762+ def _do_task(self):
763+ self._fetch_from_queue()
764+
765+ def _fetch_from_queue(self):
766+ self.consumer.fetch(enable_callbacks=True)
767+
768+class ProcessMessageThread(Thread):
769+ """ Thread that processes a queue message """
770+ def run(self):
771+ LOG.warning("Invoking %s %s", self.handler_function, self.handler_args)
772+
773+ try:
774+ rval = self.handler_function(**self.handler_args)
775+ except Exception as e:
776+ LOG.exception("Error invoking message handler")
777+ if self.msg_id:
778+ msg_reply(self.msg_id, str(e))
779+ else:
780+ LOG.warning("No msg_id, cannot send error message")
781+
782+ LOG.warning("retval = %s", rval)
783+
784+ if self.msg_id:
785+ msg_reply(self.msg_id, rval)
786+
787 class Publisher(messaging.Publisher):
788 """Publisher base class"""
789 pass
790@@ -144,9 +195,11 @@
791
792 class AdapterConsumer(TopicConsumer):
793 """Calls methods on a proxy object based on method and args"""
794- def __init__(self, connection=None, topic="broadcast", proxy=None):
795+ def __init__(self, connection=None, topic="broadcast", proxy=None,
796+ twisted=True):
797 LOG.debug('Initing the Adapter Consumer for %s' % (topic))
798 self.proxy = proxy
799+ self.twisted = twisted
800 super(AdapterConsumer, self).__init__(connection=connection,
801 topic=topic)
802
803@@ -177,12 +230,21 @@
804
805 node_func = getattr(self.proxy, str(method))
806 node_args = dict((str(k), v) for k, v in args.iteritems())
807- # NOTE(vish): magic is fun!
808- # pylint: disable-msg=W0142
809- d = defer.maybeDeferred(node_func, **node_args)
810- if msg_id:
811- d.addCallback(lambda rval: msg_reply(msg_id, rval, None))
812- d.addErrback(lambda e: msg_reply(msg_id, None, e))
813+
814+ if self.twisted:
815+ # NOTE(vish): magic is fun!
816+ # pylint: disable-msg=W0142
817+ d = defer.maybeDeferred(node_func, **node_args)
818+ if msg_id:
819+ d.addCallback(lambda rval: msg_reply(msg_id, rval, None))
820+ d.addErrback(lambda e: msg_reply(msg_id, None, e))
821+ else:
822+ handler_thread = ProcessMessageThread()
823+ handler_thread.setName('Message handler: %s' % str(method))
824+ handler_thread.handler_function = node_func
825+ handler_thread.handler_args = node_args
826+ handler_thread.msg_id = msg_id
827+ handler_thread.start()
828 return
829
830
831@@ -227,14 +289,15 @@
832 if failure:
833 message = failure.getErrorMessage()
834 traceback = failure.getTraceback()
835- logging.error("Returning exception %s to caller", message)
836- logging.error(traceback)
837+ LOG.error("Returning exception %s to caller", message)
838+ LOG.error(traceback)
839 failure = (failure.type.__name__, str(failure.value), traceback)
840 conn = Connection.instance()
841 publisher = DirectPublisher(connection=conn, msg_id=msg_id)
842 try:
843 publisher.send({'result': reply, 'failure': failure})
844 except TypeError:
845+ LOG.exception("Error sending reply")
846 publisher.send(
847 {'result': dict((k, repr(v))
848 for k, v in reply.__dict__.iteritems()),
849@@ -338,3 +401,40 @@
850 # topic and a json sting representing a dictionary
851 # for the method
852 send_message(sys.argv[1], json.loads(sys.argv[2]))
853+
854+
855+
856+
857+class ConnectionManager(object):
858+ def __init__(self):
859+ self.connection_map_lock = threading.Lock()
860+ self.connection_map = {}
861+
862+ def _build_key(self, config):
863+ key = tuple(sorted(config.items()))
864+ return key
865+
866+ def _build_connection(self, config):
867+ LOG.debug('Building queue connection using %s', config)
868+ c = Connection(**config)
869+ c.queue_config = config
870+ c.connection_manager = self
871+ return c
872+
873+ def get_connection(self, config):
874+ key = self._build_key(config)
875+
876+ with self.connection_map_lock:
877+ c = self.connection_map.get(key)
878+ if not c:
879+ c = self._build_connection(config)
880+ self.connection_map[key] = c
881+ return c
882+
883+ def invalidate_connection(self, connection):
884+ key = self._build_key(connection.queue_config)
885+
886+ with self.connection_map_lock:
887+ c = self.connection_map.get(key)
888+ if c == connection:
889+ del self.connection_map[key]
890
891=== modified file 'nova/server.py'
892--- nova/server.py 2010-08-09 14:46:33 +0000
893+++ nova/server.py 2010-08-21 14:09:42 +0000
894@@ -22,7 +22,6 @@
895
896 import daemon
897 from daemon import pidlockfile
898-import logging
899 import logging.handlers
900 import os
901 import signal
902@@ -65,6 +64,7 @@
903 time.sleep(0.1)
904 except OSError, err:
905 err = str(err)
906+ # Should this be >= 0 ?
907 if err.find("No such process") > 0:
908 if os.path.exists(pidfile):
909 os.remove(pidfile)
910
911=== modified file 'nova/service.py'
912--- nova/service.py 2010-07-23 22:27:18 +0000
913+++ nova/service.py 2010-08-21 14:09:42 +0000
914@@ -1,4 +1,5 @@
915 # vim: tabstop=4 shiftwidth=4 softtabstop=4
916+from nova.threads import PollingThread
917
918 # Copyright 2010 United States Government as represented by the
919 # Administrator of the National Aeronautics and Space Administration.
920@@ -23,10 +24,13 @@
921 import inspect
922 import logging
923 import os
924+import signal
925+import time
926
927 from twisted.internet import defer
928 from twisted.internet import task
929 from twisted.application import service
930+from threading import Thread
931
932 from nova import datastore
933 from nova import flags
934@@ -40,64 +44,117 @@
935 'seconds between nodes reporting state to cloud',
936 lower_bound=1)
937
938-class Service(object, service.Service):
939+def handle_sig_int(signum, frame):
940+ logging.info('Received SIGINT')
941+ ThreadedService.keep_running = False
942+
943+class ThreadedService(object):
944 """Base class for workers that run on hosts"""
945-
946+ keep_running = True
947+
948 @classmethod
949 def create(cls,
950 report_interval=None, # defaults to flag
951 bin_name=None, # defaults to basename of executable
952 topic=None): # defaults to basename - "nova-" part
953+ return _create_common(cls, report_interval=report_interval,
954+ bin_name=bin_name, topic=topic, twisted=False)
955+
956+ @classmethod
957+ def main_loop(cls):
958+ signal.signal(signal.SIGINT, handle_sig_int)
959+ while ThreadedService.keep_running:
960+ time.sleep(1)
961+
962+def _create_common(cls,
963+ report_interval=None, # defaults to flag
964+ bin_name=None, # defaults to basename of executable
965+ topic=None, # defaults to basename - "nova-" part
966+ twisted=False):
967+ if True: # Hack to minimize diff
968 """Instantiates class and passes back application object"""
969- if not report_interval:
970- # NOTE(vish): set here because if it is set to flag in the
971- # parameter list, it wrongly uses the default
972- report_interval = FLAGS.report_interval
973 # NOTE(vish): magic to automatically determine bin_name and topic
974 if not bin_name:
975 bin_name = os.path.basename(inspect.stack()[-1][1])
976 if not topic:
977 topic = bin_name.rpartition("nova-")[2]
978- logging.warn("Starting %s node" % topic)
979+ logging.warn("Starting %s node", topic)
980 node_instance = cls()
981
982 conn = rpc.Connection.instance()
983 consumer_all = rpc.AdapterConsumer(
984 connection=conn,
985 topic='%s' % topic,
986- proxy=node_instance)
987+ proxy=node_instance,
988+ twisted=twisted)
989
990 consumer_node = rpc.AdapterConsumer(
991 connection=conn,
992 topic='%s.%s' % (topic, FLAGS.node_name),
993- proxy=node_instance)
994-
995- pulse = task.LoopingCall(node_instance.report_state,
996- FLAGS.node_name,
997- bin_name)
998- pulse.start(interval=report_interval, now=False)
999-
1000- consumer_all.attach_to_twisted()
1001- consumer_node.attach_to_twisted()
1002-
1003- # This is the parent service that twistd will be looking for when it
1004- # parses this file, return it so that we can get it into globals below
1005- application = service.Application(bin_name)
1006- node_instance.setServiceParent(application)
1007- return application
1008+ proxy=node_instance,
1009+ twisted=twisted)
1010+
1011+ if not report_interval:
1012+ # NOTE(vish): set here because if it is set to flag in the
1013+ # parameter list, it wrongly uses the default
1014+ report_interval = FLAGS.report_interval
1015+ if twisted:
1016+ pulse = task.LoopingCall(node_instance._report_state_twisted,
1017+ FLAGS.node_name,
1018+ bin_name)
1019+ pulse.start(interval=report_interval, now=False)
1020+
1021+ consumer_all.attach_to_twisted()
1022+ consumer_node.attach_to_twisted()
1023+ else:
1024+ report_state_thread = ReportStateThread()
1025+ report_state_thread.setName('Report state thread')
1026+ report_state_thread.task_interval = report_interval
1027+ report_state_thread.nodename = FLAGS.node_name
1028+ report_state_thread.daemon = bin_name
1029+ report_state_thread.setDaemon(True)
1030+ report_state_thread.start()
1031+
1032+ consumer_all.start_background_thread()
1033+ consumer_node.start_background_thread()
1034+
1035+ if twisted:
1036+ # This is the parent service that twistd will be looking for when it
1037+ # parses this file, return it so that we can get it into globals below
1038+ application = service.Application(bin_name)
1039+ node_instance.setServiceParent(application)
1040+ return application
1041+
1042+class TwistedService(object, service.Service):
1043+ @classmethod
1044+ def create(cls,
1045+ report_interval=None, # defaults to flag
1046+ bin_name=None, # defaults to basename of executable
1047+ topic=None): # defaults to basename - "nova-" part
1048+ return _create_common(cls, report_interval=report_interval,
1049+ bin_name=bin_name, topic=topic, twisted=True)
1050+
1051
1052 @defer.inlineCallbacks
1053- def report_state(self, nodename, daemon):
1054- # TODO(termie): make this pattern be more elegant. -todd
1055- try:
1056- record = model.Daemon(nodename, daemon)
1057- record.heartbeat()
1058- if getattr(self, "model_disconnected", False):
1059- self.model_disconnected = False
1060- logging.error("Recovered model server connection!")
1061-
1062- except datastore.ConnectionError, ex:
1063- if not getattr(self, "model_disconnected", False):
1064- self.model_disconnected = True
1065- logging.exception("model server went away")
1066+ def _report_state_twisted(self, nodename, daemon):
1067+ _report_state_common(self, nodename, daemon)
1068 yield
1069+
1070+def _report_state_common(target, nodename, daemon):
1071+ # TODO(termie): make this pattern be more elegant. -todd
1072+ try:
1073+ record = model.Daemon(nodename, daemon)
1074+ record.heartbeat()
1075+ if getattr(target, "model_disconnected", False):
1076+ target.model_disconnected = False
1077+ logging.error("Recovered model server connection!")
1078+ except datastore.ConnectionError, ex:
1079+ if not getattr(target, "model_disconnected", False):
1080+ target.model_disconnected = True
1081+ logging.exception("model server went away")
1082+
1083+class ReportStateThread(PollingThread):
1084+ """ Thread that reports state back periodically (heartbeat) """
1085+ def _do_task(self):
1086+ _report_state_common(self, self.nodename, self.daemon)
1087+
1088
1089=== added file 'nova/threads.py'
1090--- nova/threads.py 1970-01-01 00:00:00 +0000
1091+++ nova/threads.py 2010-08-21 14:09:42 +0000
1092@@ -0,0 +1,40 @@
1093+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1094+
1095+# Copyright 2010 FathomDB Inc.
1096+# All Rights Reserved.
1097+#
1098+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1099+# not use this file except in compliance with the License. You may obtain
1100+# a copy of the License at
1101+#
1102+# http://www.apache.org/licenses/LICENSE-2.0
1103+#
1104+# Unless required by applicable law or agreed to in writing, software
1105+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1106+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1107+# License for the specific language governing permissions and limitations
1108+# under the License.
1109+
1110+from threading import Thread
1111+import logging
1112+import time
1113+
1114+"""
1115+Helper functions for dealing with (simple python) threads
1116+"""
1117+
1118+class PollingThread(Thread):
1119+ """ Thread that does something periodically """
1120+ def run(self):
1121+ while True:
1122+ try:
1123+ self._run()
1124+ except Exception as e:
1125+ logging.exception("Error on thread %s", self.name)
1126+
1127+ def _run(self):
1128+ time.sleep(self.task_interval)
1129+ self._do_task()
1130+
1131+ def _do_task(self):
1132+ raise Exception("Must implement _do_task")
1133
1134=== modified file 'nova/twistd.py'
1135--- nova/twistd.py 2010-08-12 18:52:32 +0000
1136+++ nova/twistd.py 2010-08-21 14:09:42 +0000
1137@@ -193,6 +193,7 @@
1138 time.sleep(0.1)
1139 except OSError, err:
1140 err = str(err)
1141+ # Should this be >= 0 ?
1142 if err.find("No such process") > 0:
1143 if os.path.exists(pidfile):
1144 os.remove(pidfile)
1145@@ -201,7 +202,7 @@
1146 sys.exit(1)
1147
1148
1149-def serve(filename):
1150+def serve(filename, twisted=True):
1151 logging.debug("Serving %s" % filename)
1152 name = os.path.basename(filename)
1153 OptionsClass = WrapTwistedOptions(TwistdServerOptions)
1154@@ -256,4 +257,5 @@
1155 for flag in FLAGS:
1156 logging.debug("%s : %s" % (flag, FLAGS.get(flag, None)))
1157
1158- twistd.runApp(options)
1159+ if twisted:
1160+ twistd.runApp(options)
1161
1162=== modified file 'nova/utils.py'
1163--- nova/utils.py 2010-08-18 21:14:24 +0000
1164+++ nova/utils.py 2010-08-21 14:09:42 +0000
1165@@ -36,6 +36,16 @@
1166 FLAGS = flags.FLAGS
1167 TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
1168
1169+class ProcessExecutionError(IOError):
1170+ def __init__( self, stdout=None, stderr=None, exit_code=None, cmd=None,
1171+ description=None):
1172+ if description is None:
1173+ description = "Unexpected error while running command."
1174+ if exit_code is None:
1175+ exit_code = '-'
1176+ message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % (
1177+ description, cmd, exit_code, stdout, stderr)
1178+ IOError.__init__(self, message)
1179
1180 def import_class(import_str):
1181 """Returns a class from a string including module and class"""
1182@@ -59,6 +69,7 @@
1183 execute("curl --fail %s -o %s" % (url, target))
1184
1185 def execute(cmd, process_input=None, addl_env=None, check_exit_code=True):
1186+ logging.debug("Running command %s" % cmd)
1187 env = os.environ.copy()
1188 if addl_env:
1189 env.update(addl_env)
1190@@ -73,8 +84,11 @@
1191 if obj.returncode:
1192 logging.debug("Result was %s" % (obj.returncode))
1193 if check_exit_code and obj.returncode <> 0:
1194- raise Exception( "Unexpected exit code: %s. result=%s"
1195- % (obj.returncode, result))
1196+ (stdout, stderr) = result
1197+ raise ProcessExecutionError(exit_code=obj.returncode,
1198+ stdout=stdout,
1199+ stderr=stderr,
1200+ cmd=cmd)
1201 return result
1202
1203
1204
1205=== modified file 'nova/virt/connection.py'
1206--- nova/virt/connection.py 2010-08-13 10:51:33 +0000
1207+++ nova/virt/connection.py 2010-08-21 14:09:42 +0000
1208@@ -17,11 +17,14 @@
1209 # License for the specific language governing permissions and limitations
1210 # under the License.
1211
1212+import logging
1213+import sys
1214+
1215 from nova import flags
1216 from nova.virt import fake
1217 from nova.virt import libvirt_conn
1218 from nova.virt import xenapi
1219-
1220+from nova.virt import virtualbox
1221
1222 FLAGS = flags.FLAGS
1223
1224@@ -39,16 +42,21 @@
1225 # TODO(termie): maybe lazy load after initial check for permissions
1226 # TODO(termie): check whether we can be disconnected
1227 t = FLAGS.connection_type
1228+ print "connection_type %s" % t
1229 if t == 'fake':
1230 conn = fake.get_connection(read_only)
1231 elif t == 'libvirt':
1232 conn = libvirt_conn.get_connection(read_only)
1233 elif t == 'xenapi':
1234 conn = xenapi.get_connection(read_only)
1235+ elif t == 'virtualbox':
1236+ conn = virtualbox.get_connection(read_only)
1237 else:
1238- raise Exception('Unknown connection type "%s"' % t)
1239+ logging.error('Unknown connection type "%s"', t)
1240+ sys.exit(1)
1241
1242 if conn is None:
1243 logging.error('Failed to open connection to the hypervisor')
1244 sys.exit(1)
1245+
1246 return conn
1247
1248=== modified file 'nova/virt/images.py'
1249--- nova/virt/images.py 2010-08-18 21:14:24 +0000
1250+++ nova/virt/images.py 2010-08-21 14:09:42 +0000
1251@@ -29,6 +29,7 @@
1252 from nova import process
1253 from nova.auth import manager
1254 from nova.auth import signer
1255+from nova.auth import manager
1256
1257
1258 FLAGS = flags.FLAGS
1259@@ -43,7 +44,6 @@
1260 f = _fetch_local_image
1261 return f(image, path, user, project)
1262
1263-
1264 def _fetch_s3_image(image, path, user, project):
1265 url = image_url(image)
1266
1267@@ -53,11 +53,11 @@
1268 headers = {}
1269 headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())
1270
1271- (_, _, url_path, _, _, _) = urlparse.urlparse(url)
1272+ uri = '/' + url.partition('/')[2]
1273 access = manager.AuthManager().get_access_key(user, project)
1274 signature = signer.Signer(user.secret.encode()).s3_authorization(headers,
1275 'GET',
1276- url_path)
1277+ uri)
1278 headers['Authorization'] = 'AWS %s:%s' % (access, signature)
1279
1280 cmd = ['/usr/bin/curl', '--fail', '--silent', url]
1281@@ -67,8 +67,7 @@
1282 cmd += ['-o', path]
1283 return process.SharedPool().execute(executable=cmd[0], args=cmd[1:])
1284
1285-
1286-def _fetch_local_image(image, path, user, project):
1287+def _fetch_local_image(image, path, _user, _project):
1288 source = _image_path('%s/image' % image)
1289 return process.simple_execute('cp %s %s' % (source, path))
1290
1291
1292=== modified file 'nova/virt/libvirt_conn.py'
1293--- nova/virt/libvirt_conn.py 2010-08-21 14:09:42 +0000
1294+++ nova/virt/libvirt_conn.py 2010-08-21 14:09:42 +0000
1295@@ -244,6 +244,8 @@
1296
1297 key = data['key_data']
1298 net = None
1299+ dns = None
1300+
1301 if data.get('inject_network', False):
1302 with open(FLAGS.injected_network_template) as f:
1303 net = f.read() % {'address': data['private_dns_name'],
1304@@ -281,6 +283,7 @@
1305
1306 # TODO(termie): lazy lazy hack because xml is annoying
1307 xml_info['nova'] = json.dumps(instance.datamodel.copy())
1308+ xml_info['type'] = FLAGS.libvirt_type
1309
1310 if xml_info['kernel_id']:
1311 xml_info['kernel'] = xml_info['basepath'] + "/kernel"
1312@@ -311,6 +314,12 @@
1313 'cpu_time': cpu_time}
1314
1315 def get_disks(self, instance_id):
1316+ """
1317+ Note that this function takes an instance ID, not an Instance, so
1318+ that it can be called by monitor.
1319+
1320+ Returns a list of all block devices for this domain.
1321+ """
1322 domain = self._conn.lookupByName(instance_id)
1323 # TODO(devcamcar): Replace libxml2 with etree.
1324 xml = domain.XMLDesc(0)
1325@@ -347,6 +356,12 @@
1326 return disks
1327
1328 def get_interfaces(self, instance_id):
1329+ """
1330+ Note that this function takes an instance ID, not an Instance, so
1331+ that it can be called by monitor.
1332+
1333+ Returns a list of all network interfaces for this instance.
1334+ """
1335 domain = self._conn.lookupByName(instance_id)
1336 # TODO(devcamcar): Replace libxml2 with etree.
1337 xml = domain.XMLDesc(0)
1338@@ -383,9 +398,17 @@
1339 return interfaces
1340
1341 def block_stats(self, instance_id, disk):
1342+ """
1343+ Note that this function takes an instance ID, not an Instance, so
1344+ that it can be called by monitor.
1345+ """
1346 domain = self._conn.lookupByName(instance_id)
1347 return domain.blockStats(disk)
1348
1349 def interface_stats(self, instance_id, interface):
1350+ """
1351+ Note that this function takes an instance ID, not an Instance, so
1352+ that it can be called by monitor.
1353+ """
1354 domain = self._conn.lookupByName(instance_id)
1355 return domain.interfaceStats(interface)
1356
1357=== added file 'nova/virt/virtualbox.py'
1358--- nova/virt/virtualbox.py 1970-01-01 00:00:00 +0000
1359+++ nova/virt/virtualbox.py 2010-08-21 14:09:42 +0000
1360@@ -0,0 +1,332 @@
1361+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1362+import uuid
1363+import time
1364+
1365+# Copyright 2010 FathomDB Inc
1366+# All Rights Reserved.
1367+#
1368+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1369+# not use this file except in compliance with the License. You may obtain
1370+# a copy of the License at
1371+#
1372+# http://www.apache.org/licenses/LICENSE-2.0
1373+#
1374+# Unless required by applicable law or agreed to in writing, software
1375+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1376+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1377+# License for the specific language governing permissions and limitations
1378+# under the License.
1379+
1380+"""
1381+A connection to VirtualBox; this actually uses the VBoxManage command line tool
1382+because libvirt is problematic with Virtualbox and on a Mac
1383+"""
1384+
1385+import logging
1386+import os.path
1387+import shutil
1388+import time
1389+
1390+from nova import exception
1391+from nova import flags
1392+from nova import utils
1393+from nova.auth import manager
1394+from nova.compute import disk
1395+from nova.compute import instance_types
1396+from nova.compute import power_state
1397+from nova.virt import images
1398+
1399+from Cheetah.Template import Template
1400+
1401+FLAGS = flags.FLAGS
1402+flags.DEFINE_string('virtualbox_bridge_adapter',
1403+ 'eth0',
1404+ 'Network adapter to use for VirtualBox bridged networking')
1405+
1406+def get_connection(read_only):
1407+ return VirtualBoxConnection(read_only)
1408+
1409+
1410+class VirtualBoxConnection(object):
1411+ def __init__(self, read_only):
1412+ pass
1413+
1414+ def list_instances(self):
1415+ out, err = utils.execute('VBoxManage --nologo list vms')
1416+ if err:
1417+ raise exception.Error('Could not list virtualbox vms: %s' % err)
1418+
1419+ instance_ids = []
1420+ for line in out.splitlines():
1421+ tokens = line.split()
1422+ if len(tokens) == 2:
1423+ instance_id = tokens[0].strip('"')
1424+ instance_ids.append(instance_id)
1425+
1426+ return instance_ids
1427+
1428+ def destroy(self, instance):
1429+ _, err = utils.execute('VBoxManage --nologo controlvm %s poweroff' % instance.name)
1430+ if err:
1431+ # TODO(justinsb): If the instance was already stopped, we should accept that and continue...
1432+ raise exception.Error('Could not stop virtualbox vm: %s' % err)
1433+
1434+ while True:
1435+ try:
1436+ instance.update_state()
1437+ if instance.state == power_state.SHUTOFF:
1438+ return
1439+ except Exception:
1440+ logging.exception("Error waiting for machine to shut down")
1441+ instance.set_state(power_state.SHUTOFF)
1442+ return
1443+ time.sleep(0.5)
1444+
1445+ def _cleanup(self, instance):
1446+ target = os.path.abspath(instance.datamodel['basepath'])
1447+ logging.info("Deleting instance files at %s", target)
1448+ shutil.rmtree(target)
1449+
1450+ @exception.wrap_exception
1451+ def reboot(self, instance):
1452+ _, err = utils.execute('VBoxManage --nologo controlvm %s reset' % instance.name)
1453+ if err:
1454+ raise exception.Error('Could not reset virtualbox vm: %s' % err)
1455+ # TODO(justinsb): Should we wait for restart?
1456+
1457+ def _register_image(self, instance):
1458+ # See e.g. http://www.uhleeka.com/blog/2009/12/virtualbox-3-1-install-windows-xp-guest-on-ubuntu-9-04-host/
1459+ instance_name = instance.name
1460+ base_folder = self.basepath(instance)
1461+ image_path = self._image_path(instance)
1462+ image_uuid = uuid.uuid4()
1463+ memory_size = 1024
1464+ net_bridge_adapter = str(FLAGS.virtualbox_bridge_adapter)
1465+
1466+ _, err = utils.execute('VBoxManage --nologo createvm --name %s --register --basefolder %s' % (instance_name, base_folder))
1467+ if err:
1468+ raise exception.Error('Could not create virtualbox vm: %s' % err)
1469+
1470+ _, err = utils.execute('VBoxManage --nologo storagectl %s --name "SATA Controller" --add sata --controller IntelAhci' % (instance_name, ))
1471+ if err:
1472+ raise exception.Error('Could not create SATA controller for virtualbox vm: %s' % err)
1473+
1474+ # VirtualBox seems to pull a UUID from the disk.
1475+ # Thus we have to specify the UUID, or else VirtualBox refuses to
1476+ # register duplicate images because they have duplicate UUIDs
1477+ _, err = utils.execute('VBoxManage --nologo openmedium disk %s --uuid=%s' % (image_path, image_uuid))
1478+ if err:
1479+ raise exception.Error('Could not register disk with virtualbox vm: %s' % err)
1480+
1481+ _, err = utils.execute('VBoxManage --nologo storageattach %s --storagectl "SATA Controller" --port 0 --device 0 --type hdd --medium %s' % (instance_name, image_uuid))
1482+ if err:
1483+ raise exception.Error('Could not attach disk to virtualbox vm: %s' % err)
1484+
1485+ # TODO(justinsb): Do we need --sata on --sataportcount 30 ??
1486+ cmd = 'VBoxManage --nologo modifyvm %s --memory %s --vram 64 --nic1 bridged --bridgeadapter1 %s' % (instance_name, memory_size, net_bridge_adapter)
1487+ _, err = utils.execute(cmd)
1488+ if err:
1489+ raise exception.Error('Could not configure virtualbox vm: %s' % err)
1490+
1491+ def _start_image(self, instance):
1492+ instance_name = instance.name
1493+ _, err = utils.execute('VBoxManage --nologo startvm %s' % instance_name)
1494+ if err:
1495+ raise exception.Error('Could not start virtualbox instance %s: %s' % (instance_name, err))
1496+
1497+ @exception.wrap_exception
1498+ def spawn(self, instance):
1499+ instance.set_state(power_state.NOSTATE, 'launching')
1500+
1501+ self._create_image(instance)
1502+
1503+ self._register_image(instance)
1504+
1505+ self._start_image(instance)
1506+
1507+ # TODO(termie): this should actually register
1508+ # a callback to check for successful boot
1509+ logging.debug("Instance is running")
1510+
1511+ while True:
1512+ try:
1513+ instance.update_state()
1514+ if instance.is_running():
1515+ logging.debug('booted instance %s', instance.name)
1516+ return
1517+ except Exception as exn:
1518+ logging.error("_wait_for_boot exception %s", exn)
1519+ instance.set_state(power_state.SHUTDOWN)
1520+ logging.error('Failed to boot instance %s', instance.name)
1521+ return
1522+ time.sleep(0.5)
1523+
1524+
1525+ def _create_image(self, instance):
1526+ # syntactic nicety
1527+ data = instance.datamodel
1528+ basepath = lambda x = '': self.basepath(instance, x)
1529+
1530+ # ensure directories exist and are writable
1531+ utils.execute('mkdir -p %s' % basepath())
1532+ utils.execute('chmod 0777 %s' % basepath())
1533+
1534+
1535+ user = manager.AuthManager().get_user(data['user_id'])
1536+ project = manager.AuthManager().get_project(data['project_id'])
1537+
1538+ if not os.path.exists(basepath('disk')):
1539+ images.fetch(data['image_id'], basepath('disk-raw'), user, project)
1540+
1541+ using_kernel = data['kernel_id'] and True
1542+
1543+ if using_kernel:
1544+ if not os.path.exists(basepath('kernel')):
1545+ images.fetch(data['kernel_id'], basepath('kernel'), user, project)
1546+ if not os.path.exists(basepath('ramdisk')):
1547+ images.fetch(data['ramdisk_id'], basepath('ramdisk'), user, project)
1548+
1549+ execute = lambda cmd, process_input = None: \
1550+ utils.execute( cmd=cmd,
1551+ process_input=process_input,
1552+ check_exit_code=True)
1553+
1554+ # For now, we assume that if we're not using a kernel, we're using a partitioned disk image
1555+ # where the target partition is the first partition
1556+ target_partition = None
1557+ if not using_kernel:
1558+ target_partition = "1"
1559+
1560+ key = data['key_data']
1561+ net = None
1562+ dns = None
1563+
1564+ if data.get('inject_network', False):
1565+ network_info = { 'address': data['private_dns_name'],
1566+ 'network': data['network_network'],
1567+ 'netmask': data['network_netmask'],
1568+ 'gateway': data['network_gateway'],
1569+ 'broadcast': data['network_broadcast'],
1570+ 'dns': data['network_dns']}
1571+
1572+ with open(FLAGS.injected_network_template) as f:
1573+ net = f.read() % network_info
1574+
1575+ #with open(FLAGS.simple_network_dns_template) as f:
1576+ # dns = str(Template(f.read(), searchList=[ network_info ]))
1577+
1578+
1579+ if key or net or dns:
1580+ logging.info('Injecting data into image %s', data['image_id'])
1581+ try:
1582+ disk.inject_data(basepath('disk-raw'), key=key, net=net, dns=dns, remove_network_udev=True, partition=target_partition, execute=execute)
1583+ except Exception as e:
1584+ # This could be a windows image, or a vmdk format disk
1585+ logging.warn('Could not inject data; ignoring. (%s)', e)
1586+
1587+ if using_kernel:
1588+ if os.path.exists(basepath('disk')):
1589+ utils.execute('rm -f %s' % basepath('disk'))
1590+
1591+ disk_bytes = (instance_types.INSTANCE_TYPES[data['instance_type']]['local_gb']
1592+ * 1024 * 1024 * 1024)
1593+ disk.partition(
1594+ basepath('disk-raw'), basepath('disk'), disk_bytes, execute=execute)
1595+
1596+
1597+ def _uses_kernel(self, instance):
1598+ if instance.datamodel['ramdisk_id'] or instance.datamodel['kernel_id']:
1599+ return True
1600+ return False
1601+
1602+ def basepath(self, instance, path=''):
1603+ return os.path.abspath(os.path.join(instance.datamodel['basepath'], path))
1604+
1605+ def _image_path(self, instance):
1606+ if self._uses_kernel(instance):
1607+ return instance.datamodel['basepath'] + '/disk'
1608+ else:
1609+ return instance.datamodel['basepath'] + '/disk-raw'
1610+
1611+ def _get_virtualbox_info(self, instance_id):
1612+ out, err = utils.execute('VBoxManage --nologo showvminfo %s --machinereadable' % instance_id, check_exit_code=False)
1613+ if out.find("VBOX_E_OBJECT_NOT_FOUND") != -1:
1614+ return {}
1615+
1616+ if err:
1617+ raise exception.Error('Could not get virtualbox info: %s' % err)
1618+ # The output is a series of lines, each containing a key="value" pair
1619+ info_map = {}
1620+ for line in out.splitlines():
1621+ line = line.strip()
1622+ if line == "":
1623+ continue
1624+ key, sep, value = line.partition('=')
1625+ if sep != '=':
1626+ if line.find('E_ACCESSDENIED') != -1:
1627+ if 'poweroff' == info_map.get('VMState', None):
1628+ # When the machine is being destroyed, we get some status and then E_ACCESSDENED; return what we've got
1629+ break
1630+ logging.info("Could not parse virtualbox line: %s", line)
1631+ raise exception.Error('Could not parse virtualbox info: %s' % out)
1632+ info_map[key] = value.strip('"')
1633+
1634+ logging.debug("Virtual box info %s", info_map)
1635+
1636+ return info_map
1637+
1638+ def get_info(self, instance_id):
1639+ vb_info = self._get_virtualbox_info(instance_id)
1640+
1641+ state = self.instance_state_from_virtualbox_state[vb_info.get('VMState', None)]
1642+ max_mem = vb_info.get('memory', 0)
1643+ mem = max_mem
1644+ num_cpu = vb_info.get('cpus', 0)
1645+ cpu_time = 0
1646+
1647+ return {'state': state, 'max_mem': max_mem, 'mem': mem, 'num_cpu': num_cpu, 'cpu_time': cpu_time}
1648+
1649+ def get_disks(self, instance_id):
1650+ """
1651+ Note that this function takes an instance ID, not an Instance, so
1652+ that it can be called by monitor.
1653+
1654+ Returns a list of all block devices for this domain.
1655+ """
1656+ raise Exception('Not yet implemented')
1657+
1658+
1659+ def get_interfaces(self, instance_id):
1660+ """
1661+ Note that this function takes an instance ID, not an Instance, so
1662+ that it can be called by monitor.
1663+
1664+ Returns a list of all network interfaces for this instance.
1665+ """
1666+ raise Exception('Not yet implemented')
1667+
1668+
1669+ def block_stats(self, instance_id, disk):
1670+ """
1671+ Note that this function takes an instance ID, not an Instance, so
1672+ that it can be called by monitor.
1673+ """
1674+ raise Exception('Not yet implemented')
1675+
1676+
1677+ def interface_stats(self, instance_id, interface):
1678+ """
1679+ Note that this function takes an instance ID, not an Instance, so
1680+ that it can be called by monitor.
1681+ """
1682+ raise Exception('Not yet implemented')
1683+
1684+ instance_state_from_virtualbox_state = {
1685+ None : power_state.NOSTATE,
1686+ 'poweroff': power_state.SHUTOFF,
1687+ 'starting': power_state.RUNNING,
1688+ 'running' : power_state.RUNNING,
1689+ #'Running' : power_state.RUNNING,
1690+ #'Paused' : power_state.PAUSED,
1691+ #'Crashed' : power_state.CRASHED
1692+ }
1693
1694=== modified file 'nova/volume/service.py'
1695--- nova/volume/service.py 2010-08-18 21:19:39 +0000
1696+++ nova/volume/service.py 2010-08-21 14:09:42 +0000
1697@@ -77,7 +77,7 @@
1698 raise exception.Error("Volume does not exist")
1699
1700
1701-class VolumeService(service.Service):
1702+class VolumeService(service.TwistedService):
1703 """
1704 There is one VolumeNode running on each host.
1705 However, each VolumeNode can report on the state of