Merge lp:~midokura/nova/network-service into lp:~ntt-pf-lab/nova/network-service

Proposed by Ryu Ishimoto
Status: Merged
Merged at revision: 773
Proposed branch: lp:~midokura/nova/network-service
Merge into: lp:~ntt-pf-lab/nova/network-service
Diff against target: 64148 lines (+34518/-21481)
129 files modified
.bzrignore (+6/-7)
Authors (+3/-0)
MANIFEST.in (+1/-1)
bin/nova-ajax-console-proxy (+10/-11)
bin/nova-dhcpbridge (+3/-1)
bin/nova-manage (+72/-80)
bin/nova-vncproxy (+101/-0)
doc/source/devref/zone.rst (+127/-0)
doc/source/man/novamanage.rst (+10/-0)
doc/source/runnova/vncconsole.rst (+76/-0)
nova/CA/geninter.sh (+1/-1)
nova/CA/genrootca.sh (+2/-1)
nova/CA/openssl.cnf.tmpl (+5/-1)
nova/adminclient.py (+0/-476)
nova/api/direct.py (+5/-1)
nova/api/ec2/cloud.py (+80/-27)
nova/api/openstack/__init__.py (+23/-12)
nova/api/openstack/accounts.py (+2/-3)
nova/api/openstack/backup_schedules.py (+7/-3)
nova/api/openstack/common.py (+17/-0)
nova/api/openstack/consoles.py (+2/-2)
nova/api/openstack/contrib/__init__.py (+22/-0)
nova/api/openstack/contrib/volumes.py (+336/-0)
nova/api/openstack/extensions.py (+156/-73)
nova/api/openstack/faults.py (+5/-3)
nova/api/openstack/flavors.py (+5/-2)
nova/api/openstack/image_metadata.py (+14/-1)
nova/api/openstack/images.py (+106/-213)
nova/api/openstack/ips.py (+72/-0)
nova/api/openstack/limits.py (+2/-2)
nova/api/openstack/server_metadata.py (+2/-1)
nova/api/openstack/servers.py (+88/-29)
nova/api/openstack/shared_ip_groups.py (+5/-5)
nova/api/openstack/users.py (+1/-2)
nova/api/openstack/versions.py (+8/-2)
nova/api/openstack/views/addresses.py (+8/-2)
nova/api/openstack/views/images.py (+91/-11)
nova/api/openstack/views/servers.py (+14/-14)
nova/api/openstack/zones.py (+2/-4)
nova/compute/api.py (+39/-10)
nova/compute/instance_types.py (+37/-35)
nova/compute/manager.py (+49/-16)
nova/crypto.py (+10/-2)
nova/db/api.py (+5/-0)
nova/db/sqlalchemy/api.py (+25/-1)
nova/db/sqlalchemy/migrate_repo/versions/014_add_instance_type_id_to_instances.py (+84/-0)
nova/db/sqlalchemy/migrate_repo/versions/014_diablo.py (+0/-92)
nova/db/sqlalchemy/migrate_repo/versions/015_diablo.py (+93/-0)
nova/db/sqlalchemy/models.py (+7/-1)
nova/image/fake.py (+113/-0)
nova/image/glance.py (+15/-33)
nova/image/local.py (+13/-3)
nova/image/s3.py (+8/-37)
nova/image/service.py (+27/-0)
nova/network/api.py (+15/-0)
nova/network/linux_net.py (+10/-1)
nova/network/manager.py (+1/-0)
nova/network/xenapi_net.py (+85/-0)
nova/rpc.py (+8/-3)
nova/scheduler/chance.py (+3/-1)
nova/scheduler/simple.py (+9/-3)
nova/scheduler/zone.py (+4/-1)
nova/tests/api/openstack/extensions/__init__.py (+15/-0)
nova/tests/api/openstack/test_api.py (+4/-4)
nova/tests/api/openstack/test_faults.py (+106/-23)
nova/tests/api/openstack/test_image_metadata.py (+39/-2)
nova/tests/api/openstack/test_images.py (+463/-37)
nova/tests/api/openstack/test_limits.py (+13/-5)
nova/tests/api/openstack/test_servers.py (+371/-66)
nova/tests/api/openstack/test_shared_ip_groups.py (+27/-3)
nova/tests/api/openstack/test_versions.py (+26/-0)
nova/tests/db/fakes.py (+61/-26)
nova/tests/fake_utils.py (+10/-7)
nova/tests/image/test_glance.py (+54/-9)
nova/tests/integrated/api/client.py (+37/-3)
nova/tests/integrated/integrated_helpers.py (+99/-24)
nova/tests/integrated/test_extensions.py (+44/-0)
nova/tests/integrated/test_login.py (+5/-16)
nova/tests/integrated/test_servers.py (+184/-0)
nova/tests/integrated/test_volumes.py (+295/-0)
nova/tests/integrated/test_xml.py (+56/-0)
nova/tests/test_auth.py (+4/-4)
nova/tests/test_cloud.py (+77/-1)
nova/tests/test_compute.py (+22/-9)
nova/tests/test_console.py (+1/-1)
nova/tests/test_instance_types.py (+5/-1)
nova/tests/test_quota.py (+11/-6)
nova/tests/test_scheduler.py (+1/-1)
nova/tests/test_virt.py (+46/-2)
nova/tests/test_volume.py (+1/-1)
nova/tests/test_xenapi.py (+60/-51)
nova/virt/disk.py (+35/-0)
nova/virt/driver.py (+18/-9)
nova/virt/fake.py (+15/-10)
nova/virt/hyperv.py (+4/-0)
nova/virt/libvirt.xml.template (+24/-10)
nova/virt/libvirt_conn.py (+292/-70)
nova/virt/vmwareapi/vim.py (+29/-25)
nova/virt/vmwareapi_conn.py (+2/-1)
nova/virt/xenapi/fake.py (+107/-65)
nova/virt/xenapi/network_utils.py (+17/-2)
nova/virt/xenapi/vm_utils.py (+29/-8)
nova/virt/xenapi/vmops.py (+142/-94)
nova/virt/xenapi_conn.py (+8/-1)
nova/vnc/__init__.py (+34/-0)
nova/vnc/auth.py (+138/-0)
nova/vnc/proxy.py (+131/-0)
nova/volume/driver.py (+75/-2)
nova/wsgi.py (+42/-5)
plugins/xenserver/xenapi/etc/xapi.d/plugins/agent (+77/-6)
plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py (+19/-16)
po/ast.po (+2487/-1769)
po/cs.po (+2508/-1777)
po/da.po (+2488/-1770)
po/de.po (+2531/-1779)
po/es.po (+3000/-1820)
po/it.po (+2541/-1784)
po/ja.po (+2989/-1791)
po/pt_BR.po (+2677/-1798)
po/ru.po (+2608/-1779)
po/uk.po (+2522/-1775)
po/zh_CN.po (+2585/-1780)
setup.py (+16/-0)
smoketests/test_admin.py (+1/-1)
smoketests/test_sysadmin.py (+3/-2)
tools/euca-get-ajax-console (+6/-0)
tools/eventlet-patch (+24/-0)
tools/install_venv.py (+6/-0)
tools/pip-requires (+1/-0)
To merge this branch: bzr merge lp:~midokura/nova/network-service
Reviewer Review Type Date Requested Status
NTT PF Lab. Pending
Review via email: mp+57303@code.launchpad.net

Description of the change

Merged trunk

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2011-02-21 20:35:30 +0000
3+++ .bzrignore 2011-04-12 11:01:25 +0000
4@@ -5,12 +5,11 @@
5 keys
6 networks
7 nova.sqlite
8-CA/cacert.pem
9-CA/crl.pem
10-CA/index.txt*
11-CA/openssl.cnf
12-CA/serial*
13-CA/newcerts/*.pem
14-CA/private/cakey.pem
15+CA
16 nova/vcsversion.py
17 *.DS_Store
18+.project
19+.pydevproject
20+clean.sqlite
21+run_tests.log
22+tests.sqlite
23
24=== modified file 'Authors'
25--- Authors 2011-03-25 13:38:57 +0000
26+++ Authors 2011-04-12 11:01:25 +0000
27@@ -12,6 +12,7 @@
28 Chmouel Boudjnah <chmouel@chmouel.com>
29 Chris Behrens <cbehrens@codestud.com>
30 Christian Berendt <berendt@b1-systems.de>
31+Chuck Short <zulcss@ubuntu.com>
32 Cory Wright <corywright@gmail.com>
33 Dan Prince <dan.prince@rackspace.com>
34 David Pravec <David.Pravec@danix.org>
35@@ -30,7 +31,9 @@
36 Jesse Andrews <anotherjesse@gmail.com>
37 Joe Heck <heckj@mac.com>
38 Joel Moore <joelbm24@gmail.com>
39+Johannes Erdfelt <johannes.erdfelt@rackspace.com>
40 John Dewey <john@dewey.ws>
41+John Tran <jtran@attinteractive.com>
42 Jonathan Bryce <jbryce@jbryce.com>
43 Jordan Rinke <jordan@openstack.org>
44 Josh Durgin <joshd@hq.newdream.net>
45
46=== modified file 'MANIFEST.in'
47--- MANIFEST.in 2011-03-14 20:10:11 +0000
48+++ MANIFEST.in 2011-04-12 11:01:25 +0000
49@@ -1,7 +1,7 @@
50 include HACKING LICENSE run_tests.py run_tests.sh
51 include README builddeb.sh exercise_rsapi.py
52 include ChangeLog MANIFEST.in pylintrc Authors
53-graft CA
54+graft nova/CA
55 graft doc
56 graft smoketests
57 graft tools
58
59=== modified file 'bin/nova-ajax-console-proxy'
60--- bin/nova-ajax-console-proxy 2011-03-18 13:56:05 +0000
61+++ bin/nova-ajax-console-proxy 2011-04-12 11:01:25 +0000
62@@ -108,17 +108,17 @@
63 return "Server Error"
64
65 def register_listeners(self):
66- class Callback:
67- def __call__(self, data, message):
68- if data['method'] == 'authorize_ajax_console':
69- AjaxConsoleProxy.tokens[data['args']['token']] = \
70- {'args': data['args'], 'last_activity': time.time()}
71+ class TopicProxy():
72+ @staticmethod
73+ def authorize_ajax_console(context, **kwargs):
74+ AjaxConsoleProxy.tokens[kwargs['token']] = \
75+ {'args': kwargs, 'last_activity': time.time()}
76
77 conn = rpc.Connection.instance(new=True)
78- consumer = rpc.TopicConsumer(
79- connection=conn,
80- topic=FLAGS.ajax_console_proxy_topic)
81- consumer.register_callback(Callback())
82+ consumer = rpc.TopicAdapterConsumer(
83+ connection=conn,
84+ proxy=TopicProxy,
85+ topic=FLAGS.ajax_console_proxy_topic)
86
87 def delete_expired_tokens():
88 now = time.time()
89@@ -130,8 +130,7 @@
90 for k in to_delete:
91 del AjaxConsoleProxy.tokens[k]
92
93- utils.LoopingCall(consumer.fetch, auto_ack=True,
94- enable_callbacks=True).start(0.1)
95+ utils.LoopingCall(consumer.fetch, enable_callbacks=True).start(0.1)
96 utils.LoopingCall(delete_expired_tokens).start(1)
97
98 if __name__ == '__main__':
99
100=== modified file 'bin/nova-dhcpbridge'
101--- bin/nova-dhcpbridge 2011-03-14 17:59:41 +0000
102+++ bin/nova-dhcpbridge 2011-04-12 11:01:25 +0000
103@@ -48,6 +48,7 @@
104 flags.DECLARE('network_size', 'nova.network.manager')
105 flags.DECLARE('num_networks', 'nova.network.manager')
106 flags.DECLARE('update_dhcp_on_disassociate', 'nova.network.manager')
107+flags.DEFINE_string('dnsmasq_interface', 'br0', 'Default Dnsmasq interface')
108
109 LOG = logging.getLogger('nova.dhcpbridge')
110
111@@ -103,7 +104,8 @@
112 utils.default_flagfile(flagfile)
113 argv = FLAGS(sys.argv)
114 logging.setup()
115- interface = os.environ.get('DNSMASQ_INTERFACE', 'br0')
116+ # check ENV first so we don't break any older deploys
117+ interface = os.environ.get('DNSMASQ_INTERFACE', FLAGS.dnsmasq_interface)
118 if int(os.environ.get('TESTING', '0')):
119 from nova.tests import fake_flags
120 action = argv[1]
121
122=== modified file 'bin/nova-manage'
123--- bin/nova-manage 2011-04-07 14:25:01 +0000
124+++ bin/nova-manage 2011-04-12 11:01:25 +0000
125@@ -528,6 +528,49 @@
126 class VmCommands(object):
127 """Class for mangaging VM instances."""
128
129+ def list(self, host=None):
130+ """Show a list of all instances
131+
132+ :param host: show all instance on specified host.
133+ :param instance: show specificed instance.
134+ """
135+ print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \
136+ " %-10s %-10s %-10s %-5s" % (
137+ _('instance'),
138+ _('node'),
139+ _('type'),
140+ _('state'),
141+ _('launched'),
142+ _('image'),
143+ _('kernel'),
144+ _('ramdisk'),
145+ _('project'),
146+ _('user'),
147+ _('zone'),
148+ _('index'))
149+
150+ if host == None:
151+ instances = db.instance_get_all(context.get_admin_context())
152+ else:
153+ instances = db.instance_get_all_by_host(
154+ context.get_admin_context(), host)
155+
156+ for instance in instances:
157+ print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \
158+ " %-10s %-10s %-10s %-5d" % (
159+ instance['hostname'],
160+ instance['host'],
161+ instance['instance_type'],
162+ instance['state_description'],
163+ instance['launched_at'],
164+ instance['image_id'],
165+ instance['kernel_id'],
166+ instance['ramdisk_id'],
167+ instance['project_id'],
168+ instance['user_id'],
169+ instance['availability_zone'],
170+ instance['launch_index'])
171+
172 def live_migration(self, ec2_id, dest):
173 """Migrates a running instance to a new machine.
174
175@@ -659,15 +702,6 @@
176 {"method": "update_available_resource"})
177
178
179-class LogCommands(object):
180- def request(self, request_id, logfile='/var/log/nova.log'):
181- """Show all fields in the log for the given request. Assumes you
182- haven't changed the log format too much.
183- ARGS: request_id [logfile]"""
184- lines = utils.execute("cat %s | grep '\[%s '" % (logfile, request_id))
185- print re.sub('#012', "\n", "\n".join(lines))
186-
187-
188 class DbCommands(object):
189 """Class for managing the database."""
190
191@@ -683,49 +717,6 @@
192 print migration.db_version()
193
194
195-class InstanceCommands(object):
196- """Class for managing instances."""
197-
198- def list(self, host=None, instance=None):
199- """Show a list of all instances"""
200- print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \
201- " %-10s %-10s %-10s %-5s" % (
202- _('instance'),
203- _('node'),
204- _('type'),
205- _('state'),
206- _('launched'),
207- _('image'),
208- _('kernel'),
209- _('ramdisk'),
210- _('project'),
211- _('user'),
212- _('zone'),
213- _('index'))
214-
215- if host == None:
216- instances = db.instance_get_all(context.get_admin_context())
217- else:
218- instances = db.instance_get_all_by_host(
219- context.get_admin_context(), host)
220-
221- for instance in instances:
222- print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \
223- " %-10s %-10s %-10s %-5d" % (
224- instance['hostname'],
225- instance['host'],
226- instance['instance_type'],
227- instance['state_description'],
228- instance['launched_at'],
229- instance['image_id'],
230- instance['kernel_id'],
231- instance['ramdisk_id'],
232- instance['project_id'],
233- instance['user_id'],
234- instance['availability_zone'],
235- instance['launch_index'])
236-
237-
238 class VolumeCommands(object):
239 """Methods for dealing with a cloud in an odd state"""
240
241@@ -836,7 +827,7 @@
242 elif name == "--all":
243 inst_types = instance_types.get_all_types(True)
244 else:
245- inst_types = instance_types.get_instance_type(name)
246+ inst_types = instance_types.get_instance_type_by_name(name)
247 except exception.DBError, e:
248 _db_error(e)
249 if isinstance(inst_types.values()[0], dict):
250@@ -852,20 +843,17 @@
251 def __init__(self, *args, **kwargs):
252 self.image_service = utils.import_object(FLAGS.image_service)
253
254- def _register(self, image_type, disk_format, container_format,
255+ def _register(self, container_format, disk_format,
256 path, owner, name=None, is_public='T',
257 architecture='x86_64', kernel_id=None, ramdisk_id=None):
258- meta = {'is_public': True,
259+ meta = {'is_public': (is_public == 'T'),
260 'name': name,
261+ 'container_format': container_format,
262 'disk_format': disk_format,
263- 'container_format': container_format,
264 'properties': {'image_state': 'available',
265- 'owner': owner,
266- 'type': image_type,
267+ 'project_id': owner,
268 'architecture': architecture,
269- 'image_location': 'local',
270- 'is_public': (is_public == 'T')}}
271- print image_type, meta
272+ 'image_location': 'local'}}
273 if kernel_id:
274 meta['properties']['kernel_id'] = int(kernel_id)
275 if ramdisk_id:
276@@ -890,16 +878,18 @@
277 ramdisk_id = self.ramdisk_register(ramdisk, owner, None,
278 is_public, architecture)
279 self.image_register(image, owner, name, is_public,
280- architecture, kernel_id, ramdisk_id)
281+ architecture, 'ami', 'ami',
282+ kernel_id, ramdisk_id)
283
284 def image_register(self, path, owner, name=None, is_public='T',
285- architecture='x86_64', kernel_id=None, ramdisk_id=None,
286- disk_format='ami', container_format='ami'):
287+ architecture='x86_64', container_format='bare',
288+ disk_format='raw', kernel_id=None, ramdisk_id=None):
289 """Uploads an image into the image_service
290 arguments: path owner [name] [is_public='T'] [architecture='x86_64']
291+ [container_format='bare'] [disk_format='raw']
292 [kernel_id=None] [ramdisk_id=None]
293- [disk_format='ami'] [container_format='ami']"""
294- return self._register('machine', disk_format, container_format, path,
295+ """
296+ return self._register(container_format, disk_format, path,
297 owner, name, is_public, architecture,
298 kernel_id, ramdisk_id)
299
300@@ -908,7 +898,7 @@
301 """Uploads a kernel into the image_service
302 arguments: path owner [name] [is_public='T'] [architecture='x86_64']
303 """
304- return self._register('kernel', 'aki', 'aki', path, owner, name,
305+ return self._register('aki', 'aki', path, owner, name,
306 is_public, architecture)
307
308 def ramdisk_register(self, path, owner, name=None, is_public='T',
309@@ -916,7 +906,7 @@
310 """Uploads a ramdisk into the image_service
311 arguments: path owner [name] [is_public='T'] [architecture='x86_64']
312 """
313- return self._register('ramdisk', 'ari', 'ari', path, owner, name,
314+ return self._register('ari', 'ari', path, owner, name,
315 is_public, architecture)
316
317 def _lookup(self, old_image_id):
318@@ -933,16 +923,17 @@
319 'ramdisk': 'ari'}
320 container_format = mapping[old['type']]
321 disk_format = container_format
322+ if container_format == 'ami' and not old.get('kernelId'):
323+ container_format = 'bare'
324+ disk_format = 'raw'
325 new = {'disk_format': disk_format,
326 'container_format': container_format,
327- 'is_public': True,
328+ 'is_public': old['isPublic'],
329 'name': old['imageId'],
330 'properties': {'image_state': old['imageState'],
331- 'owner': old['imageOwnerId'],
332+ 'project_id': old['imageOwnerId'],
333 'architecture': old['architecture'],
334- 'type': old['type'],
335- 'image_location': old['imageLocation'],
336- 'is_public': old['isPublic']}}
337+ 'image_location': old['imageLocation']}}
338 if old.get('kernelId'):
339 new['properties']['kernel_id'] = self._lookup(old['kernelId'])
340 if old.get('ramdiskId'):
341@@ -1006,13 +997,11 @@
342 ('floating', FloatingIpCommands),
343 ('vm', VmCommands),
344 ('service', ServiceCommands),
345- ('log', LogCommands),
346 ('db', DbCommands),
347 ('volume', VolumeCommands),
348 ('instance_type', InstanceTypeCommands),
349 ('image', ImageCommands),
350- ('flavor', InstanceTypeCommands),
351- ('instance', InstanceCommands)]
352+ ('flavor', InstanceTypeCommands)]
353
354
355 def lazy_match(name, key_value_tuples):
356@@ -1055,8 +1044,8 @@
357 script_name = argv.pop(0)
358 if len(argv) < 1:
359 print script_name + " category action [<args>]"
360- print "Available categories:"
361- for k, _ in CATEGORIES:
362+ print _("Available categories:")
363+ for k, _v in CATEGORIES:
364 print "\t%s" % k
365 sys.exit(2)
366 category = argv.pop(0)
367@@ -1067,7 +1056,7 @@
368 actions = methods_of(command_object)
369 if len(argv) < 1:
370 print script_name + " category action [<args>]"
371- print "Available actions for %s category:" % category
372+ print _("Available actions for %s category:") % category
373 for k, _v in actions:
374 print "\t%s" % k
375 sys.exit(2)
376@@ -1079,9 +1068,12 @@
377 fn(*argv)
378 sys.exit(0)
379 except TypeError:
380- print "Possible wrong number of arguments supplied"
381+ print _("Possible wrong number of arguments supplied")
382 print "%s %s: %s" % (category, action, fn.__doc__)
383 raise
384+ except Exception:
385+ print _("Command failed, please check log for more info")
386+ raise
387
388 if __name__ == '__main__':
389 main()
390
391=== added file 'bin/nova-vncproxy'
392--- bin/nova-vncproxy 1970-01-01 00:00:00 +0000
393+++ bin/nova-vncproxy 2011-04-12 11:01:25 +0000
394@@ -0,0 +1,101 @@
395+#!/usr/bin/env python
396+# vim: tabstop=4 shiftwidth=4 softtabstop=4
397+
398+# Copyright (c) 2010 Openstack, LLC.
399+# All Rights Reserved.
400+#
401+# Licensed under the Apache License, Version 2.0 (the "License");
402+# you may not use this file except in compliance with the License.
403+# You may obtain a copy of the License at
404+#
405+# http://www.apache.org/licenses/LICENSE-2.0
406+#
407+# Unless required by applicable law or agreed to in writing, software
408+# distributed under the License is distributed on an "AS IS" BASIS,
409+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
410+# See the License for the specific language governing permissions and
411+# limitations under the License.
412+
413+"""VNC Console Proxy Server."""
414+
415+import eventlet
416+import gettext
417+import os
418+import sys
419+
420+possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
421+ os.pardir,
422+ os.pardir))
423+if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
424+ sys.path.insert(0, possible_topdir)
425+
426+gettext.install('nova', unicode=1)
427+
428+from nova import flags
429+from nova import log as logging
430+from nova import service
431+from nova import utils
432+from nova import wsgi
433+from nova import version
434+from nova.vnc import auth
435+from nova.vnc import proxy
436+
437+
438+LOG = logging.getLogger('nova.vnc-proxy')
439+
440+
441+FLAGS = flags.FLAGS
442+flags.DEFINE_string('vncproxy_wwwroot', '/var/lib/nova/noVNC/',
443+ 'Full path to noVNC directory')
444+flags.DEFINE_boolean('vnc_debug', False,
445+ 'Enable debugging features, like token bypassing')
446+flags.DEFINE_integer('vncproxy_port', 6080,
447+ 'Port that the VNC proxy should bind to')
448+flags.DEFINE_string('vncproxy_host', '0.0.0.0',
449+ 'Address that the VNC proxy should bind to')
450+flags.DEFINE_integer('vnc_token_ttl', 300,
451+ 'How many seconds before deleting tokens')
452+flags.DEFINE_string('vncproxy_manager', 'nova.vnc.auth.VNCProxyAuthManager',
453+ 'Manager for vncproxy auth')
454+
455+flags.DEFINE_flag(flags.HelpFlag())
456+flags.DEFINE_flag(flags.HelpshortFlag())
457+flags.DEFINE_flag(flags.HelpXMLFlag())
458+
459+
460+if __name__ == "__main__":
461+ utils.default_flagfile()
462+ FLAGS(sys.argv)
463+ logging.setup()
464+
465+ LOG.audit(_("Starting nova-vnc-proxy node (version %s)"),
466+ version.version_string_with_vcs())
467+
468+ if not (os.path.exists(FLAGS.vncproxy_wwwroot) and
469+ os.path.exists(FLAGS.vncproxy_wwwroot + '/vnc_auto.html')):
470+ LOG.info(_("Missing vncproxy_wwwroot (version %s)"),
471+ FLAGS.vncproxy_wwwroot)
472+ LOG.info(_("You need a slightly modified version of noVNC "
473+ "to work with the nova-vnc-proxy"))
474+ LOG.info(_("Check out the most recent nova noVNC code: %s"),
475+ "git://github.com/sleepsonthefloor/noVNC.git")
476+ LOG.info(_("And drop it in %s"), FLAGS.vncproxy_wwwroot)
477+ exit(1)
478+
479+ app = proxy.WebsocketVNCProxy(FLAGS.vncproxy_wwwroot)
480+
481+ LOG.audit(_("Allowing access to the following files: %s"),
482+ app.get_whitelist())
483+
484+ with_logging = auth.LoggingMiddleware(app)
485+
486+ if FLAGS.vnc_debug:
487+ with_auth = proxy.DebugMiddleware(with_logging)
488+ else:
489+ with_auth = auth.VNCNovaAuthMiddleware(with_logging)
490+
491+ service.serve()
492+
493+ server = wsgi.Server()
494+ server.start(with_auth, FLAGS.vncproxy_port, host=FLAGS.vncproxy_host)
495+ server.wait()
496
497=== added file 'doc/source/devref/zone.rst'
498--- doc/source/devref/zone.rst 1970-01-01 00:00:00 +0000
499+++ doc/source/devref/zone.rst 2011-04-12 11:01:25 +0000
500@@ -0,0 +1,127 @@
501+..
502+ Copyright 2010-2011 OpenStack LLC
503+ All Rights Reserved.
504+
505+ Licensed under the Apache License, Version 2.0 (the "License"); you may
506+ not use this file except in compliance with the License. You may obtain
507+ a copy of the License at
508+
509+ http://www.apache.org/licenses/LICENSE-2.0
510+
511+ Unless required by applicable law or agreed to in writing, software
512+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
513+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
514+ License for the specific language governing permissions and limitations
515+ under the License.
516+
517+Zones
518+=====
519+
520+A Nova deployment is called a Zone. At the very least a Zone requires an API node, a Scheduler node, a database and RabbitMQ. Pushed further a Zone may contain many API nodes, many Scheduler, Volume, Network and Compute nodes as well as a cluster of databases and RabbitMQ servers. A Zone allows you to partition your deployments into logical groups for load balancing and instance distribution.
521+
522+The idea behind Zones is, if a particular deployment is not capable of servicing a particular request, the request may be forwarded to (child) Zones for possible processing. Zones may be nested in a tree fashion.
523+
524+Zones only know about their immediate children, they do not know about their parent Zones and may in fact have more than one parent. Likewise, a Zone's children may themselves have child Zones.
525+
526+Zones share nothing. They communicate via the public OpenStack API only. No database, queue, user or project definition is shared between Zones.
527+
528+
529+Capabilities
530+------------
531+Routing between Zones is based on the Capabilities of that Zone. Capabilities are nothing more than key/value pairs. Values are multi-value, with each value separated with a semicolon (`;`). When expressed as a string they take the form:
532+
533+::
534+
535+ key=value;value;value, key=value;value;value
536+
537+Zones have Capabilities which are general to the Zone and are set via `--zone-capabilities` flag. Zones also have dynamic per-service Capabilities. Services derived from `nova.manager.SchedulerDependentManager` (such as Compute, Volume and Network) can set these capabilities by calling the `update_service_capabilities()` method on their `Manager` base class. These capabilities will be periodically sent to the Scheduler service automatically. The rate at which these updates are sent is controlled by the `--periodic_interval` flag.
538+
539+Flow within a Zone
540+------------------
541+The brunt of the work within a Zone is done in the Scheduler Service. The Scheduler is responsible for:
542+- collecting capability messages from the Compute, Volume and Network nodes,
543+- polling the child Zones for their status and
544+- providing data to the Distributed Scheduler for performing load balancing calculations
545+
546+Inter-service communication within a Zone is done with RabbitMQ. Each class of Service (Compute, Volume and Network) has both a named message exchange (particular to that host) and a general message exchange (particular to that class of service). Messages sent to these exchanges are picked off in round-robin fashion. Zones introduce a new fan-out exchange per service. Messages sent to the fan-out exchange are picked up by all services of a particular class. This fan-out exchange is used by the Scheduler services to receive capability messages from the Compute, Volume and Network nodes.
547+
548+These capability messages are received by the Scheduler services and stored in the `ZoneManager` object. The SchedulerManager object has a reference to the `ZoneManager` it can use for load balancing.
549+
550+The `ZoneManager` also polls the child Zones periodically to gather their capabilities to aid in decision making. This is done via the OpenStack API `/v1.0/zones/info` REST call. This also captures the name of each child Zone. The Zone name is set via the `--zone-name` flag (and defaults to "nova").
551+
552+Zone administrative functions
553+-----------------------------
554+Zone administrative operations are usually done using python-novaclient_
555+
556+.. _python-novaclient: https://github.com/rackspace/python-novaclient
557+
558+In order to use the Zone operations, be sure to enable administrator operations in OpenStack API by setting the `--allow_admin_api=true` flag.
559+
560+Finally you need to enable Zone Forwarding. This will be used by the Distributed Scheduler initiative currently underway. Set `--enable_zone_routing=true` to enable this feature.
561+
562+Find out about this Zone
563+------------------------
564+In any Zone you can find the Zone's name and capabilities with the ``nova zone-info`` command.
565+
566+::
567+
568+ alice@novadev:~$ nova zone-info
569+ +-----------------+---------------+
570+ | Property | Value |
571+ +-----------------+---------------+
572+ | compute_cpu | 0.7,0.7 |
573+ | compute_disk | 123000,123000 |
574+ | compute_network | 800,800 |
575+ | hypervisor | xenserver |
576+ | name | nova |
577+ | network_cpu | 0.7,0.7 |
578+ | network_disk | 123000,123000 |
579+ | network_network | 800,800 |
580+ | os | linux |
581+ +-----------------+---------------+
582+
583+This equates to a GET operation on `.../zones/info`. If you have no child Zones defined you'll usually only get back the default `name`, `hypervisor` and `os` capabilities. Otherwise you'll get back a tuple of min, max values for each capabilities of all the hosts of all the services running in the child zone. These take the `<service>_<capability> = <min>,<max>` format.
584+
585+Adding a child Zone
586+-------------------
587+Any Zone can be a parent Zone. Children are associated to a Zone. The Zone where this command originates from is known as the Parent Zone. Routing is only ever conducted from a Zone to its children, never the other direction. From a parent zone you can add a child zone with the following command:
588+
589+::
590+
591+ nova zone-add <child zone api url> <username> <nova api key>
592+
593+You can get the `child zone api url`, `nova api key` and `username` from the `novarc` file in the child zone. For example:
594+
595+::
596+
597+ export NOVA_API_KEY="3bd1af06-6435-4e23-a827-413b2eb86934"
598+ export NOVA_USERNAME="alice"
599+ export NOVA_URL="http://192.168.2.120:8774/v1.0/"
600+
601+
602+This equates to a POST operation to `.../zones/` to add a new zone. No connection attempt to the child zone is done when this command. It only puts an entry in the db at this point. After about 30 seconds the `ZoneManager` in the Scheduler services will attempt to talk to the child zone and get its information.
603+
604+Getting a list of child Zones
605+-----------------------------
606+
607+::
608+
609+ nova zone-list
610+
611+ alice@novadev:~$ nova zone-list
612+ +----+-------+-----------+--------------------------------------------+---------------------------------+
613+ | ID | Name | Is Active | Capabilities | API URL |
614+ +----+-------+-----------+--------------------------------------------+---------------------------------+
615+ | 2 | zone1 | True | hypervisor=xenserver;kvm, os=linux;windows | http://192.168.2.108:8774/v1.0/ |
616+ | 3 | zone2 | True | hypervisor=xenserver;kvm, os=linux;windows | http://192.168.2.115:8774/v1.0/ |
617+ +----+-------+-----------+--------------------------------------------+---------------------------------+
618+
619+This equates to a GET operation to `.../zones`.
620+
621+Removing a child Zone
622+---------------------
623+::
624+
625+ nova zone-delete <N>
626+
627+This equates to a DELETE call to `.../zones/N`. The Zone with ID=N will be removed. This will only remove the zone entry from the current (parent) Zone, no child Zones are affected. Removing a Child Zone doesn't affect any other part of the hierarchy.
628
629=== modified file 'doc/source/man/novamanage.rst'
630--- doc/source/man/novamanage.rst 2011-03-08 20:28:11 +0000
631+++ doc/source/man/novamanage.rst 2011-04-12 11:01:25 +0000
632@@ -240,6 +240,16 @@
633
634 Converts all images in directory from the old (Bexar) format to the new format.
635
636+Nova VM
637+~~~~~~~~~~~
638+
639+``nova-manage vm list [host]``
640+ Show a list of all instances. Accepts optional hostname (to show only instances on specific host).
641+
642+``nova-manage live-migration <ec2_id> <destination host name>``
643+ Live migrate instance from current host to destination host. Requires instance id (which comes from euca-describe-instance) and destination host name (which can be found from nova-manage service list).
644+
645+
646 FILES
647 ========
648
649
650=== added file 'doc/source/runnova/vncconsole.rst'
651--- doc/source/runnova/vncconsole.rst 1970-01-01 00:00:00 +0000
652+++ doc/source/runnova/vncconsole.rst 2011-04-12 11:01:25 +0000
653@@ -0,0 +1,76 @@
654+..
655+ Copyright 2010-2011 United States Government as represented by the
656+ Administrator of the National Aeronautics and Space Administration.
657+ All Rights Reserved.
658+
659+ Licensed under the Apache License, Version 2.0 (the "License"); you may
660+ not use this file except in compliance with the License. You may obtain
661+ a copy of the License at
662+
663+ http://www.apache.org/licenses/LICENSE-2.0
664+
665+ Unless required by applicable law or agreed to in writing, software
666+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
667+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
668+ License for the specific language governing permissions and limitations
669+ under the License.
670+
671+Getting Started with the VNC Proxy
672+==================================
673+
674+The VNC Proxy is an OpenStack component that allows users of Nova to access
675+their instances through a websocket enabled browser (like Google Chrome).
676+
677+A VNC Connection works like so:
678+
679+* User connects over an api and gets a url like http://ip:port/?token=xyz
680+* User pastes url in browser
681+* Browser connects to VNC Proxy though a websocket enabled client like noVNC
682+* VNC Proxy authorizes users token, maps the token to a host and port of an
683+ instance's VNC server
684+* VNC Proxy initiates connection to VNC server, and continues proxying until
685+ the session ends
686+
687+
688+Configuring the VNC Proxy
689+-------------------------
690+nova-vncproxy requires a websocket enabled html client to work properly. At
691+this time, the only tested client is a slightly modified fork of noVNC, which
692+you can at find http://github.com/openstack/noVNC.git
693+
694+.. todo:: add instruction for installing from package
695+
696+noVNC must be in the location specified by --vncproxy_wwwroot, which defaults
697+to /var/lib/nova/noVNC. nova-vncproxy will fail to launch until this code
698+is properly installed.
699+
700+By default, nova-vncproxy binds 0.0.0.0:6080. This can be configured with:
701+
702+* --vncproxy_port=[port]
703+* --vncproxy_host=[host]
704+
705+
706+Enabling VNC Consoles in Nova
707+-----------------------------
708+At the moment, VNC support is supported only when using libvirt. To enable VNC
709+Console, configure the following flags:
710+
711+* --vnc_console_proxy_url=http://[proxy_host]:[proxy_port] - proxy_port
712+ defaults to 6080. This url must point to nova-vncproxy
713+* --vnc_enabled=[True|False] - defaults to True. If this flag is not set your
714+ instances will launch without vnc support.
715+
716+
717+Getting an instance's VNC Console
718+---------------------------------
719+You can access an instance's VNC Console url in the following methods:
720+
721+* Using the direct api:
722+ eg: 'stack --user=admin --project=admin compute get_vnc_console instance_id=1'
723+* Support for Dashboard, and the Openstack API will be forthcoming
724+
725+
726+Accessing VNC Consoles without a web browser
727+--------------------------------------------
728+At the moment, VNC Consoles are only supported through the web browser, but
729+more general VNC support is in the works.
730
731=== added directory 'etc/nova'
732=== renamed file 'etc/api-paste.ini' => 'etc/nova/api-paste.ini'
733=== renamed directory 'CA' => 'nova/CA'
734=== modified file 'nova/CA/geninter.sh'
735--- CA/geninter.sh 2010-11-06 00:02:36 +0000
736+++ nova/CA/geninter.sh 2011-04-12 11:01:25 +0000
737@@ -23,7 +23,7 @@
738 cd projects/$NAME
739 cp ../../openssl.cnf.tmpl openssl.cnf
740 sed -i -e s/%USERNAME%/$NAME/g openssl.cnf
741-mkdir certs crl newcerts private
742+mkdir -p certs crl newcerts private
743 openssl req -new -x509 -extensions v3_ca -keyout private/cakey.pem -out cacert.pem -days 365 -config ./openssl.cnf -batch -nodes
744 echo "10" > serial
745 touch index.txt
746
747=== modified file 'nova/CA/genrootca.sh'
748--- CA/genrootca.sh 2010-11-06 00:02:36 +0000
749+++ nova/CA/genrootca.sh 2011-04-12 11:01:25 +0000
750@@ -20,8 +20,9 @@
751 then
752 echo "Not installing, it's already done."
753 else
754- cp openssl.cnf.tmpl openssl.cnf
755+ cp "$(dirname $0)/openssl.cnf.tmpl" openssl.cnf
756 sed -i -e s/%USERNAME%/ROOT/g openssl.cnf
757+ mkdir -p certs crl newcerts private
758 openssl req -new -x509 -extensions v3_ca -keyout private/cakey.pem -out cacert.pem -days 365 -config ./openssl.cnf -batch -nodes
759 touch index.txt
760 echo "10" > serial
761
762=== modified file 'nova/CA/openssl.cnf.tmpl'
763--- CA/openssl.cnf.tmpl 2011-03-16 18:22:29 +0000
764+++ nova/CA/openssl.cnf.tmpl 2011-04-12 11:01:25 +0000
765@@ -41,9 +41,13 @@
766 certopt = default_ca
767 policy = policy_match
768
769+# NOTE(dprince): stateOrProvinceName must be 'supplied' or 'optional' to
770+# work around a stateOrProvince printable string UTF8 mismatch on
771+# RHEL 6 and Fedora 14 (using openssl-1.0.0-4.el6.x86_64 or
772+# openssl-1.0.0d-1.fc14.x86_64)
773 [ policy_match ]
774 countryName = match
775-stateOrProvinceName = match
776+stateOrProvinceName = supplied
777 organizationName = optional
778 organizationalUnitName = optional
779 commonName = supplied
780
781=== removed file 'nova/adminclient.py'
782--- nova/adminclient.py 2011-02-24 19:36:15 +0000
783+++ nova/adminclient.py 1970-01-01 00:00:00 +0000
784@@ -1,476 +0,0 @@
785-# vim: tabstop=4 shiftwidth=4 softtabstop=4
786-
787-# Copyright 2010 United States Government as represented by the
788-# Administrator of the National Aeronautics and Space Administration.
789-# All Rights Reserved.
790-#
791-# Licensed under the Apache License, Version 2.0 (the "License"); you may
792-# not use this file except in compliance with the License. You may obtain
793-# a copy of the License at
794-#
795-# http://www.apache.org/licenses/LICENSE-2.0
796-#
797-# Unless required by applicable law or agreed to in writing, software
798-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
799-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
800-# License for the specific language governing permissions and limitations
801-# under the License.
802-"""
803-Nova User API client library.
804-"""
805-
806-import base64
807-import boto
808-import boto.exception
809-import httplib
810-import re
811-import string
812-
813-from boto.ec2.regioninfo import RegionInfo
814-
815-
816-DEFAULT_CLC_URL = 'http://127.0.0.1:8773'
817-DEFAULT_REGION = 'nova'
818-
819-
820-class UserInfo(object):
821- """
822- Information about a Nova user, as parsed through SAX.
823-
824- **Fields Include**
825-
826- * username
827- * accesskey
828- * secretkey
829- * file (optional) containing zip of X509 cert & rc file
830-
831- """
832-
833- def __init__(self, connection=None, username=None, endpoint=None):
834- self.connection = connection
835- self.username = username
836- self.endpoint = endpoint
837-
838- def __repr__(self):
839- return 'UserInfo:%s' % self.username
840-
841- def startElement(self, name, attrs, connection):
842- return None
843-
844- def endElement(self, name, value, connection):
845- if name == 'username':
846- self.username = str(value)
847- elif name == 'file':
848- self.file = base64.b64decode(str(value))
849- elif name == 'accesskey':
850- self.accesskey = str(value)
851- elif name == 'secretkey':
852- self.secretkey = str(value)
853-
854-
855-class UserRole(object):
856- """
857- Information about a Nova user's role, as parsed through SAX.
858-
859- **Fields include**
860-
861- * role
862-
863- """
864-
865- def __init__(self, connection=None):
866- self.connection = connection
867- self.role = None
868-
869- def __repr__(self):
870- return 'UserRole:%s' % self.role
871-
872- def startElement(self, name, attrs, connection):
873- return None
874-
875- def endElement(self, name, value, connection):
876- if name == 'role':
877- self.role = value
878- else:
879- setattr(self, name, str(value))
880-
881-
882-class ProjectInfo(object):
883- """
884- Information about a Nova project, as parsed through SAX.
885-
886- **Fields include**
887-
888- * projectname
889- * description
890- * projectManagerId
891- * memberIds
892-
893- """
894-
895- def __init__(self, connection=None):
896- self.connection = connection
897- self.projectname = None
898- self.description = None
899- self.projectManagerId = None
900- self.memberIds = []
901-
902- def __repr__(self):
903- return 'ProjectInfo:%s' % self.projectname
904-
905- def startElement(self, name, attrs, connection):
906- return None
907-
908- def endElement(self, name, value, connection):
909- if name == 'projectname':
910- self.projectname = value
911- elif name == 'description':
912- self.description = value
913- elif name == 'projectManagerId':
914- self.projectManagerId = value
915- elif name == 'memberId':
916- self.memberIds.append(value)
917- else:
918- setattr(self, name, str(value))
919-
920-
921-class ProjectMember(object):
922- """
923- Information about a Nova project member, as parsed through SAX.
924-
925- **Fields include**
926-
927- * memberId
928-
929- """
930-
931- def __init__(self, connection=None):
932- self.connection = connection
933- self.memberId = None
934-
935- def __repr__(self):
936- return 'ProjectMember:%s' % self.memberId
937-
938- def startElement(self, name, attrs, connection):
939- return None
940-
941- def endElement(self, name, value, connection):
942- if name == 'member':
943- self.memberId = value
944- else:
945- setattr(self, name, str(value))
946-
947-
948-class HostInfo(object):
949- """
950- Information about a Nova Host, as parsed through SAX.
951-
952- **Fields Include**
953-
954- * Hostname
955- * Compute service status
956- * Volume service status
957- * Instance count
958- * Volume count
959- """
960-
961- def __init__(self, connection=None):
962- self.connection = connection
963- self.hostname = None
964- self.compute = None
965- self.volume = None
966- self.instance_count = 0
967- self.volume_count = 0
968-
969- def __repr__(self):
970- return 'Host:%s' % self.hostname
971-
972- # this is needed by the sax parser, so ignore the ugly name
973- def startElement(self, name, attrs, connection):
974- return None
975-
976- # this is needed by the sax parser, so ignore the ugly name
977- def endElement(self, name, value, connection):
978- fixed_name = string.lower(re.sub(r'([A-Z])', r'_\1', name))
979- setattr(self, fixed_name, value)
980-
981-
982-class Vpn(object):
983- """
984- Information about a Vpn, as parsed through SAX
985-
986- **Fields Include**
987-
988- * instance_id
989- * project_id
990- * public_ip
991- * public_port
992- * created_at
993- * internal_ip
994- * state
995- """
996-
997- def __init__(self, connection=None):
998- self.connection = connection
999- self.instance_id = None
1000- self.project_id = None
1001-
1002- def __repr__(self):
1003- return 'Vpn:%s:%s' % (self.project_id, self.instance_id)
1004-
1005- def startElement(self, name, attrs, connection):
1006- return None
1007-
1008- def endElement(self, name, value, connection):
1009- fixed_name = string.lower(re.sub(r'([A-Z])', r'_\1', name))
1010- setattr(self, fixed_name, value)
1011-
1012-
1013-class InstanceType(object):
1014- """
1015- Information about a Nova instance type, as parsed through SAX.
1016-
1017- **Fields include**
1018-
1019- * name
1020- * vcpus
1021- * disk_gb
1022- * memory_mb
1023- * flavor_id
1024-
1025- """
1026-
1027- def __init__(self, connection=None):
1028- self.connection = connection
1029- self.name = None
1030- self.vcpus = None
1031- self.disk_gb = None
1032- self.memory_mb = None
1033- self.flavor_id = None
1034-
1035- def __repr__(self):
1036- return 'InstanceType:%s' % self.name
1037-
1038- def startElement(self, name, attrs, connection):
1039- return None
1040-
1041- def endElement(self, name, value, connection):
1042- if name == "memoryMb":
1043- self.memory_mb = str(value)
1044- elif name == "flavorId":
1045- self.flavor_id = str(value)
1046- elif name == "diskGb":
1047- self.disk_gb = str(value)
1048- else:
1049- setattr(self, name, str(value))
1050-
1051-
1052-class NovaAdminClient(object):
1053-
1054- def __init__(
1055- self,
1056- clc_url=DEFAULT_CLC_URL,
1057- region=DEFAULT_REGION,
1058- access_key=None,
1059- secret_key=None,
1060- **kwargs):
1061- parts = self.split_clc_url(clc_url)
1062-
1063- self.clc_url = clc_url
1064- self.region = region
1065- self.access = access_key
1066- self.secret = secret_key
1067- self.apiconn = boto.connect_ec2(aws_access_key_id=access_key,
1068- aws_secret_access_key=secret_key,
1069- is_secure=parts['is_secure'],
1070- region=RegionInfo(None,
1071- region,
1072- parts['ip']),
1073- port=parts['port'],
1074- path='/services/Admin',
1075- **kwargs)
1076- self.apiconn.APIVersion = 'nova'
1077-
1078- def connection_for(self, username, project, clc_url=None, region=None,
1079- **kwargs):
1080- """Returns a boto ec2 connection for the given username."""
1081- if not clc_url:
1082- clc_url = self.clc_url
1083- if not region:
1084- region = self.region
1085- parts = self.split_clc_url(clc_url)
1086- user = self.get_user(username)
1087- access_key = '%s:%s' % (user.accesskey, project)
1088- return boto.connect_ec2(aws_access_key_id=access_key,
1089- aws_secret_access_key=user.secretkey,
1090- is_secure=parts['is_secure'],
1091- region=RegionInfo(None,
1092- self.region,
1093- parts['ip']),
1094- port=parts['port'],
1095- path='/services/Cloud',
1096- **kwargs)
1097-
1098- def split_clc_url(self, clc_url):
1099- """Splits a cloud controller endpoint url."""
1100- parts = httplib.urlsplit(clc_url)
1101- is_secure = parts.scheme == 'https'
1102- ip, port = parts.netloc.split(':')
1103- return {'ip': ip, 'port': int(port), 'is_secure': is_secure}
1104-
1105- def get_users(self):
1106- """Grabs the list of all users."""
1107- return self.apiconn.get_list('DescribeUsers', {}, [('item', UserInfo)])
1108-
1109- def get_user(self, name):
1110- """Grab a single user by name."""
1111- try:
1112- return self.apiconn.get_object('DescribeUser',
1113- {'Name': name},
1114- UserInfo)
1115- except boto.exception.BotoServerError, e:
1116- if e.status == 400 and e.error_code == 'NotFound':
1117- return None
1118- raise
1119-
1120- def has_user(self, username):
1121- """Determine if user exists."""
1122- return self.get_user(username) != None
1123-
1124- def create_user(self, username):
1125- """Creates a new user, returning the userinfo object with
1126- access/secret."""
1127- return self.apiconn.get_object('RegisterUser', {'Name': username},
1128- UserInfo)
1129-
1130- def delete_user(self, username):
1131- """Deletes a user."""
1132- return self.apiconn.get_object('DeregisterUser', {'Name': username},
1133- UserInfo)
1134-
1135- def get_roles(self, project_roles=True):
1136- """Returns a list of available roles."""
1137- return self.apiconn.get_list('DescribeRoles',
1138- {'ProjectRoles': project_roles},
1139- [('item', UserRole)])
1140-
1141- def get_user_roles(self, user, project=None):
1142- """Returns a list of roles for the given user.
1143-
1144- Omitting project will return any global roles that the user has.
1145- Specifying project will return only project specific roles.
1146-
1147- """
1148- params = {'User': user}
1149- if project:
1150- params['Project'] = project
1151- return self.apiconn.get_list('DescribeUserRoles',
1152- params,
1153- [('item', UserRole)])
1154-
1155- def add_user_role(self, user, role, project=None):
1156- """Add a role to a user either globally or for a specific project."""
1157- return self.modify_user_role(user, role, project=project,
1158- operation='add')
1159-
1160- def remove_user_role(self, user, role, project=None):
1161- """Remove a role from a user either globally or for a specific
1162- project."""
1163- return self.modify_user_role(user, role, project=project,
1164- operation='remove')
1165-
1166- def modify_user_role(self, user, role, project=None, operation='add',
1167- **kwargs):
1168- """Add or remove a role for a user and project."""
1169- params = {'User': user,
1170- 'Role': role,
1171- 'Project': project,
1172- 'Operation': operation}
1173- return self.apiconn.get_status('ModifyUserRole', params)
1174-
1175- def get_projects(self, user=None):
1176- """Returns a list of all projects."""
1177- if user:
1178- params = {'User': user}
1179- else:
1180- params = {}
1181- return self.apiconn.get_list('DescribeProjects',
1182- params,
1183- [('item', ProjectInfo)])
1184-
1185- def get_project(self, name):
1186- """Returns a single project with the specified name."""
1187- project = self.apiconn.get_object('DescribeProject',
1188- {'Name': name},
1189- ProjectInfo)
1190-
1191- if project.projectname != None:
1192- return project
1193-
1194- def create_project(self, projectname, manager_user, description=None,
1195- member_users=None):
1196- """Creates a new project."""
1197- params = {'Name': projectname,
1198- 'ManagerUser': manager_user,
1199- 'Description': description,
1200- 'MemberUsers': member_users}
1201- return self.apiconn.get_object('RegisterProject', params, ProjectInfo)
1202-
1203- def modify_project(self, projectname, manager_user=None, description=None):
1204- """Modifies an existing project."""
1205- params = {'Name': projectname,
1206- 'ManagerUser': manager_user,
1207- 'Description': description}
1208- return self.apiconn.get_status('ModifyProject', params)
1209-
1210- def delete_project(self, projectname):
1211- """Permanently deletes the specified project."""
1212- return self.apiconn.get_object('DeregisterProject',
1213- {'Name': projectname},
1214- ProjectInfo)
1215-
1216- def get_project_members(self, name):
1217- """Returns a list of members of a project."""
1218- return self.apiconn.get_list('DescribeProjectMembers',
1219- {'Name': name},
1220- [('item', ProjectMember)])
1221-
1222- def add_project_member(self, user, project):
1223- """Adds a user to a project."""
1224- return self.modify_project_member(user, project, operation='add')
1225-
1226- def remove_project_member(self, user, project):
1227- """Removes a user from a project."""
1228- return self.modify_project_member(user, project, operation='remove')
1229-
1230- def modify_project_member(self, user, project, operation='add'):
1231- """Adds or removes a user from a project."""
1232- params = {'User': user,
1233- 'Project': project,
1234- 'Operation': operation}
1235- return self.apiconn.get_status('ModifyProjectMember', params)
1236-
1237- def get_zip(self, user, project):
1238- """Returns the content of a zip file containing novarc and access
1239- credentials."""
1240- params = {'Name': user, 'Project': project}
1241- zip = self.apiconn.get_object('GenerateX509ForUser', params, UserInfo)
1242- return zip.file
1243-
1244- def start_vpn(self, project):
1245- """
1246- Starts the vpn for a user
1247- """
1248- return self.apiconn.get_object('StartVpn', {'Project': project}, Vpn)
1249-
1250- def get_vpns(self):
1251- """Return a list of vpn with project name"""
1252- return self.apiconn.get_list('DescribeVpns', {}, [('item', Vpn)])
1253-
1254- def get_hosts(self):
1255- return self.apiconn.get_list('DescribeHosts', {}, [('item', HostInfo)])
1256-
1257- def get_instance_types(self):
1258- """Grabs the list of all users."""
1259- return self.apiconn.get_list('DescribeInstanceTypes', {},
1260- [('item', InstanceType)])
1261
1262=== modified file 'nova/api/direct.py'
1263--- nova/api/direct.py 2011-03-24 20:20:15 +0000
1264+++ nova/api/direct.py 2011-04-12 11:01:25 +0000
1265@@ -206,10 +206,14 @@
1266 # NOTE(vish): make sure we have no unicode keys for py2.6.
1267 params = dict([(str(k), v) for (k, v) in params.iteritems()])
1268 result = method(context, **params)
1269+
1270 if result is None or type(result) is str or type(result) is unicode:
1271 return result
1272+
1273 try:
1274- return self._serialize(result, req.best_match_content_type())
1275+ content_type = req.best_match_content_type()
1276+ default_xmlns = self.get_default_xmlns(req)
1277+ return self._serialize(result, content_type, default_xmlns)
1278 except:
1279 raise exception.Error("returned non-serializable type: %s"
1280 % result)
1281
1282=== modified file 'nova/api/ec2/cloud.py'
1283--- nova/api/ec2/cloud.py 2011-04-07 08:40:22 +0000
1284+++ nova/api/ec2/cloud.py 2011-04-12 11:01:25 +0000
1285@@ -103,10 +103,18 @@
1286 # Gen root CA, if we don't have one
1287 root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file)
1288 if not os.path.exists(root_ca_path):
1289+ genrootca_sh_path = os.path.join(os.path.dirname(__file__),
1290+ os.path.pardir,
1291+ os.path.pardir,
1292+ 'CA',
1293+ 'genrootca.sh')
1294+
1295 start = os.getcwd()
1296+ if not os.path.exists(FLAGS.ca_path):
1297+ os.makedirs(FLAGS.ca_path)
1298 os.chdir(FLAGS.ca_path)
1299 # TODO(vish): Do this with M2Crypto instead
1300- utils.runthis(_("Generating root CA: %s"), "sh", "genrootca.sh")
1301+ utils.runthis(_("Generating root CA: %s"), "sh", genrootca_sh_path)
1302 os.chdir(start)
1303
1304 def _get_mpi_data(self, context, project_id):
1305@@ -134,6 +142,11 @@
1306 instance_ref = self.compute_api.get_all(ctxt, fixed_ip=address)
1307 if instance_ref is None:
1308 return None
1309+
1310+ # This ensures that all attributes of the instance
1311+ # are populated.
1312+ instance_ref = db.instance_get(ctxt, instance_ref['id'])
1313+
1314 mpi = self._get_mpi_data(ctxt, instance_ref['project_id'])
1315 if instance_ref['key_name']:
1316 keys = {'0': {'_name': instance_ref['key_name'],
1317@@ -156,7 +169,7 @@
1318 floating_ip = net['IPs'][0]['floating_ips'][0]
1319
1320 ec2_id = ec2utils.id_to_ec2_id(instance_ref['id'])
1321- image_ec2_id = self._image_ec2_id(instance_ref['image_id'], 'machine')
1322+ image_ec2_id = self._image_ec2_id(instance_ref['image_id'], 'ami')
1323 data = {
1324 'user-data': base64.b64decode(instance_ref['user_data']),
1325 'meta-data': {
1326@@ -186,7 +199,7 @@
1327 for image_type in ['kernel', 'ramdisk']:
1328 if '%s_id' % image_type in instance_ref:
1329 ec2_id = self._image_ec2_id(instance_ref['%s_id' % image_type],
1330- image_type)
1331+ self._image_type(image_type))
1332 data['meta-data']['%s-id' % image_type] = ec2_id
1333
1334 if False: # TODO(vish): store ancestor ids
1335@@ -546,6 +559,13 @@
1336 return self.compute_api.get_ajax_console(context,
1337 instance_id=instance_id)
1338
1339+ def get_vnc_console(self, context, instance_id, **kwargs):
1340+ """Returns vnc browser url. Used by OS dashboard."""
1341+ ec2_id = instance_id
1342+ instance_id = ec2utils.ec2_id_to_id(ec2_id)
1343+ return self.compute_api.get_vnc_console(context,
1344+ instance_id=instance_id)
1345+
1346 def describe_volumes(self, context, volume_id=None, **kwargs):
1347 if volume_id:
1348 volumes = []
1349@@ -734,7 +754,10 @@
1350 instance['project_id'],
1351 instance['host'])
1352 i['productCodesSet'] = self._convert_to_set([], 'product_codes')
1353- i['instanceType'] = instance['instance_type']
1354+ if instance['instance_type']:
1355+ i['instanceType'] = instance['instance_type'].get('name')
1356+ else:
1357+ i['instanceType'] = None
1358 i['launchTime'] = instance['created_at']
1359 i['amiLaunchIndex'] = instance['launch_index']
1360 i['displayName'] = instance['display_name']
1361@@ -763,6 +786,7 @@
1362
1363 def format_addresses(self, context):
1364 addresses = []
1365+
1366 net_factory = net_service.get_service_factory(context,
1367 context.project_id)
1368 net_api = net_factory.get_api_service()
1369@@ -770,6 +794,9 @@
1370 ip_info_list = net_api.get_addresses(context, project_id)
1371
1372 for ip_info in ip_info_list:
1373+ if ip_info['project_id'] is None:
1374+ continue
1375+
1376 ec2_id = None
1377 if ip_info['vnic_id']:
1378 instance = db.instance_get_by_virtual_nic(context,
1379@@ -788,6 +815,7 @@
1380
1381 def allocate_address(self, context, **kwargs):
1382 LOG.audit(_("Allocate address"), context=context)
1383+
1384 net_factory = net_service.get_service_factory(context,
1385 context.project_id)
1386 net_api_service = net_factory.get_api_service()
1387@@ -802,7 +830,7 @@
1388 context.project_id)
1389 raise ex
1390
1391- return {'addressSet': [{'publicIp': public_ip}]}
1392+ return {'publicIp': public_ip}
1393
1394 def release_address(self, context, public_ip, **kwargs):
1395 LOG.audit(_("Release address %s"), public_ip, context=context)
1396@@ -841,7 +869,7 @@
1397 ramdisk = self._get_image(context, kwargs['ramdisk_id'])
1398 kwargs['ramdisk_id'] = ramdisk['id']
1399 instances = self.compute_api.create(context,
1400- instance_type=instance_types.get_by_type(
1401+ instance_type=instance_types.get_instance_type_by_name(
1402 kwargs.get('instance_type', None)),
1403 image_id=self._get_image(context, kwargs['image_id'])['id'],
1404 min_count=int(kwargs.get('min_count', max_count)),
1405@@ -898,13 +926,27 @@
1406 self.compute_api.update(context, instance_id=instance_id, **kwargs)
1407 return True
1408
1409- _type_prefix_map = {'machine': 'ami',
1410- 'kernel': 'aki',
1411- 'ramdisk': 'ari'}
1412-
1413- def _image_ec2_id(self, image_id, image_type='machine'):
1414- prefix = self._type_prefix_map[image_type]
1415- template = prefix + '-%08x'
1416+ @staticmethod
1417+ def _image_type(image_type):
1418+ """Converts to a three letter image type.
1419+
1420+ aki, kernel => aki
1421+ ari, ramdisk => ari
1422+ anything else => ami
1423+
1424+ """
1425+ if image_type == 'kernel':
1426+ return 'aki'
1427+ if image_type == 'ramdisk':
1428+ return 'ari'
1429+ if image_type not in ['aki', 'ari']:
1430+ return 'ami'
1431+ return image_type
1432+
1433+ @staticmethod
1434+ def _image_ec2_id(image_id, image_type='ami'):
1435+ """Returns image ec2_id using id and three letter type."""
1436+ template = image_type + '-%08x'
1437 return ec2utils.id_to_ec2_id(int(image_id), template=template)
1438
1439 def _get_image(self, context, ec2_id):
1440@@ -917,24 +959,34 @@
1441 def _format_image(self, image):
1442 """Convert from format defined by BaseImageService to S3 format."""
1443 i = {}
1444- image_type = image['properties'].get('type')
1445+ image_type = self._image_type(image.get('container_format'))
1446 ec2_id = self._image_ec2_id(image.get('id'), image_type)
1447 name = image.get('name')
1448- if name:
1449- i['imageId'] = "%s (%s)" % (ec2_id, name)
1450- else:
1451- i['imageId'] = ec2_id
1452+ i['imageId'] = ec2_id
1453 kernel_id = image['properties'].get('kernel_id')
1454 if kernel_id:
1455- i['kernelId'] = self._image_ec2_id(kernel_id, 'kernel')
1456+ i['kernelId'] = self._image_ec2_id(kernel_id, 'aki')
1457 ramdisk_id = image['properties'].get('ramdisk_id')
1458 if ramdisk_id:
1459- i['ramdiskId'] = self._image_ec2_id(ramdisk_id, 'ramdisk')
1460+ i['ramdiskId'] = self._image_ec2_id(ramdisk_id, 'ari')
1461 i['imageOwnerId'] = image['properties'].get('owner_id')
1462- i['imageLocation'] = image['properties'].get('image_location')
1463- i['imageState'] = image['properties'].get('image_state')
1464- i['type'] = image_type
1465- i['isPublic'] = str(image['properties'].get('is_public', '')) == 'True'
1466+ if name:
1467+ i['imageLocation'] = "%s (%s)" % (image['properties'].
1468+ get('image_location'), name)
1469+ else:
1470+ i['imageLocation'] = image['properties'].get('image_location')
1471+ # NOTE(vish): fallback status if image_state isn't set
1472+ state = image.get('status')
1473+ if state == 'active':
1474+ state = 'available'
1475+ i['imageState'] = image['properties'].get('image_state', state)
1476+ i['displayName'] = name
1477+ i['description'] = image.get('description')
1478+ display_mapping = {'aki': 'kernel',
1479+ 'ari': 'ramdisk',
1480+ 'ami': 'machine'}
1481+ i['imageType'] = display_mapping.get(image_type)
1482+ i['isPublic'] = image.get('is_public') == True
1483 i['architecture'] = image['properties'].get('architecture')
1484 return i
1485
1486@@ -966,8 +1018,9 @@
1487 image_location = kwargs['name']
1488 metadata = {'properties': {'image_location': image_location}}
1489 image = self.image_service.create(context, metadata)
1490+ image_type = self._image_type(image.get('container_format'))
1491 image_id = self._image_ec2_id(image['id'],
1492- image['properties']['type'])
1493+ image_type)
1494 msg = _("Registered image %(image_location)s with"
1495 " id %(image_id)s") % locals()
1496 LOG.audit(msg, context=context)
1497@@ -982,7 +1035,7 @@
1498 except exception.NotFound:
1499 raise exception.NotFound(_('Image %s not found') % image_id)
1500 result = {'imageId': image_id, 'launchPermission': []}
1501- if image['properties']['is_public']:
1502+ if image['is_public']:
1503 result['launchPermission'].append({'group': 'all'})
1504 return result
1505
1506@@ -1007,7 +1060,7 @@
1507 internal_id = image['id']
1508 del(image['id'])
1509
1510- image['properties']['is_public'] = (operation_type == 'add')
1511+ image['is_public'] = (operation_type == 'add')
1512 return self.image_service.update(context, internal_id, image)
1513
1514 def update_image(self, context, image_id, **kwargs):
1515
1516=== modified file 'nova/api/openstack/__init__.py'
1517--- nova/api/openstack/__init__.py 2011-04-07 01:08:35 +0000
1518+++ nova/api/openstack/__init__.py 2011-04-12 11:01:25 +0000
1519@@ -36,6 +36,7 @@
1520 from nova.api.openstack import flavors
1521 from nova.api.openstack import images
1522 from nova.api.openstack import image_metadata
1523+from nova.api.openstack import ips
1524 from nova.api.openstack import limits
1525 from nova.api.openstack import servers
1526 from nova.api.openstack import server_metadata
1527@@ -108,23 +109,11 @@
1528 controller=accounts.Controller(),
1529 collection={'detail': 'GET'})
1530
1531- mapper.resource("backup_schedule", "backup_schedule",
1532- controller=backup_schedules.Controller(),
1533- parent_resource=dict(member_name='server',
1534- collection_name='servers'))
1535-
1536 mapper.resource("console", "consoles",
1537 controller=consoles.Controller(),
1538 parent_resource=dict(member_name='server',
1539 collection_name='servers'))
1540
1541- mapper.resource("image", "images", controller=images.Controller(),
1542- collection={'detail': 'GET'})
1543-
1544- mapper.resource("shared_ip_group", "shared_ip_groups",
1545- collection={'detail': 'GET'},
1546- controller=shared_ip_groups.Controller())
1547-
1548 _limits = limits.LimitsController()
1549 mapper.resource("limit", "limits", controller=_limits)
1550
1551@@ -144,10 +133,28 @@
1552 collection={'detail': 'GET'},
1553 member=self.server_members)
1554
1555+ mapper.resource("image", "images",
1556+ controller=images.ControllerV10(),
1557+ collection={'detail': 'GET'})
1558+
1559 mapper.resource("flavor", "flavors",
1560 controller=flavors.ControllerV10(),
1561 collection={'detail': 'GET'})
1562
1563+ mapper.resource("shared_ip_group", "shared_ip_groups",
1564+ collection={'detail': 'GET'},
1565+ controller=shared_ip_groups.Controller())
1566+
1567+ mapper.resource("backup_schedule", "backup_schedule",
1568+ controller=backup_schedules.Controller(),
1569+ parent_resource=dict(member_name='server',
1570+ collection_name='servers'))
1571+
1572+ mapper.resource("ip", "ips", controller=ips.Controller(),
1573+ collection=dict(public='GET', private='GET'),
1574+ parent_resource=dict(member_name='server',
1575+ collection_name='servers'))
1576+
1577
1578 class APIRouterV11(APIRouter):
1579 """Define routes specific to OpenStack API V1.1."""
1580@@ -159,6 +166,10 @@
1581 collection={'detail': 'GET'},
1582 member=self.server_members)
1583
1584+ mapper.resource("image", "images",
1585+ controller=images.ControllerV11(),
1586+ collection={'detail': 'GET'})
1587+
1588 mapper.resource("image_meta", "meta",
1589 controller=image_metadata.Controller(),
1590 parent_resource=dict(member_name='image',
1591
1592=== modified file 'nova/api/openstack/accounts.py'
1593--- nova/api/openstack/accounts.py 2011-03-18 14:34:08 +0000
1594+++ nova/api/openstack/accounts.py 2011-04-12 11:01:25 +0000
1595@@ -13,15 +13,14 @@
1596 # License for the specific language governing permissions and limitations
1597 # under the License.
1598
1599-import common
1600 import webob.exc
1601
1602 from nova import exception
1603 from nova import flags
1604 from nova import log as logging
1605-from nova import wsgi
1606
1607 from nova.auth import manager
1608+from nova.api.openstack import common
1609 from nova.api.openstack import faults
1610
1611 FLAGS = flags.FLAGS
1612@@ -35,7 +34,7 @@
1613 manager=account.project_manager_id)
1614
1615
1616-class Controller(wsgi.Controller):
1617+class Controller(common.OpenstackController):
1618
1619 _serialization_metadata = {
1620 'application/xml': {
1621
1622=== modified file 'nova/api/openstack/backup_schedules.py'
1623--- nova/api/openstack/backup_schedules.py 2011-03-09 18:10:45 +0000
1624+++ nova/api/openstack/backup_schedules.py 2011-04-12 11:01:25 +0000
1625@@ -19,7 +19,7 @@
1626
1627 from webob import exc
1628
1629-from nova import wsgi
1630+from nova.api.openstack import common
1631 from nova.api.openstack import faults
1632 import nova.image.service
1633
1634@@ -29,7 +29,7 @@
1635 return dict(backupSchedule=inst)
1636
1637
1638-class Controller(wsgi.Controller):
1639+class Controller(common.OpenstackController):
1640 """ The backup schedule API controller for the Openstack API """
1641
1642 _serialization_metadata = {
1643@@ -42,7 +42,11 @@
1644
1645 def index(self, req, server_id):
1646 """ Returns the list of backup schedules for a given instance """
1647- return _translate_keys({})
1648+ return faults.Fault(exc.HTTPNotImplemented())
1649+
1650+ def show(self, req, server_id, id):
1651+ """ Returns a single backup schedule for a given instance """
1652+ return faults.Fault(exc.HTTPNotImplemented())
1653
1654 def create(self, req, server_id):
1655 """ No actual update method required, since the existing API allows
1656
1657=== modified file 'nova/api/openstack/common.py'
1658--- nova/api/openstack/common.py 2011-03-24 23:34:52 +0000
1659+++ nova/api/openstack/common.py 2011-04-12 11:01:25 +0000
1660@@ -21,10 +21,20 @@
1661
1662 from nova import exception
1663 from nova import flags
1664+from nova import log as logging
1665+from nova import wsgi
1666+
1667+
1668+LOG = logging.getLogger('common')
1669+
1670
1671 FLAGS = flags.FLAGS
1672
1673
1674+XML_NS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0'
1675+XML_NS_V11 = 'http://docs.openstack.org/compute/api/v1.1'
1676+
1677+
1678 def limited(items, request, max_limit=FLAGS.osapi_max_limit):
1679 """
1680 Return a slice of items according to requested offset and limit.
1681@@ -121,4 +131,11 @@
1682 try:
1683 return int(urlparse(href).path.split('/')[-1])
1684 except:
1685+ LOG.debug(_("Error extracting id from href: %s") % href)
1686 raise webob.exc.HTTPBadRequest(_('could not parse id from href'))
1687+
1688+
1689+class OpenstackController(wsgi.Controller):
1690+ def get_default_xmlns(self, req):
1691+ # Use V10 by default
1692+ return XML_NS_V10
1693
1694=== modified file 'nova/api/openstack/consoles.py'
1695--- nova/api/openstack/consoles.py 2011-03-11 19:49:32 +0000
1696+++ nova/api/openstack/consoles.py 2011-04-12 11:01:25 +0000
1697@@ -19,7 +19,7 @@
1698
1699 from nova import console
1700 from nova import exception
1701-from nova import wsgi
1702+from nova.api.openstack import common
1703 from nova.api.openstack import faults
1704
1705
1706@@ -43,7 +43,7 @@
1707 return dict(console=info)
1708
1709
1710-class Controller(wsgi.Controller):
1711+class Controller(common.OpenstackController):
1712 """The Consoles Controller for the Openstack API"""
1713
1714 _serialization_metadata = {
1715
1716=== added directory 'nova/api/openstack/contrib'
1717=== added file 'nova/api/openstack/contrib/__init__.py'
1718--- nova/api/openstack/contrib/__init__.py 1970-01-01 00:00:00 +0000
1719+++ nova/api/openstack/contrib/__init__.py 2011-04-12 11:01:25 +0000
1720@@ -0,0 +1,22 @@
1721+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1722+
1723+# Copyright 2011 Justin Santa Barbara
1724+# All Rights Reserved.
1725+#
1726+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1727+# not use this file except in compliance with the License. You may obtain
1728+# a copy of the License at
1729+#
1730+# http://www.apache.org/licenses/LICENSE-2.0
1731+#
1732+# Unless required by applicable law or agreed to in writing, software
1733+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1734+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1735+# License for the specific language governing permissions and limitations
1736+# under the License.import datetime
1737+
1738+"""Contrib contains extensions that are shipped with nova.
1739+
1740+It can't be called 'extensions' because that causes namespacing problems.
1741+
1742+"""
1743
1744=== added file 'nova/api/openstack/contrib/volumes.py'
1745--- nova/api/openstack/contrib/volumes.py 1970-01-01 00:00:00 +0000
1746+++ nova/api/openstack/contrib/volumes.py 2011-04-12 11:01:25 +0000
1747@@ -0,0 +1,336 @@
1748+# Copyright 2011 Justin Santa Barbara
1749+# All Rights Reserved.
1750+#
1751+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1752+# not use this file except in compliance with the License. You may obtain
1753+# a copy of the License at
1754+#
1755+# http://www.apache.org/licenses/LICENSE-2.0
1756+#
1757+# Unless required by applicable law or agreed to in writing, software
1758+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1759+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1760+# License for the specific language governing permissions and limitations
1761+# under the License.
1762+
1763+"""The volumes extension."""
1764+
1765+from webob import exc
1766+
1767+from nova import compute
1768+from nova import exception
1769+from nova import flags
1770+from nova import log as logging
1771+from nova import volume
1772+from nova import wsgi
1773+from nova.api.openstack import common
1774+from nova.api.openstack import extensions
1775+from nova.api.openstack import faults
1776+
1777+
1778+LOG = logging.getLogger("nova.api.volumes")
1779+
1780+
1781+FLAGS = flags.FLAGS
1782+
1783+
1784+def _translate_volume_detail_view(context, vol):
1785+ """Maps keys for volumes details view."""
1786+
1787+ d = _translate_volume_summary_view(context, vol)
1788+
1789+ # No additional data / lookups at the moment
1790+
1791+ return d
1792+
1793+
1794+def _translate_volume_summary_view(context, vol):
1795+ """Maps keys for volumes summary view."""
1796+ d = {}
1797+
1798+ d['id'] = vol['id']
1799+ d['status'] = vol['status']
1800+ d['size'] = vol['size']
1801+ d['availabilityZone'] = vol['availability_zone']
1802+ d['createdAt'] = vol['created_at']
1803+
1804+ if vol['attach_status'] == 'attached':
1805+ d['attachments'] = [_translate_attachment_detail_view(context, vol)]
1806+ else:
1807+ d['attachments'] = [{}]
1808+
1809+ d['displayName'] = vol['display_name']
1810+ d['displayDescription'] = vol['display_description']
1811+ return d
1812+
1813+
1814+class VolumeController(wsgi.Controller):
1815+ """The Volumes API controller for the OpenStack API."""
1816+
1817+ _serialization_metadata = {
1818+ 'application/xml': {
1819+ "attributes": {
1820+ "volume": [
1821+ "id",
1822+ "status",
1823+ "size",
1824+ "availabilityZone",
1825+ "createdAt",
1826+ "displayName",
1827+ "displayDescription",
1828+ ]}}}
1829+
1830+ def __init__(self):
1831+ self.volume_api = volume.API()
1832+ super(VolumeController, self).__init__()
1833+
1834+ def show(self, req, id):
1835+ """Return data about the given volume."""
1836+ context = req.environ['nova.context']
1837+
1838+ try:
1839+ vol = self.volume_api.get(context, id)
1840+ except exception.NotFound:
1841+ return faults.Fault(exc.HTTPNotFound())
1842+
1843+ return {'volume': _translate_volume_detail_view(context, vol)}
1844+
1845+ def delete(self, req, id):
1846+ """Delete a volume."""
1847+ context = req.environ['nova.context']
1848+
1849+ LOG.audit(_("Delete volume with id: %s"), id, context=context)
1850+
1851+ try:
1852+ self.volume_api.delete(context, volume_id=id)
1853+ except exception.NotFound:
1854+ return faults.Fault(exc.HTTPNotFound())
1855+ return exc.HTTPAccepted()
1856+
1857+ def index(self, req):
1858+ """Returns a summary list of volumes."""
1859+ return self._items(req, entity_maker=_translate_volume_summary_view)
1860+
1861+ def detail(self, req):
1862+ """Returns a detailed list of volumes."""
1863+ return self._items(req, entity_maker=_translate_volume_detail_view)
1864+
1865+ def _items(self, req, entity_maker):
1866+ """Returns a list of volumes, transformed through entity_maker."""
1867+ context = req.environ['nova.context']
1868+
1869+ volumes = self.volume_api.get_all(context)
1870+ limited_list = common.limited(volumes, req)
1871+ res = [entity_maker(context, vol) for vol in limited_list]
1872+ return {'volumes': res}
1873+
1874+ def create(self, req):
1875+ """Creates a new volume."""
1876+ context = req.environ['nova.context']
1877+
1878+ env = self._deserialize(req.body, req.get_content_type())
1879+ if not env:
1880+ return faults.Fault(exc.HTTPUnprocessableEntity())
1881+
1882+ vol = env['volume']
1883+ size = vol['size']
1884+ LOG.audit(_("Create volume of %s GB"), size, context=context)
1885+ new_volume = self.volume_api.create(context, size,
1886+ vol.get('display_name'),
1887+ vol.get('display_description'))
1888+
1889+ # Work around problem that instance is lazy-loaded...
1890+ new_volume['instance'] = None
1891+
1892+ retval = _translate_volume_detail_view(context, new_volume)
1893+
1894+ return {'volume': retval}
1895+
1896+
1897+def _translate_attachment_detail_view(_context, vol):
1898+ """Maps keys for attachment details view."""
1899+
1900+ d = _translate_attachment_summary_view(_context, vol)
1901+
1902+ # No additional data / lookups at the moment
1903+
1904+ return d
1905+
1906+
1907+def _translate_attachment_summary_view(_context, vol):
1908+ """Maps keys for attachment summary view."""
1909+ d = {}
1910+
1911+ volume_id = vol['id']
1912+
1913+ # NOTE(justinsb): We use the volume id as the id of the attachment object
1914+ d['id'] = volume_id
1915+
1916+ d['volumeId'] = volume_id
1917+ if vol.get('instance_id'):
1918+ d['serverId'] = vol['instance_id']
1919+ if vol.get('mountpoint'):
1920+ d['device'] = vol['mountpoint']
1921+
1922+ return d
1923+
1924+
1925+class VolumeAttachmentController(wsgi.Controller):
1926+ """The volume attachment API controller for the Openstack API.
1927+
1928+ A child resource of the server. Note that we use the volume id
1929+ as the ID of the attachment (though this is not guaranteed externally)
1930+
1931+ """
1932+
1933+ _serialization_metadata = {
1934+ 'application/xml': {
1935+ 'attributes': {
1936+ 'volumeAttachment': ['id',
1937+ 'serverId',
1938+ 'volumeId',
1939+ 'device']}}}
1940+
1941+ def __init__(self):
1942+ self.compute_api = compute.API()
1943+ self.volume_api = volume.API()
1944+ super(VolumeAttachmentController, self).__init__()
1945+
1946+ def index(self, req, server_id):
1947+ """Returns the list of volume attachments for a given instance."""
1948+ return self._items(req, server_id,
1949+ entity_maker=_translate_attachment_summary_view)
1950+
1951+ def show(self, req, server_id, id):
1952+ """Return data about the given volume attachment."""
1953+ context = req.environ['nova.context']
1954+
1955+ volume_id = id
1956+ try:
1957+ vol = self.volume_api.get(context, volume_id)
1958+ except exception.NotFound:
1959+ LOG.debug("volume_id not found")
1960+ return faults.Fault(exc.HTTPNotFound())
1961+
1962+ if str(vol['instance_id']) != server_id:
1963+ LOG.debug("instance_id != server_id")
1964+ return faults.Fault(exc.HTTPNotFound())
1965+
1966+ return {'volumeAttachment': _translate_attachment_detail_view(context,
1967+ vol)}
1968+
1969+ def create(self, req, server_id):
1970+ """Attach a volume to an instance."""
1971+ context = req.environ['nova.context']
1972+
1973+ env = self._deserialize(req.body, req.get_content_type())
1974+ if not env:
1975+ return faults.Fault(exc.HTTPUnprocessableEntity())
1976+
1977+ instance_id = server_id
1978+ volume_id = env['volumeAttachment']['volumeId']
1979+ device = env['volumeAttachment']['device']
1980+
1981+ msg = _("Attach volume %(volume_id)s to instance %(server_id)s"
1982+ " at %(device)s") % locals()
1983+ LOG.audit(msg, context=context)
1984+
1985+ try:
1986+ self.compute_api.attach_volume(context,
1987+ instance_id=instance_id,
1988+ volume_id=volume_id,
1989+ device=device)
1990+ except exception.NotFound:
1991+ return faults.Fault(exc.HTTPNotFound())
1992+
1993+ # The attach is async
1994+ attachment = {}
1995+ attachment['id'] = volume_id
1996+ attachment['volumeId'] = volume_id
1997+
1998+ # NOTE(justinsb): And now, we have a problem...
1999+ # The attach is async, so there's a window in which we don't see
2000+ # the attachment (until the attachment completes). We could also
2001+ # get problems with concurrent requests. I think we need an
2002+ # attachment state, and to write to the DB here, but that's a bigger
2003+ # change.
2004+ # For now, we'll probably have to rely on libraries being smart
2005+
2006+ # TODO(justinsb): How do I return "accepted" here?
2007+ return {'volumeAttachment': attachment}
2008+
2009+ def update(self, _req, _server_id, _id):
2010+ """Update a volume attachment. We don't currently support this."""
2011+ return faults.Fault(exc.HTTPBadRequest())
2012+
2013+ def delete(self, req, server_id, id):
2014+ """Detach a volume from an instance."""
2015+ context = req.environ['nova.context']
2016+
2017+ volume_id = id
2018+ LOG.audit(_("Detach volume %s"), volume_id, context=context)
2019+
2020+ try:
2021+ vol = self.volume_api.get(context, volume_id)
2022+ except exception.NotFound:
2023+ return faults.Fault(exc.HTTPNotFound())
2024+
2025+ if str(vol['instance_id']) != server_id:
2026+ LOG.debug("instance_id != server_id")
2027+ return faults.Fault(exc.HTTPNotFound())
2028+
2029+ self.compute_api.detach_volume(context,
2030+ volume_id=volume_id)
2031+
2032+ return exc.HTTPAccepted()
2033+
2034+ def _items(self, req, server_id, entity_maker):
2035+ """Returns a list of attachments, transformed through entity_maker."""
2036+ context = req.environ['nova.context']
2037+
2038+ try:
2039+ instance = self.compute_api.get(context, server_id)
2040+ except exception.NotFound:
2041+ return faults.Fault(exc.HTTPNotFound())
2042+
2043+ volumes = instance['volumes']
2044+ limited_list = common.limited(volumes, req)
2045+ res = [entity_maker(context, vol) for vol in limited_list]
2046+ return {'volumeAttachments': res}
2047+
2048+
2049+class Volumes(extensions.ExtensionDescriptor):
2050+ def get_name(self):
2051+ return "Volumes"
2052+
2053+ def get_alias(self):
2054+ return "VOLUMES"
2055+
2056+ def get_description(self):
2057+ return "Volumes support"
2058+
2059+ def get_namespace(self):
2060+ return "http://docs.openstack.org/ext/volumes/api/v1.1"
2061+
2062+ def get_updated(self):
2063+ return "2011-03-25T00:00:00+00:00"
2064+
2065+ def get_resources(self):
2066+ resources = []
2067+
2068+ # NOTE(justinsb): No way to provide singular name ('volume')
2069+ # Does this matter?
2070+ res = extensions.ResourceExtension('volumes',
2071+ VolumeController(),
2072+ collection_actions={'detail': 'GET'}
2073+ )
2074+ resources.append(res)
2075+
2076+ res = extensions.ResourceExtension('volume_attachments',
2077+ VolumeAttachmentController(),
2078+ parent=dict(
2079+ member_name='server',
2080+ collection_name='servers'))
2081+ resources.append(res)
2082+
2083+ return resources
2084
2085=== modified file 'nova/api/openstack/extensions.py'
2086--- nova/api/openstack/extensions.py 2011-03-21 18:00:39 +0000
2087+++ nova/api/openstack/extensions.py 2011-04-12 11:01:25 +0000
2088@@ -1,6 +1,7 @@
2089 # vim: tabstop=4 shiftwidth=4 softtabstop=4
2090
2091 # Copyright 2011 OpenStack LLC.
2092+# Copyright 2011 Justin Santa Barbara
2093 # All Rights Reserved.
2094 #
2095 # Licensed under the Apache License, Version 2.0 (the "License"); you may
2096@@ -16,15 +17,18 @@
2097 # under the License.
2098
2099 import imp
2100+import inspect
2101 import os
2102 import sys
2103 import routes
2104 import webob.dec
2105 import webob.exc
2106
2107+from nova import exception
2108 from nova import flags
2109 from nova import log as logging
2110 from nova import wsgi
2111+from nova.api.openstack import common
2112 from nova.api.openstack import faults
2113
2114
2115@@ -34,7 +38,85 @@
2116 FLAGS = flags.FLAGS
2117
2118
2119-class ActionExtensionController(wsgi.Controller):
2120+class ExtensionDescriptor(object):
2121+ """Base class that defines the contract for extensions.
2122+
2123+ Note that you don't have to derive from this class to have a valid
2124+ extension; it is purely a convenience.
2125+
2126+ """
2127+
2128+ def get_name(self):
2129+ """The name of the extension.
2130+
2131+ e.g. 'Fox In Socks'
2132+
2133+ """
2134+ raise NotImplementedError()
2135+
2136+ def get_alias(self):
2137+ """The alias for the extension.
2138+
2139+ e.g. 'FOXNSOX'
2140+
2141+ """
2142+ raise NotImplementedError()
2143+
2144+ def get_description(self):
2145+ """Friendly description for the extension.
2146+
2147+ e.g. 'The Fox In Socks Extension'
2148+
2149+ """
2150+ raise NotImplementedError()
2151+
2152+ def get_namespace(self):
2153+ """The XML namespace for the extension.
2154+
2155+ e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'
2156+
2157+ """
2158+ raise NotImplementedError()
2159+
2160+ def get_updated(self):
2161+ """The timestamp when the extension was last updated.
2162+
2163+ e.g. '2011-01-22T13:25:27-06:00'
2164+
2165+ """
2166+ # NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS
2167+ raise NotImplementedError()
2168+
2169+ def get_resources(self):
2170+ """List of extensions.ResourceExtension extension objects.
2171+
2172+ Resources define new nouns, and are accessible through URLs.
2173+
2174+ """
2175+ resources = []
2176+ return resources
2177+
2178+ def get_actions(self):
2179+ """List of extensions.ActionExtension extension objects.
2180+
2181+ Actions are verbs callable from the API.
2182+
2183+ """
2184+ actions = []
2185+ return actions
2186+
2187+ def get_response_extensions(self):
2188+ """List of extensions.ResponseExtension extension objects.
2189+
2190+ Response extensions are used to insert information into existing
2191+ response data.
2192+
2193+ """
2194+ response_exts = []
2195+ return response_exts
2196+
2197+
2198+class ActionExtensionController(common.OpenstackController):
2199
2200 def __init__(self, application):
2201
2202@@ -55,7 +137,7 @@
2203 return res
2204
2205
2206-class ResponseExtensionController(wsgi.Controller):
2207+class ResponseExtensionController(common.OpenstackController):
2208
2209 def __init__(self, application):
2210 self.application = application
2211@@ -74,7 +156,8 @@
2212 body = res.body
2213 headers = res.headers
2214 except AttributeError:
2215- body = self._serialize(res, content_type)
2216+ default_xmlns = None
2217+ body = self._serialize(res, content_type, default_xmlns)
2218 headers = {"Content-Type": content_type}
2219 res = webob.Response()
2220 res.body = body
2221@@ -82,7 +165,7 @@
2222 return res
2223
2224
2225-class ExtensionController(wsgi.Controller):
2226+class ExtensionController(common.OpenstackController):
2227
2228 def __init__(self, extension_manager):
2229 self.extension_manager = extension_manager
2230@@ -94,45 +177,38 @@
2231 ext_data['description'] = ext.get_description()
2232 ext_data['namespace'] = ext.get_namespace()
2233 ext_data['updated'] = ext.get_updated()
2234- ext_data['links'] = [] # TODO: implement extension links
2235+ ext_data['links'] = [] # TODO(dprince): implement extension links
2236 return ext_data
2237
2238 def index(self, req):
2239 extensions = []
2240- for alias, ext in self.extension_manager.extensions.iteritems():
2241+ for _alias, ext in self.extension_manager.extensions.iteritems():
2242 extensions.append(self._translate(ext))
2243 return dict(extensions=extensions)
2244
2245 def show(self, req, id):
2246- # NOTE: the extensions alias is used as the 'id' for show
2247+ # NOTE(dprince): the extensions alias is used as the 'id' for show
2248 ext = self.extension_manager.extensions[id]
2249 return self._translate(ext)
2250
2251 def delete(self, req, id):
2252- raise faults.Fault(exc.HTTPNotFound())
2253+ raise faults.Fault(webob.exc.HTTPNotFound())
2254
2255 def create(self, req):
2256- raise faults.Fault(exc.HTTPNotFound())
2257-
2258- def delete(self, req, id):
2259- raise faults.Fault(exc.HTTPNotFound())
2260+ raise faults.Fault(webob.exc.HTTPNotFound())
2261
2262
2263 class ExtensionMiddleware(wsgi.Middleware):
2264- """
2265- Extensions middleware that intercepts configured routes for extensions.
2266- """
2267+ """Extensions middleware for WSGI."""
2268 @classmethod
2269 def factory(cls, global_config, **local_config):
2270- """ paste factory """
2271+ """Paste factory."""
2272 def _factory(app):
2273 return cls(app, **local_config)
2274 return _factory
2275
2276 def _action_ext_controllers(self, application, ext_mgr, mapper):
2277- """
2278- Return a dict of ActionExtensionController objects by collection
2279- """
2280+ """Return a dict of ActionExtensionController-s by collection."""
2281 action_controllers = {}
2282 for action in ext_mgr.get_actions():
2283 if not action.collection in action_controllers.keys():
2284@@ -151,9 +227,7 @@
2285 return action_controllers
2286
2287 def _response_ext_controllers(self, application, ext_mgr, mapper):
2288- """
2289- Return a dict of ResponseExtensionController objects by collection
2290- """
2291+ """Returns a dict of ResponseExtensionController-s by collection."""
2292 response_ext_controllers = {}
2293 for resp_ext in ext_mgr.get_response_extensions():
2294 if not resp_ext.key in response_ext_controllers.keys():
2295@@ -212,18 +286,18 @@
2296
2297 @webob.dec.wsgify(RequestClass=wsgi.Request)
2298 def __call__(self, req):
2299- """
2300- Route the incoming request with router.
2301- """
2302+ """Route the incoming request with router."""
2303 req.environ['extended.app'] = self.application
2304 return self._router
2305
2306 @staticmethod
2307 @webob.dec.wsgify(RequestClass=wsgi.Request)
2308 def _dispatch(req):
2309- """
2310+ """Dispatch the request.
2311+
2312 Returns the routed WSGI app's response or defers to the extended
2313 application.
2314+
2315 """
2316 match = req.environ['wsgiorg.routing_args'][1]
2317 if not match:
2318@@ -233,10 +307,11 @@
2319
2320
2321 class ExtensionManager(object):
2322- """
2323- Load extensions from the configured extension path.
2324- See nova/tests/api/openstack/extensions/foxinsocks.py for an example
2325- extension implementation.
2326+ """Load extensions from the configured extension path.
2327+
2328+ See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an
2329+ example extension implementation.
2330+
2331 """
2332
2333 def __init__(self, path):
2334@@ -244,12 +319,10 @@
2335
2336 self.path = path
2337 self.extensions = {}
2338- self._load_extensions()
2339+ self._load_all_extensions()
2340
2341 def get_resources(self):
2342- """
2343- returns a list of ResourceExtension objects
2344- """
2345+ """Returns a list of ResourceExtension objects."""
2346 resources = []
2347 resources.append(ResourceExtension('extensions',
2348 ExtensionController(self)))
2349@@ -257,40 +330,37 @@
2350 try:
2351 resources.extend(ext.get_resources())
2352 except AttributeError:
2353- # NOTE: Extension aren't required to have resource extensions
2354+ # NOTE(dprince): Extension aren't required to have resource
2355+ # extensions
2356 pass
2357 return resources
2358
2359 def get_actions(self):
2360- """
2361- returns a list of ActionExtension objects
2362- """
2363+ """Returns a list of ActionExtension objects."""
2364 actions = []
2365 for alias, ext in self.extensions.iteritems():
2366 try:
2367 actions.extend(ext.get_actions())
2368 except AttributeError:
2369- # NOTE: Extension aren't required to have action extensions
2370+ # NOTE(dprince): Extension aren't required to have action
2371+ # extensions
2372 pass
2373 return actions
2374
2375 def get_response_extensions(self):
2376- """
2377- returns a list of ResponseExtension objects
2378- """
2379+ """Returns a list of ResponseExtension objects."""
2380 response_exts = []
2381 for alias, ext in self.extensions.iteritems():
2382 try:
2383 response_exts.extend(ext.get_response_extensions())
2384 except AttributeError:
2385- # NOTE: Extension aren't required to have response extensions
2386+ # NOTE(dprince): Extension aren't required to have response
2387+ # extensions
2388 pass
2389 return response_exts
2390
2391 def _check_extension(self, extension):
2392- """
2393- Checks for required methods in extension objects.
2394- """
2395+ """Checks for required methods in extension objects."""
2396 try:
2397 LOG.debug(_('Ext name: %s'), extension.get_name())
2398 LOG.debug(_('Ext alias: %s'), extension.get_alias())
2399@@ -300,40 +370,59 @@
2400 except AttributeError as ex:
2401 LOG.exception(_("Exception loading extension: %s"), unicode(ex))
2402
2403- def _load_extensions(self):
2404- """
2405+ def _load_all_extensions(self):
2406+ """Load extensions from the configured path.
2407+
2408 Load extensions from the configured path. The extension name is
2409 constructed from the module_name. If your extension module was named
2410 widgets.py the extension class within that module should be
2411 'Widgets'.
2412
2413+ In addition, extensions are loaded from the 'contrib' directory.
2414+
2415 See nova/tests/api/openstack/extensions/foxinsocks.py for an example
2416 extension implementation.
2417+
2418 """
2419- if not os.path.exists(self.path):
2420- return
2421-
2422- for f in os.listdir(self.path):
2423+ if os.path.exists(self.path):
2424+ self._load_all_extensions_from_path(self.path)
2425+
2426+ contrib_path = os.path.join(os.path.dirname(__file__), "contrib")
2427+ if os.path.exists(contrib_path):
2428+ self._load_all_extensions_from_path(contrib_path)
2429+
2430+ def _load_all_extensions_from_path(self, path):
2431+ for f in os.listdir(path):
2432 LOG.audit(_('Loading extension file: %s'), f)
2433 mod_name, file_ext = os.path.splitext(os.path.split(f)[-1])
2434- ext_path = os.path.join(self.path, f)
2435- if file_ext.lower() == '.py':
2436+ ext_path = os.path.join(path, f)
2437+ if file_ext.lower() == '.py' and not mod_name.startswith('_'):
2438 mod = imp.load_source(mod_name, ext_path)
2439 ext_name = mod_name[0].upper() + mod_name[1:]
2440- try:
2441- new_ext = getattr(mod, ext_name)()
2442- self._check_extension(new_ext)
2443- self.extensions[new_ext.get_alias()] = new_ext
2444- except AttributeError as ex:
2445- LOG.exception(_("Exception loading extension: %s"),
2446- unicode(ex))
2447+ new_ext_class = getattr(mod, ext_name, None)
2448+ if not new_ext_class:
2449+ LOG.warn(_('Did not find expected name '
2450+ '"%(ext_name)s" in %(file)s'),
2451+ {'ext_name': ext_name,
2452+ 'file': ext_path})
2453+ continue
2454+ new_ext = new_ext_class()
2455+ self._check_extension(new_ext)
2456+ self._add_extension(new_ext)
2457+
2458+ def _add_extension(self, ext):
2459+ alias = ext.get_alias()
2460+ LOG.audit(_('Loaded extension: %s'), alias)
2461+
2462+ self._check_extension(ext)
2463+
2464+ if alias in self.extensions:
2465+ raise exception.Error("Found duplicate extension: %s" % alias)
2466+ self.extensions[alias] = ext
2467
2468
2469 class ResponseExtension(object):
2470- """
2471- ResponseExtension objects can be used to add data to responses from
2472- core nova OpenStack API controllers.
2473- """
2474+ """Add data to responses from core nova OpenStack API controllers."""
2475
2476 def __init__(self, method, url_route, handler):
2477 self.url_route = url_route
2478@@ -343,10 +432,7 @@
2479
2480
2481 class ActionExtension(object):
2482- """
2483- ActionExtension objects can be used to add custom actions to core nova
2484- nova OpenStack API controllers.
2485- """
2486+ """Add custom actions to core nova OpenStack API controllers."""
2487
2488 def __init__(self, collection, action_name, handler):
2489 self.collection = collection
2490@@ -355,10 +441,7 @@
2491
2492
2493 class ResourceExtension(object):
2494- """
2495- ResourceExtension objects can be used to add top level resources
2496- to the OpenStack API in nova.
2497- """
2498+ """Add top level resources to the OpenStack API in nova."""
2499
2500 def __init__(self, collection, controller, parent=None,
2501 collection_actions={}, member_actions={}):
2502
2503=== modified file 'nova/api/openstack/faults.py'
2504--- nova/api/openstack/faults.py 2011-03-17 20:26:52 +0000
2505+++ nova/api/openstack/faults.py 2011-04-12 11:01:25 +0000
2506@@ -20,10 +20,10 @@
2507 import webob.exc
2508
2509 from nova import wsgi
2510+from nova.api.openstack import common
2511
2512
2513 class Fault(webob.exc.HTTPException):
2514-
2515 """An RS API fault response."""
2516
2517 _fault_names = {
2518@@ -47,7 +47,7 @@
2519 """Generate a WSGI response based on the exception passed to ctor."""
2520 # Replace the body with fault details.
2521 code = self.wrapped_exc.status_int
2522- fault_name = self._fault_names.get(code, "computeFault")
2523+ fault_name = self._fault_names.get(code, "cloudServersFault")
2524 fault_data = {
2525 fault_name: {
2526 'code': code,
2527@@ -57,9 +57,11 @@
2528 fault_data[fault_name]['retryAfter'] = retry
2529 # 'code' is an attribute on the fault tag itself
2530 metadata = {'application/xml': {'attributes': {fault_name: 'code'}}}
2531- serializer = wsgi.Serializer(metadata)
2532+ default_xmlns = common.XML_NS_V10
2533+ serializer = wsgi.Serializer(metadata, default_xmlns)
2534 content_type = req.best_match_content_type()
2535 self.wrapped_exc.body = serializer.serialize(fault_data, content_type)
2536+ self.wrapped_exc.content_type = content_type
2537 return self.wrapped_exc
2538
2539
2540
2541=== modified file 'nova/api/openstack/flavors.py'
2542--- nova/api/openstack/flavors.py 2011-03-24 16:46:39 +0000
2543+++ nova/api/openstack/flavors.py 2011-04-12 11:01:25 +0000
2544@@ -19,11 +19,11 @@
2545
2546 from nova import db
2547 from nova import exception
2548-from nova import wsgi
2549+from nova.api.openstack import common
2550 from nova.api.openstack import views
2551
2552
2553-class Controller(wsgi.Controller):
2554+class Controller(common.OpenstackController):
2555 """Flavor controller for the OpenStack API."""
2556
2557 _serialization_metadata = {
2558@@ -76,3 +76,6 @@
2559 def _get_view_builder(self, req):
2560 base_url = req.application_url
2561 return views.flavors.ViewBuilderV11(base_url)
2562+
2563+ def get_default_xmlns(self, req):
2564+ return common.XML_NS_V11
2565
2566=== modified file 'nova/api/openstack/image_metadata.py'
2567--- nova/api/openstack/image_metadata.py 2011-03-25 14:07:42 +0000
2568+++ nova/api/openstack/image_metadata.py 2011-04-12 11:01:25 +0000
2569@@ -18,15 +18,17 @@
2570 from webob import exc
2571
2572 from nova import flags
2573+from nova import quota
2574 from nova import utils
2575 from nova import wsgi
2576+from nova.api.openstack import common
2577 from nova.api.openstack import faults
2578
2579
2580 FLAGS = flags.FLAGS
2581
2582
2583-class Controller(wsgi.Controller):
2584+class Controller(common.OpenstackController):
2585 """The image metadata API controller for the Openstack API"""
2586
2587 def __init__(self):
2588@@ -39,6 +41,15 @@
2589 metadata = image.get('properties', {})
2590 return metadata
2591
2592+ def _check_quota_limit(self, context, metadata):
2593+ if metadata is None:
2594+ return
2595+ num_metadata = len(metadata)
2596+ quota_metadata = quota.allowed_metadata_items(context, num_metadata)
2597+ if quota_metadata < num_metadata:
2598+ expl = _("Image metadata limit exceeded")
2599+ raise exc.HTTPBadRequest(explanation=expl)
2600+
2601 def index(self, req, image_id):
2602 """Returns the list of metadata for a given instance"""
2603 context = req.environ['nova.context']
2604@@ -61,6 +72,7 @@
2605 if 'metadata' in body:
2606 for key, value in body['metadata'].iteritems():
2607 metadata[key] = value
2608+ self._check_quota_limit(context, metadata)
2609 img['properties'] = metadata
2610 self.image_service.update(context, image_id, img, None)
2611 return dict(metadata=metadata)
2612@@ -77,6 +89,7 @@
2613 img = self.image_service.show(context, image_id)
2614 metadata = self._get_metadata(context, image_id, img)
2615 metadata[id] = body[id]
2616+ self._check_quota_limit(context, metadata)
2617 img['properties'] = metadata
2618 self.image_service.update(context, image_id, img, None)
2619
2620
2621=== modified file 'nova/api/openstack/images.py'
2622--- nova/api/openstack/images.py 2011-03-24 21:13:55 +0000
2623+++ nova/api/openstack/images.py 2011-04-12 11:01:25 +0000
2624@@ -1,6 +1,4 @@
2625-# vim: tabstop=4 shiftwidth=4 softtabstop=4
2626-
2627-# Copyright 2010 OpenStack LLC.
2628+# Copyright 2011 OpenStack LLC.
2629 # All Rights Reserved.
2630 #
2631 # Licensed under the Apache License, Version 2.0 (the "License"); you may
2632@@ -15,248 +13,143 @@
2633 # License for the specific language governing permissions and limitations
2634 # under the License.
2635
2636-import datetime
2637-
2638-from webob import exc
2639+import webob.exc
2640
2641 from nova import compute
2642 from nova import exception
2643 from nova import flags
2644 from nova import log
2645 from nova import utils
2646-from nova import wsgi
2647-import nova.api.openstack
2648 from nova.api.openstack import common
2649 from nova.api.openstack import faults
2650-import nova.image.service
2651+from nova.api.openstack.views import images as images_view
2652
2653
2654 LOG = log.getLogger('nova.api.openstack.images')
2655-
2656 FLAGS = flags.FLAGS
2657
2658
2659-def _translate_keys(item):
2660- """
2661- Maps key names to Rackspace-like attributes for return
2662- also pares down attributes to those we want
2663- item is a dict
2664-
2665- Note: should be removed when the set of keys expected by the api
2666- and the set of keys returned by the image service are equivalent
2667-
2668- """
2669- # TODO(tr3buchet): this map is specific to s3 object store,
2670- # replace with a list of keys for _filter_keys later
2671- mapped_keys = {'status': 'imageState',
2672- 'id': 'imageId',
2673- 'name': 'imageLocation'}
2674-
2675- mapped_item = {}
2676- # TODO(tr3buchet):
2677- # this chunk of code works with s3 and the local image service/glance
2678- # when we switch to glance/local image service it can be replaced with
2679- # a call to _filter_keys, and mapped_keys can be changed to a list
2680- try:
2681- for k, v in mapped_keys.iteritems():
2682- # map s3 fields
2683- mapped_item[k] = item[v]
2684- except KeyError:
2685- # return only the fields api expects
2686- mapped_item = _filter_keys(item, mapped_keys.keys())
2687-
2688- return mapped_item
2689-
2690-
2691-def _translate_status(item):
2692- """
2693- Translates status of image to match current Rackspace api bindings
2694- item is a dict
2695-
2696- Note: should be removed when the set of statuses expected by the api
2697- and the set of statuses returned by the image service are equivalent
2698-
2699- """
2700- status_mapping = {
2701- 'pending': 'queued',
2702- 'decrypting': 'preparing',
2703- 'untarring': 'saving',
2704- 'available': 'active'}
2705- try:
2706- item['status'] = status_mapping[item['status']]
2707- except KeyError:
2708- # TODO(sirp): Performing translation of status (if necessary) here for
2709- # now. Perhaps this should really be done in EC2 API and
2710- # S3ImageService
2711- pass
2712-
2713-
2714-def _filter_keys(item, keys):
2715- """
2716- Filters all model attributes except for keys
2717- item is a dict
2718-
2719- """
2720- return dict((k, v) for k, v in item.iteritems() if k in keys)
2721-
2722-
2723-def _convert_image_id_to_hash(image):
2724- if 'imageId' in image:
2725- # Convert EC2-style ID (i-blah) to Rackspace-style (int)
2726- image_id = abs(hash(image['imageId']))
2727- image['imageId'] = image_id
2728- image['id'] = image_id
2729-
2730-
2731-def _translate_s3_like_images(image_metadata):
2732- """Work-around for leaky S3ImageService abstraction"""
2733- api_metadata = image_metadata.copy()
2734- _convert_image_id_to_hash(api_metadata)
2735- api_metadata = _translate_keys(api_metadata)
2736- _translate_status(api_metadata)
2737- return api_metadata
2738-
2739-
2740-def _translate_from_image_service_to_api(image_metadata):
2741- """Translate from ImageService to OpenStack API style attribute names
2742-
2743- This involves 4 steps:
2744-
2745- 1. Filter out attributes that the OpenStack API doesn't need
2746-
2747- 2. Translate from base image attributes from names used by
2748- BaseImageService to names used by OpenStack API
2749-
2750- 3. Add in any image properties
2751-
2752- 4. Format values according to API spec (for example dates must
2753- look like "2010-08-10T12:00:00Z")
2754- """
2755- service_metadata = image_metadata.copy()
2756- properties = service_metadata.pop('properties', {})
2757-
2758- # 1. Filter out unecessary attributes
2759- api_keys = ['id', 'name', 'updated_at', 'created_at', 'status']
2760- api_metadata = utils.subset_dict(service_metadata, api_keys)
2761-
2762- # 2. Translate base image attributes
2763- api_map = {'updated_at': 'updated', 'created_at': 'created'}
2764- api_metadata = utils.map_dict_keys(api_metadata, api_map)
2765-
2766- # 3. Add in any image properties
2767- # 3a. serverId is used for backups and snapshots
2768- try:
2769- api_metadata['serverId'] = int(properties['instance_id'])
2770- except KeyError:
2771- pass # skip if it's not present
2772- except ValueError:
2773- pass # skip if it's not an integer
2774-
2775- # 3b. Progress special case
2776- # TODO(sirp): ImageService doesn't have a notion of progress yet, so for
2777- # now just fake it
2778- if service_metadata['status'] == 'saving':
2779- api_metadata['progress'] = 0
2780-
2781- # 4. Format values
2782- # 4a. Format Image Status (API requires uppercase)
2783- api_metadata['status'] = _format_status_for_api(api_metadata['status'])
2784-
2785- # 4b. Format timestamps
2786- for attr in ('created', 'updated'):
2787- if attr in api_metadata:
2788- api_metadata[attr] = _format_datetime_for_api(
2789- api_metadata[attr])
2790-
2791- return api_metadata
2792-
2793-
2794-def _format_status_for_api(status):
2795- """Return status in a format compliant with OpenStack API"""
2796- mapping = {'queued': 'QUEUED',
2797- 'preparing': 'PREPARING',
2798- 'saving': 'SAVING',
2799- 'active': 'ACTIVE',
2800- 'killed': 'FAILED'}
2801- return mapping[status]
2802-
2803-
2804-def _format_datetime_for_api(datetime_):
2805- """Stringify datetime objects in a format compliant with OpenStack API"""
2806- API_DATETIME_FMT = '%Y-%m-%dT%H:%M:%SZ'
2807- return datetime_.strftime(API_DATETIME_FMT)
2808-
2809-
2810-def _safe_translate(image_metadata):
2811- """Translate attributes for OpenStack API, temporary workaround for
2812- S3ImageService attribute leakage.
2813- """
2814- # FIXME(sirp): The S3ImageService appears to be leaking implementation
2815- # details, including its internal attribute names, and internal
2816- # `status` values. Working around it for now.
2817- s3_like_image = ('imageId' in image_metadata)
2818- if s3_like_image:
2819- translate = _translate_s3_like_images
2820- else:
2821- translate = _translate_from_image_service_to_api
2822- return translate(image_metadata)
2823-
2824-
2825-class Controller(wsgi.Controller):
2826+class Controller(common.OpenstackController):
2827+ """Base `wsgi.Controller` for retrieving/displaying images."""
2828
2829 _serialization_metadata = {
2830 'application/xml': {
2831 "attributes": {
2832 "image": ["id", "name", "updated", "created", "status",
2833- "serverId", "progress"]}}}
2834-
2835- def __init__(self):
2836- self._service = utils.import_object(FLAGS.image_service)
2837+ "serverId", "progress"],
2838+ "link": ["rel", "type", "href"],
2839+ },
2840+ },
2841+ }
2842+
2843+ def __init__(self, image_service=None, compute_service=None):
2844+ """Initialize new `ImageController`.
2845+
2846+ :param compute_service: `nova.compute.api:API`
2847+ :param image_service: `nova.image.service:BaseImageService`
2848+ """
2849+ _default_service = utils.import_object(flags.FLAGS.image_service)
2850+
2851+ self._compute_service = compute_service or compute.API()
2852+ self._image_service = image_service or _default_service
2853
2854 def index(self, req):
2855- """Return all public images in brief"""
2856+ """Return an index listing of images available to the request.
2857+
2858+ :param req: `wsgi.Request` object
2859+ """
2860 context = req.environ['nova.context']
2861- image_metas = self._service.index(context)
2862- image_metas = common.limited(image_metas, req)
2863- return dict(images=image_metas)
2864+ images = self._image_service.index(context)
2865+ images = common.limited(images, req)
2866+ builder = self.get_builder(req).build
2867+ return dict(images=[builder(image, detail=False) for image in images])
2868
2869 def detail(self, req):
2870- """Return all public images in detail"""
2871+ """Return a detailed index listing of images available to the request.
2872+
2873+ :param req: `wsgi.Request` object.
2874+ """
2875 context = req.environ['nova.context']
2876- image_metas = self._service.detail(context)
2877- image_metas = common.limited(image_metas, req)
2878- api_image_metas = [_safe_translate(image_meta)
2879- for image_meta in image_metas]
2880- return dict(images=api_image_metas)
2881+ images = self._image_service.detail(context)
2882+ images = common.limited(images, req)
2883+ builder = self.get_builder(req).build
2884+ return dict(images=[builder(image, detail=True) for image in images])
2885
2886 def show(self, req, id):
2887- """Return data about the given image id"""
2888+ """Return detailed information about a specific image.
2889+
2890+ :param req: `wsgi.Request` object
2891+ :param id: Image identifier (integer)
2892+ """
2893 context = req.environ['nova.context']
2894- try:
2895- image_id = common.get_image_id_from_image_hash(
2896- self._service, context, id)
2897+
2898+ try:
2899+ image_id = int(id)
2900+ except ValueError:
2901+ explanation = _("Image not found.")
2902+ raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation))
2903+
2904+ try:
2905+ image = self._image_service.show(context, image_id)
2906 except exception.NotFound:
2907- raise faults.Fault(exc.HTTPNotFound())
2908+ explanation = _("Image '%d' not found.") % (image_id)
2909+ raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation))
2910
2911- image_meta = self._service.show(context, image_id)
2912- api_image_meta = _safe_translate(image_meta)
2913- return dict(image=api_image_meta)
2914+ return dict(image=self.get_builder(req).build(image, detail=True))
2915
2916 def delete(self, req, id):
2917- # Only public images are supported for now.
2918- raise faults.Fault(exc.HTTPNotFound())
2919+ """Delete an image, if allowed.
2920+
2921+ :param req: `wsgi.Request` object
2922+ :param id: Image identifier (integer)
2923+ """
2924+ image_id = id
2925+ context = req.environ['nova.context']
2926+ self._image_service.delete(context, image_id)
2927+ return webob.exc.HTTPNoContent()
2928
2929 def create(self, req):
2930+ """Snapshot a server instance and save the image.
2931+
2932+ :param req: `wsgi.Request` object
2933+ """
2934 context = req.environ['nova.context']
2935- env = self._deserialize(req.body, req.get_content_type())
2936- instance_id = env["image"]["serverId"]
2937- name = env["image"]["name"]
2938- image_meta = compute.API().snapshot(
2939- context, instance_id, name)
2940- api_image_meta = _safe_translate(image_meta)
2941- return dict(image=api_image_meta)
2942-
2943- def update(self, req, id):
2944- # Users may not modify public images, and that's all that
2945- # we support for now.
2946- raise faults.Fault(exc.HTTPNotFound())
2947+ content_type = req.get_content_type()
2948+ image = self._deserialize(req.body, content_type)
2949+
2950+ if not image:
2951+ raise webob.exc.HTTPBadRequest()
2952+
2953+ try:
2954+ server_id = image["image"]["serverId"]
2955+ image_name = image["image"]["name"]
2956+ except KeyError:
2957+ raise webob.exc.HTTPBadRequest()
2958+
2959+ image = self._compute_service.snapshot(context, server_id, image_name)
2960+ return self.get_builder(req).build(image, detail=True)
2961+
2962+ def get_builder(self, request):
2963+ """Indicates that you must use a Controller subclass."""
2964+ raise NotImplementedError
2965+
2966+
2967+class ControllerV10(Controller):
2968+ """Version 1.0 specific controller logic."""
2969+
2970+ def get_builder(self, request):
2971+ """Property to get the ViewBuilder class we need to use."""
2972+ base_url = request.application_url
2973+ return images_view.ViewBuilderV10(base_url)
2974+
2975+
2976+class ControllerV11(Controller):
2977+ """Version 1.1 specific controller logic."""
2978+
2979+ def get_builder(self, request):
2980+ """Property to get the ViewBuilder class we need to use."""
2981+ base_url = request.application_url
2982+ return images_view.ViewBuilderV11(base_url)
2983+
2984+ def get_default_xmlns(self, req):
2985+ return common.XML_NS_V11
2986
2987=== added file 'nova/api/openstack/ips.py'
2988--- nova/api/openstack/ips.py 1970-01-01 00:00:00 +0000
2989+++ nova/api/openstack/ips.py 2011-04-12 11:01:25 +0000
2990@@ -0,0 +1,72 @@
2991+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2992+
2993+# Copyright 2011 OpenStack LLC.
2994+# All Rights Reserved.
2995+#
2996+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2997+# not use this file except in compliance with the License. You may obtain
2998+# a copy of the License at
2999+#
3000+# http://www.apache.org/licenses/LICENSE-2.0
3001+#
3002+# Unless required by applicable law or agreed to in writing, software
3003+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
3004+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
3005+# License for the specific language governing permissions and limitations
3006+# under the License.
3007+
3008+import time
3009+
3010+from webob import exc
3011+
3012+import nova
3013+import nova.api.openstack.views.addresses
3014+from nova.api.openstack import common
3015+from nova.api.openstack import faults
3016+
3017+
3018+class Controller(common.OpenstackController):
3019+ """The servers addresses API controller for the Openstack API."""
3020+
3021+ _serialization_metadata = {
3022+ 'application/xml': {
3023+ 'list_collections': {
3024+ 'public': {'item_name': 'ip', 'item_key': 'addr'},
3025+ 'private': {'item_name': 'ip', 'item_key': 'addr'},
3026+ },
3027+ },
3028+ }
3029+
3030+ def __init__(self):
3031+ self.compute_api = nova.compute.API()
3032+ self.builder = nova.api.openstack.views.addresses.ViewBuilderV10()
3033+
3034+ def index(self, req, server_id):
3035+ try:
3036+ instance = self.compute_api.get(req.environ['nova.context'], id)
3037+ except nova.exception.NotFound:
3038+ return faults.Fault(exc.HTTPNotFound())
3039+ return {'addresses': self.builder.build(instance)}
3040+
3041+ def public(self, req, server_id):
3042+ try:
3043+ instance = self.compute_api.get(req.environ['nova.context'], id)
3044+ except nova.exception.NotFound:
3045+ return faults.Fault(exc.HTTPNotFound())
3046+ return {'public': self.builder.build_public_parts(instance)}
3047+
3048+ def private(self, req, server_id):
3049+ try:
3050+ instance = self.compute_api.get(req.environ['nova.context'], id)
3051+ except nova.exception.NotFound:
3052+ return faults.Fault(exc.HTTPNotFound())
3053+ return {'private': self.builder.build_private_parts(instance)}
3054+
3055+ def show(self, req, server_id, id):
3056+ return faults.Fault(exc.HTTPNotImplemented())
3057+
3058+ def create(self, req, server_id):
3059+ return faults.Fault(exc.HTTPNotImplemented())
3060+
3061+ def delete(self, req, server_id, id):
3062+ return faults.Fault(exc.HTTPNotImplemented())
3063
3064=== modified file 'nova/api/openstack/limits.py'
3065--- nova/api/openstack/limits.py 2011-03-17 20:26:52 +0000
3066+++ nova/api/openstack/limits.py 2011-04-12 11:01:25 +0000
3067@@ -31,8 +31,8 @@
3068 from webob.dec import wsgify
3069
3070 from nova import wsgi
3071+from nova.api.openstack import common
3072 from nova.api.openstack import faults
3073-from nova.wsgi import Controller
3074 from nova.wsgi import Middleware
3075
3076
3077@@ -43,7 +43,7 @@
3078 PER_DAY = 60 * 60 * 24
3079
3080
3081-class LimitsController(Controller):
3082+class LimitsController(common.OpenstackController):
3083 """
3084 Controller for accessing limits in the OpenStack API.
3085 """
3086
3087=== modified file 'nova/api/openstack/server_metadata.py'
3088--- nova/api/openstack/server_metadata.py 2011-03-22 14:01:18 +0000
3089+++ nova/api/openstack/server_metadata.py 2011-04-12 11:01:25 +0000
3090@@ -19,10 +19,11 @@
3091
3092 from nova import compute
3093 from nova import wsgi
3094+from nova.api.openstack import common
3095 from nova.api.openstack import faults
3096
3097
3098-class Controller(wsgi.Controller):
3099+class Controller(common.OpenstackController):
3100 """ The server metadata API controller for the Openstack API """
3101
3102 def __init__(self):
3103
3104=== modified file 'nova/api/openstack/servers.py'
3105--- nova/api/openstack/servers.py 2011-03-28 08:27:17 +0000
3106+++ nova/api/openstack/servers.py 2011-04-12 11:01:25 +0000
3107@@ -44,7 +44,7 @@
3108 FLAGS = flags.FLAGS
3109
3110
3111-class Controller(wsgi.Controller):
3112+class Controller(common.OpenstackController):
3113 """ The Server API controller for the OpenStack API """
3114
3115 _serialization_metadata = {
3116@@ -55,6 +55,13 @@
3117 "imageRef", "vnics"],
3118 "link": ["rel", "type", "href"],
3119 },
3120+ "dict_collections": {
3121+ "metadata": {"item_name": "meta", "item_key": "key"},
3122+ },
3123+ "list_collections": {
3124+ "public": {"item_name": "ip", "item_key": "addr"},
3125+ "private": {"item_name": "ip", "item_key": "addr"},
3126+ },
3127 },
3128 }
3129
3130@@ -63,15 +70,6 @@
3131 self._image_service = utils.import_object(FLAGS.image_service)
3132 super(Controller, self).__init__()
3133
3134- def ips(self, req, id):
3135- try:
3136- instance = self.compute_api.get(req.environ['nova.context'], id)
3137- except exception.NotFound:
3138- return faults.Fault(exc.HTTPNotFound())
3139-
3140- builder = self._get_addresses_view_builder(req)
3141- return builder.build(instance)
3142-
3143 def index(self, req):
3144 """ Returns a list of server names and ids for a given user """
3145 return self._items(req, is_detail=False)
3146@@ -151,16 +149,27 @@
3147
3148 vnics = [env['server']['vnics']] if 'vnics' in env['server'] else None
3149 flavor_id = self._flavor_id_from_req_data(env)
3150+
3151+ if not 'name' in env['server']:
3152+ msg = _("Server name is not defined")
3153+ return exc.HTTPBadRequest(msg)
3154+
3155+ name = env['server']['name']
3156+ self._validate_server_name(name)
3157+ name = name.strip()
3158+
3159 try:
3160+ inst_type = \
3161+ instance_types.get_instance_type_by_flavor_id(flavor_id)
3162 (inst,) = self.compute_api.create(
3163 context,
3164- instance_types.get_by_flavor_id(flavor_id),
3165+ inst_type,
3166 image_id,
3167 vnic_list=vnics,
3168 kernel_id=kernel_id,
3169 ramdisk_id=ramdisk_id,
3170- display_name=env['server']['name'],
3171- display_description=env['server']['name'],
3172+ display_name=name,
3173+ display_description=name,
3174 key_name=key_name,
3175 key_data=key_data,
3176 metadata=metadata,
3177@@ -168,13 +177,12 @@
3178 except quota.QuotaError as error:
3179 self._handle_quota_error(error)
3180
3181- inst['instance_type'] = flavor_id
3182+ inst['instance_type'] = inst_type
3183 inst['image_id'] = requested_image_id
3184
3185 builder = self._get_view_builder(req)
3186 server = builder.build(inst, is_detail=True)
3187- password = "%s%s" % (server['server']['name'][:4],
3188- utils.generate_password(12))
3189+ password = utils.generate_password(16)
3190 server['server']['adminPass'] = password
3191 self.compute_api.set_admin_password(context, server['server']['id'],
3192 password)
3193@@ -248,31 +256,45 @@
3194
3195 ctxt = req.environ['nova.context']
3196 update_dict = {}
3197- if 'adminPass' in inst_dict['server']:
3198- update_dict['admin_pass'] = inst_dict['server']['adminPass']
3199- try:
3200- self.compute_api.set_admin_password(ctxt, id)
3201- except exception.TimeoutException:
3202- return exc.HTTPRequestTimeout()
3203+
3204 if 'name' in inst_dict['server']:
3205- update_dict['display_name'] = inst_dict['server']['name']
3206+ name = inst_dict['server']['name']
3207+ self._validate_server_name(name)
3208+ update_dict['display_name'] = name.strip()
3209+
3210+ self._parse_update(ctxt, id, inst_dict, update_dict)
3211+
3212 try:
3213 self.compute_api.update(ctxt, id, **update_dict)
3214 except exception.NotFound:
3215 return faults.Fault(exc.HTTPNotFound())
3216+
3217 return exc.HTTPNoContent()
3218
3219+ def _validate_server_name(self, value):
3220+ if not isinstance(value, basestring):
3221+ msg = _("Server name is not a string or unicode")
3222+ raise exc.HTTPBadRequest(msg)
3223+
3224+ if value.strip() == '':
3225+ msg = _("Server name is an empty string")
3226+ raise exc.HTTPBadRequest(msg)
3227+
3228+ def _parse_update(self, context, id, inst_dict, update_dict):
3229+ pass
3230+
3231 @scheduler_api.redirect_handler
3232 def action(self, req, id):
3233 """Multi-purpose method used to reboot, rebuild, or
3234 resize a server"""
3235
3236 actions = {
3237- 'reboot': self._action_reboot,
3238- 'resize': self._action_resize,
3239+ 'changePassword': self._action_change_password,
3240+ 'reboot': self._action_reboot,
3241+ 'resize': self._action_resize,
3242 'confirmResize': self._action_confirm_resize,
3243- 'revertResize': self._action_revert_resize,
3244- 'rebuild': self._action_rebuild,
3245+ 'revertResize': self._action_revert_resize,
3246+ 'rebuild': self._action_rebuild,
3247 }
3248
3249 input_dict = self._deserialize(req.body, req.get_content_type())
3250@@ -281,6 +303,9 @@
3251 return actions[key](input_dict, req, id)
3252 return faults.Fault(exc.HTTPNotImplemented())
3253
3254+ def _action_change_password(self, input_dict, req, id):
3255+ return exc.HTTPNotImplemented()
3256+
3257 def _action_confirm_resize(self, input_dict, req, id):
3258 try:
3259 self.compute_api.confirm_resize(req.environ['nova.context'], id)
3260@@ -479,7 +504,7 @@
3261
3262 @scheduler_api.redirect_handler
3263 def get_ajax_console(self, req, id):
3264- """ Returns a url to an instance's ajaxterm console. """
3265+ """Returns a url to an instance's ajaxterm console."""
3266 try:
3267 self.compute_api.get_ajax_console(req.environ['nova.context'],
3268 int(id))
3269@@ -488,6 +513,16 @@
3270 return exc.HTTPAccepted()
3271
3272 @scheduler_api.redirect_handler
3273+ def get_vnc_console(self, req, id):
3274+ """Returns a url to an instance's ajaxterm console."""
3275+ try:
3276+ self.compute_api.get_vnc_console(req.environ['nova.context'],
3277+ int(id))
3278+ except exception.NotFound:
3279+ return faults.Fault(exc.HTTPNotFound())
3280+ return exc.HTTPAccepted()
3281+
3282+ @scheduler_api.redirect_handler
3283 def diagnostics(self, req, id):
3284 """Permit Admins to retrieve server diagnostics."""
3285 ctxt = req.environ["nova.context"]
3286@@ -532,7 +567,7 @@
3287 _("Cannot build from image %(image_id)s, status not active") %
3288 locals())
3289
3290- if image_meta['properties']['disk_format'] != 'ami':
3291+ if image_meta.get('container_format') != 'ami':
3292 return None, None
3293
3294 try:
3295@@ -568,6 +603,14 @@
3296 def _limit_items(self, items, req):
3297 return common.limited(items, req)
3298
3299+ def _parse_update(self, context, server_id, inst_dict, update_dict):
3300+ if 'adminPass' in inst_dict['server']:
3301+ update_dict['admin_pass'] = inst_dict['server']['adminPass']
3302+ try:
3303+ self.compute_api.set_admin_password(context, server_id)
3304+ except exception.TimeoutException:
3305+ return exc.HTTPRequestTimeout()
3306+
3307
3308 class ControllerV11(Controller):
3309 def _image_id_from_req_data(self, data):
3310@@ -591,9 +634,25 @@
3311 def _get_addresses_view_builder(self, req):
3312 return nova.api.openstack.views.addresses.ViewBuilderV11(req)
3313
3314+ def _action_change_password(self, input_dict, req, id):
3315+ context = req.environ['nova.context']
3316+ if (not 'changePassword' in input_dict
3317+ or not 'adminPass' in input_dict['changePassword']):
3318+ msg = _("No adminPass was specified")
3319+ return exc.HTTPBadRequest(msg)
3320+ password = input_dict['changePassword']['adminPass']
3321+ if not isinstance(password, basestring) or password == '':
3322+ msg = _("Invalid adminPass")
3323+ return exc.HTTPBadRequest(msg)
3324+ self.compute_api.set_admin_password(context, id, password)
3325+ return exc.HTTPAccepted()
3326+
3327 def _limit_items(self, items, req):
3328 return common.limited_by_marker(items, req)
3329
3330+ def get_default_xmlns(self, req):
3331+ return common.XML_NS_V11
3332+
3333
3334 class ServerCreateRequestXMLDeserializer(object):
3335 """
3336
3337=== modified file 'nova/api/openstack/shared_ip_groups.py'
3338--- nova/api/openstack/shared_ip_groups.py 2011-03-09 18:10:45 +0000
3339+++ nova/api/openstack/shared_ip_groups.py 2011-04-12 11:01:25 +0000
3340@@ -17,7 +17,7 @@
3341
3342 from webob import exc
3343
3344-from nova import wsgi
3345+from nova.api.openstack import common
3346 from nova.api.openstack import faults
3347
3348
3349@@ -32,7 +32,7 @@
3350 return dict(sharedIpGroups=inst)
3351
3352
3353-class Controller(wsgi.Controller):
3354+class Controller(common.OpenstackController):
3355 """ The Shared IP Groups Controller for the Openstack API """
3356
3357 _serialization_metadata = {
3358@@ -42,11 +42,11 @@
3359
3360 def index(self, req):
3361 """ Returns a list of Shared IP Groups for the user """
3362- return dict(sharedIpGroups=[])
3363+ raise faults.Fault(exc.HTTPNotImplemented())
3364
3365 def show(self, req, id):
3366 """ Shows in-depth information on a specific Shared IP Group """
3367- return _translate_keys({})
3368+ raise faults.Fault(exc.HTTPNotImplemented())
3369
3370 def update(self, req, id):
3371 """ You can't update a Shared IP Group """
3372@@ -58,7 +58,7 @@
3373
3374 def detail(self, req):
3375 """ Returns a complete list of Shared IP Groups """
3376- return _translate_detail_keys({})
3377+ raise faults.Fault(exc.HTTPNotImplemented())
3378
3379 def create(self, req):
3380 """ Creates a new Shared IP group """
3381
3382=== modified file 'nova/api/openstack/users.py'
3383--- nova/api/openstack/users.py 2011-03-16 19:15:57 +0000
3384+++ nova/api/openstack/users.py 2011-04-12 11:01:25 +0000
3385@@ -18,7 +18,6 @@
3386 from nova import exception
3387 from nova import flags
3388 from nova import log as logging
3389-from nova import wsgi
3390 from nova.api.openstack import common
3391 from nova.api.openstack import faults
3392 from nova.auth import manager
3393@@ -35,7 +34,7 @@
3394 admin=user.admin)
3395
3396
3397-class Controller(wsgi.Controller):
3398+class Controller(common.OpenstackController):
3399
3400 _serialization_metadata = {
3401 'application/xml': {
3402
3403=== modified file 'nova/api/openstack/versions.py'
3404--- nova/api/openstack/versions.py 2011-03-22 18:09:23 +0000
3405+++ nova/api/openstack/versions.py 2011-04-12 11:01:25 +0000
3406@@ -15,8 +15,8 @@
3407 # License for the specific language governing permissions and limitations
3408 # under the License.
3409
3410+import webob
3411 import webob.dec
3412-import webob.exc
3413
3414 from nova import wsgi
3415 import nova.api.openstack.views.versions
3416@@ -51,4 +51,10 @@
3417 }
3418
3419 content_type = req.best_match_content_type()
3420- return wsgi.Serializer(metadata).serialize(response, content_type)
3421+ body = wsgi.Serializer(metadata).serialize(response, content_type)
3422+
3423+ response = webob.Response()
3424+ response.content_type = content_type
3425+ response.body = body
3426+
3427+ return response
3428
3429=== modified file 'nova/api/openstack/views/addresses.py'
3430--- nova/api/openstack/views/addresses.py 2011-03-17 07:21:09 +0000
3431+++ nova/api/openstack/views/addresses.py 2011-04-12 11:01:25 +0000
3432@@ -28,10 +28,16 @@
3433
3434 class ViewBuilderV10(ViewBuilder):
3435 def build(self, inst):
3436- private_ips = utils.get_from_path(inst, 'fixed_ip/address')
3437- public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address')
3438+ private_ips = self.build_private_parts(inst)
3439+ public_ips = self.build_public_parts(inst)
3440 return dict(public=public_ips, private=private_ips)
3441
3442+ def build_public_parts(self, inst):
3443+ return utils.get_from_path(inst, 'fixed_ip/floating_ips/address')
3444+
3445+ def build_private_parts(self, inst):
3446+ return utils.get_from_path(inst, 'fixed_ip/address')
3447+
3448
3449 class ViewBuilderV11(ViewBuilder):
3450 def build(self, inst):
3451
3452=== modified file 'nova/api/openstack/views/images.py'
3453--- nova/api/openstack/views/images.py 2011-03-17 07:41:01 +0000
3454+++ nova/api/openstack/views/images.py 2011-04-12 11:01:25 +0000
3455@@ -15,20 +15,100 @@
3456 # License for the specific language governing permissions and limitations
3457 # under the License.
3458
3459-from nova.api.openstack import common
3460+import os.path
3461
3462
3463 class ViewBuilder(object):
3464- def __init__(self):
3465- pass
3466-
3467- def build(self, image_obj):
3468- raise NotImplementedError()
3469-
3470-
3471-class ViewBuilderV11(ViewBuilder):
3472+ """Base class for generating responses to OpenStack API image requests."""
3473+
3474 def __init__(self, base_url):
3475- self.base_url = base_url
3476+ """Initialize new `ViewBuilder`."""
3477+ self._url = base_url
3478+
3479+ def _format_dates(self, image):
3480+ """Update all date fields to ensure standardized formatting."""
3481+ for attr in ['created_at', 'updated_at', 'deleted_at']:
3482+ if image.get(attr) is not None:
3483+ image[attr] = image[attr].strftime('%Y-%m-%dT%H:%M:%SZ')
3484+
3485+ def _format_status(self, image):
3486+ """Update the status field to standardize format."""
3487+ status_mapping = {
3488+ 'pending': 'QUEUED',
3489+ 'decrypting': 'PREPARING',
3490+ 'untarring': 'SAVING',
3491+ 'available': 'ACTIVE',
3492+ 'killed': 'FAILED',
3493+ }
3494+
3495+ try:
3496+ image['status'] = status_mapping[image['status']].upper()
3497+ except KeyError:
3498+ image['status'] = image['status'].upper()
3499
3500 def generate_href(self, image_id):
3501- return "%s/images/%s" % (self.base_url, image_id)
3502+ """Return an href string pointing to this object."""
3503+ return os.path.join(self._url, "images", str(image_id))
3504+
3505+ def build(self, image_obj, detail=False):
3506+ """Return a standardized image structure for display by the API."""
3507+ properties = image_obj.get("properties", {})
3508+
3509+ self._format_dates(image_obj)
3510+
3511+ if "status" in image_obj:
3512+ self._format_status(image_obj)
3513+
3514+ image = {
3515+ "id": image_obj.get("id"),
3516+ "name": image_obj.get("name"),
3517+ }
3518+
3519+ if "instance_id" in properties:
3520+ try:
3521+ image["serverId"] = int(properties["instance_id"])
3522+ except ValueError:
3523+ pass
3524+
3525+ if detail:
3526+ image.update({
3527+ "created": image_obj.get("created_at"),
3528+ "updated": image_obj.get("updated_at"),
3529+ "status": image_obj.get("status"),
3530+ })
3531+
3532+ if image["status"] == "SAVING":
3533+ image["progress"] = 0
3534+
3535+ return image
3536+
3537+
3538+class ViewBuilderV10(ViewBuilder):
3539+ """OpenStack API v1.0 Image Builder"""
3540+ pass
3541+
3542+
3543+class ViewBuilderV11(ViewBuilder):
3544+ """OpenStack API v1.1 Image Builder"""
3545+
3546+ def build(self, image_obj, detail=False):
3547+ """Return a standardized image structure for display by the API."""
3548+ image = ViewBuilder.build(self, image_obj, detail)
3549+ href = self.generate_href(image_obj["id"])
3550+
3551+ image["links"] = [{
3552+ "rel": "self",
3553+ "href": href,
3554+ },
3555+ {
3556+ "rel": "bookmark",
3557+ "type": "application/json",
3558+ "href": href,
3559+ },
3560+ {
3561+ "rel": "bookmark",
3562+ "type": "application/xml",
3563+ "href": href,
3564+ }]
3565+
3566+ return image
3567
3568=== modified file 'nova/api/openstack/views/servers.py'
3569--- nova/api/openstack/views/servers.py 2011-03-24 14:15:50 +0000
3570+++ nova/api/openstack/views/servers.py 2011-04-12 11:01:25 +0000
3571@@ -57,16 +57,16 @@
3572 def _build_detail(self, inst):
3573 """Returns a detailed model of a server."""
3574 power_mapping = {
3575- None: 'build',
3576- power_state.NOSTATE: 'build',
3577- power_state.RUNNING: 'active',
3578- power_state.BLOCKED: 'active',
3579- power_state.SUSPENDED: 'suspended',
3580- power_state.PAUSED: 'paused',
3581- power_state.SHUTDOWN: 'active',
3582- power_state.SHUTOFF: 'active',
3583- power_state.CRASHED: 'error',
3584- power_state.FAILED: 'error'}
3585+ None: 'BUILD',
3586+ power_state.NOSTATE: 'BUILD',
3587+ power_state.RUNNING: 'ACTIVE',
3588+ power_state.BLOCKED: 'ACTIVE',
3589+ power_state.SUSPENDED: 'SUSPENDED',
3590+ power_state.PAUSED: 'PAUSED',
3591+ power_state.SHUTDOWN: 'ACTIVE',
3592+ power_state.SHUTOFF: 'ACTIVE',
3593+ power_state.CRASHED: 'ERROR',
3594+ power_state.FAILED: 'ERROR'}
3595
3596 inst_dict = {
3597 'id': int(inst['id']),
3598@@ -77,12 +77,12 @@
3599 ctxt = nova.context.get_admin_context()
3600 compute_api = nova.compute.API()
3601 if compute_api.has_finished_migration(ctxt, inst['id']):
3602- inst_dict['status'] = 'resize-confirm'
3603+ inst_dict['status'] = 'RESIZE-CONFIRM'
3604
3605 # Return the metadata as a dictionary
3606 metadata = {}
3607 for item in inst.get('metadata', []):
3608- metadata[item['key']] = item['value']
3609+ metadata[item['key']] = str(item['value'])
3610 inst_dict['metadata'] = metadata
3611
3612 inst_dict['hostId'] = ''
3613@@ -115,7 +115,7 @@
3614
3615 def _build_flavor(self, response, inst):
3616 if 'instance_type' in dict(inst):
3617- response['flavorId'] = inst['instance_type']
3618+ response['flavorId'] = inst['instance_type']['flavorid']
3619
3620
3621 class ViewBuilderV11(ViewBuilder):
3622@@ -134,7 +134,7 @@
3623
3624 def _build_flavor(self, response, inst):
3625 if "instance_type" in dict(inst):
3626- flavor_id = inst["instance_type"]
3627+ flavor_id = inst["instance_type"]['flavorid']
3628 flavor_ref = self.flavor_builder.generate_href(flavor_id)
3629 response["flavorRef"] = flavor_ref
3630
3631
3632=== modified file 'nova/api/openstack/zones.py'
3633--- nova/api/openstack/zones.py 2011-03-24 19:04:24 +0000
3634+++ nova/api/openstack/zones.py 2011-04-12 11:01:25 +0000
3635@@ -13,12 +13,10 @@
3636 # License for the specific language governing permissions and limitations
3637 # under the License.
3638
3639-import common
3640-
3641 from nova import db
3642 from nova import flags
3643 from nova import log as logging
3644-from nova import wsgi
3645+from nova.api.openstack import common
3646 from nova.scheduler import api
3647
3648
3649@@ -43,7 +41,7 @@
3650 'deleted', 'deleted_at', 'updated_at'))
3651
3652
3653-class Controller(wsgi.Controller):
3654+class Controller(common.OpenstackController):
3655
3656 _serialization_metadata = {
3657 'application/xml': {
3658
3659=== modified file 'nova/compute/api.py'
3660--- nova/compute/api.py 2011-04-07 11:20:06 +0000
3661+++ nova/compute/api.py 2011-04-12 11:01:25 +0000
3662@@ -38,8 +38,12 @@
3663 from nova.db import base
3664 from nova.network import service as net_service
3665
3666+
3667+LOG = logging.getLogger('nova.compute.api')
3668+
3669+
3670 FLAGS = flags.FLAGS
3671-LOG = logging.getLogger('nova.compute.api')
3672+flags.DECLARE('vncproxy_topic', 'nova.vnc')
3673
3674 def generate_default_hostname(instance_id):
3675 """Default function to generate a hostname given an instance reference."""
3676@@ -107,8 +111,11 @@
3677 """Create the number of instances requested if quota and
3678 other arguments check out ok."""
3679
3680- type_data = instance_types.get_instance_type(instance_type)
3681- num_instances = quota.allowed_instances(context, max_count, type_data)
3682+ if not instance_type:
3683+ instance_type = instance_types.get_default_instance_type()
3684+
3685+ num_instances = quota.allowed_instances(context, max_count,
3686+ instance_type)
3687 if num_instances < min_count:
3688 pid = context.project_id
3689 LOG.warn(_("Quota exceeeded for %(pid)s,"
3690@@ -194,10 +201,10 @@
3691 'user_id': context.user_id,
3692 'project_id': context.project_id,
3693 'launch_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
3694- 'instance_type': instance_type,
3695- 'memory_mb': type_data['memory_mb'],
3696- 'vcpus': type_data['vcpus'],
3697- 'local_gb': type_data['local_gb'],
3698+ 'instance_type_id': instance_type['id'],
3699+ 'memory_mb': instance_type['memory_mb'],
3700+ 'vcpus': instance_type['vcpus'],
3701+ 'local_gb': instance_type['local_gb'],
3702 'display_name': display_name,
3703 'display_description': display_description,
3704 'user_data': user_data or '',
3705@@ -373,11 +380,15 @@
3706 instance_id)
3707 raise
3708
3709- if (instance['state_description'] == 'terminating'):
3710+ if instance['state_description'] == 'terminating':
3711 LOG.warning(_("Instance %s is already being terminated"),
3712 instance_id)
3713 return
3714
3715+ if instance['state_description'] == 'migrating':
3716+ LOG.warning(_("Instance %s is being migrated"), instance_id)
3717+ return
3718+
3719 self.update(context,
3720 instance['id'],
3721 state_description='terminating',
3722@@ -531,8 +542,7 @@
3723 def resize(self, context, instance_id, flavor_id):
3724 """Resize a running instance."""
3725 instance = self.db.instance_get(context, instance_id)
3726- current_instance_type = self.db.instance_type_get_by_name(
3727- context, instance['instance_type'])
3728+ current_instance_type = instance['instance_type']
3729
3730 new_instance_type = self.db.instance_type_get_by_flavor_id(
3731 context, flavor_id)
3732@@ -622,6 +632,25 @@
3733 return {'url': '%s/?token=%s' % (FLAGS.ajax_console_proxy_url,
3734 output['token'])}
3735
3736+ def get_vnc_console(self, context, instance_id):
3737+ """Get a url to a VNC Console."""
3738+ instance = self.get(context, instance_id)
3739+ output = self._call_compute_message('get_vnc_console',
3740+ context,
3741+ instance_id)
3742+ rpc.call(context, '%s' % FLAGS.vncproxy_topic,
3743+ {'method': 'authorize_vnc_console',
3744+ 'args': {'token': output['token'],
3745+ 'host': output['host'],
3746+ 'port': output['port']}})
3747+
3748+ # hostignore and portignore are compatability params for noVNC
3749+ return {'url': '%s/vnc_auto.html?token=%s&host=%s&port=%s' % (
3750+ FLAGS.vncproxy_url,
3751+ output['token'],
3752+ 'hostignore',
3753+ 'portignore')}
3754+
3755 def get_console_output(self, context, instance_id):
3756 """Get console output for an an instance"""
3757 return self._call_compute_message('get_console_output',
3758
3759=== modified file 'nova/compute/instance_types.py'
3760--- nova/compute/instance_types.py 2011-03-03 01:38:42 +0000
3761+++ nova/compute/instance_types.py 2011-04-12 11:01:25 +0000
3762@@ -59,8 +59,8 @@
3763 rxtx_quota=rxtx_quota,
3764 rxtx_cap=rxtx_cap))
3765 except exception.DBError, e:
3766- LOG.exception(_('DB error: %s' % e))
3767- raise exception.ApiError(_("Cannot create instance type: %s" % name))
3768+ LOG.exception(_('DB error: %s') % e)
3769+ raise exception.ApiError(_("Cannot create instance type: %s") % name)
3770
3771
3772 def destroy(name):
3773@@ -72,8 +72,8 @@
3774 try:
3775 db.instance_type_destroy(context.get_admin_context(), name)
3776 except exception.NotFound:
3777- LOG.exception(_('Instance type %s not found for deletion' % name))
3778- raise exception.ApiError(_("Unknown instance type: %s" % name))
3779+ LOG.exception(_('Instance type %s not found for deletion') % name)
3780+ raise exception.ApiError(_("Unknown instance type: %s") % name)
3781
3782
3783 def purge(name):
3784@@ -85,8 +85,8 @@
3785 try:
3786 db.instance_type_purge(context.get_admin_context(), name)
3787 except exception.NotFound:
3788- LOG.exception(_('Instance type %s not found for purge' % name))
3789- raise exception.ApiError(_("Unknown instance type: %s" % name))
3790+ LOG.exception(_('Instance type %s not found for purge') % name)
3791+ raise exception.ApiError(_("Unknown instance type: %s") % name)
3792
3793
3794 def get_all_types(inactive=0):
3795@@ -101,41 +101,43 @@
3796 return get_all_types(context.get_admin_context())
3797
3798
3799-def get_instance_type(name):
3800+def get_default_instance_type():
3801+ name = FLAGS.default_instance_type
3802+ try:
3803+ return get_instance_type_by_name(name)
3804+ except exception.DBError:
3805+ raise exception.ApiError(_("Unknown instance type: %s") % name)
3806+
3807+
3808+def get_instance_type(id):
3809+ """Retrieves single instance type by id"""
3810+ if id is None:
3811+ return get_default_instance_type()
3812+ try:
3813+ ctxt = context.get_admin_context()
3814+ return db.instance_type_get_by_id(ctxt, id)
3815+ except exception.DBError:
3816+ raise exception.ApiError(_("Unknown instance type: %s") % name)
3817+
3818+
3819+def get_instance_type_by_name(name):
3820 """Retrieves single instance type by name"""
3821 if name is None:
3822- return FLAGS.default_instance_type
3823+ return get_default_instance_type()
3824 try:
3825 ctxt = context.get_admin_context()
3826- inst_type = db.instance_type_get_by_name(ctxt, name)
3827- return inst_type
3828+ return db.instance_type_get_by_name(ctxt, name)
3829 except exception.DBError:
3830- raise exception.ApiError(_("Unknown instance type: %s" % name))
3831-
3832-
3833-def get_by_type(instance_type):
3834- """retrieve instance type name"""
3835- if instance_type is None:
3836- return FLAGS.default_instance_type
3837-
3838- try:
3839- ctxt = context.get_admin_context()
3840- inst_type = db.instance_type_get_by_name(ctxt, instance_type)
3841- return inst_type['name']
3842- except exception.DBError, e:
3843- LOG.exception(_('DB error: %s' % e))
3844- raise exception.ApiError(_("Unknown instance type: %s" %\
3845- instance_type))
3846-
3847-
3848-def get_by_flavor_id(flavor_id):
3849- """retrieve instance type's name by flavor_id"""
3850+ raise exception.ApiError(_("Unknown instance type: %s") % name)
3851+
3852+
3853+def get_instance_type_by_flavor_id(flavor_id):
3854+ """retrieve instance type by flavor_id"""
3855 if flavor_id is None:
3856- return FLAGS.default_instance_type
3857+ return get_default_instance_type()
3858 try:
3859 ctxt = context.get_admin_context()
3860- flavor = db.instance_type_get_by_flavor_id(ctxt, flavor_id)
3861- return flavor['name']
3862+ return db.instance_type_get_by_flavor_id(ctxt, flavor_id)
3863 except exception.DBError, e:
3864- LOG.exception(_('DB error: %s' % e))
3865- raise exception.ApiError(_("Unknown flavor: %s" % flavor_id))
3866+ LOG.exception(_('DB error: %s') % e)
3867+ raise exception.ApiError(_("Unknown flavor: %s") % flavor_id)
3868
3869=== modified file 'nova/compute/manager.py'
3870--- nova/compute/manager.py 2011-04-07 01:08:35 +0000
3871+++ nova/compute/manager.py 2011-04-12 11:01:25 +0000
3872@@ -140,12 +140,6 @@
3873 """
3874 self.driver.init_host(host=self.host)
3875
3876- def periodic_tasks(self, context=None):
3877- """Tasks to be run at a periodic interval."""
3878- super(ComputeManager, self).periodic_tasks(context)
3879- if FLAGS.rescue_timeout > 0:
3880- self.driver.poll_rescued_instances(FLAGS.rescue_timeout)
3881-
3882 def _update_state(self, context, instance_id):
3883 """Update the state of an instance from the driver info."""
3884 # FIXME(ja): include other fields from state?
3885@@ -708,6 +702,15 @@
3886
3887 return self.driver.get_ajax_console(instance_ref)
3888
3889+ @exception.wrap_exception
3890+ def get_vnc_console(self, context, instance_id):
3891+ """Return connection information for an vnc console."""
3892+ context = context.elevated()
3893+ LOG.debug(_("instance %s: getting vnc console"), instance_id)
3894+ instance_ref = self.db.instance_get(context, instance_id)
3895+
3896+ return self.driver.get_vnc_console(instance_ref)
3897+
3898 @checks_instance_lock
3899 def attach_volume(self, context, instance_id, volume_id, mountpoint):
3900 """Attach a volume to an instance."""
3901@@ -1014,11 +1017,20 @@
3902 error_list = []
3903
3904 try:
3905+ if FLAGS.rescue_timeout > 0:
3906+ self.driver.poll_rescued_instances(FLAGS.rescue_timeout)
3907+ except Exception as ex:
3908+ LOG.warning(_("Error during poll_rescued_instances: %s"),
3909+ unicode(ex))
3910+ error_list.append(ex)
3911+
3912+ try:
3913 self._poll_instance_states(context)
3914 except Exception as ex:
3915 LOG.warning(_("Error during instance poll: %s"),
3916 unicode(ex))
3917 error_list.append(ex)
3918+
3919 return error_list
3920
3921 def _poll_instance_states(self, context):
3922@@ -1032,16 +1044,41 @@
3923
3924 for db_instance in db_instances:
3925 name = db_instance['name']
3926+ db_state = db_instance['state']
3927 vm_instance = vm_instances.get(name)
3928+
3929 if vm_instance is None:
3930- LOG.info(_("Found instance '%(name)s' in DB but no VM. "
3931- "Setting state to shutoff.") % locals())
3932- vm_state = power_state.SHUTOFF
3933+ # NOTE(justinsb): We have to be very careful here, because a
3934+ # concurrent operation could be in progress (e.g. a spawn)
3935+ if db_state == power_state.NOSTATE:
3936+ # Assume that NOSTATE => spawning
3937+ # TODO(justinsb): This does mean that if we crash during a
3938+ # spawn, the machine will never leave the spawning state,
3939+ # but this is just the way nova is; this function isn't
3940+ # trying to correct that problem.
3941+ # We could have a separate task to correct this error.
3942+ # TODO(justinsb): What happens during a live migration?
3943+ LOG.info(_("Found instance '%(name)s' in DB but no VM. "
3944+ "State=%(db_state)s, so assuming spawn is in "
3945+ "progress.") % locals())
3946+ vm_state = db_state
3947+ else:
3948+ LOG.info(_("Found instance '%(name)s' in DB but no VM. "
3949+ "State=%(db_state)s, so setting state to "
3950+ "shutoff.") % locals())
3951+ vm_state = power_state.SHUTOFF
3952 else:
3953 vm_state = vm_instance.state
3954 vms_not_found_in_db.remove(name)
3955
3956- db_state = db_instance['state']
3957+ if db_instance['state_description'] == 'migrating':
3958+ # A situation which db record exists, but no instance"
3959+ # sometimes occurs while live-migration at src compute,
3960+ # this case should be ignored.
3961+ LOG.debug(_("Ignoring %(name)s, as it's currently being "
3962+ "migrated.") % locals())
3963+ continue
3964+
3965 if vm_state != db_state:
3966 LOG.info(_("DB/VM state mismatch. Changing state from "
3967 "'%(db_state)s' to '%(vm_state)s'") % locals())
3968@@ -1049,12 +1086,8 @@
3969 db_instance['id'],
3970 vm_state)
3971
3972- if vm_state == power_state.SHUTOFF:
3973- # TODO(soren): This is what the compute manager does when you
3974- # terminate an instance. At some point I figure we'll have a
3975- # "terminated" state and some sort of cleanup job that runs
3976- # occasionally, cleaning them out.
3977- self.db.instance_destroy(context, db_instance['id'])
3978+ # NOTE(justinsb): We no longer auto-remove SHUTOFF instances
3979+ # It's quite hard to get them back when we do.
3980
3981 # Are there VMs not in the DB?
3982 for vm_not_found_in_db in vms_not_found_in_db:
3983
3984=== modified file 'nova/crypto.py'
3985--- nova/crypto.py 2011-03-23 04:31:50 +0000
3986+++ nova/crypto.py 2011-04-12 11:01:25 +0000
3987@@ -215,9 +215,12 @@
3988
3989 def _ensure_project_folder(project_id):
3990 if not os.path.exists(ca_path(project_id)):
3991+ geninter_sh_path = os.path.join(os.path.dirname(__file__),
3992+ 'CA',
3993+ 'geninter.sh')
3994 start = os.getcwd()
3995 os.chdir(ca_folder())
3996- utils.execute('sh', 'geninter.sh', project_id,
3997+ utils.execute('sh', geninter_sh_path, project_id,
3998 _project_cert_subject(project_id))
3999 os.chdir(start)
4000
4001@@ -227,13 +230,16 @@
4002 csr_fn = os.path.join(project_folder, "server.csr")
4003 crt_fn = os.path.join(project_folder, "server.crt")
4004
4005+ genvpn_sh_path = os.path.join(os.path.dirname(__file__),
4006+ 'CA',
4007+ 'genvpn.sh')
4008 if os.path.exists(crt_fn):
4009 return
4010 _ensure_project_folder(project_id)
4011 start = os.getcwd()
4012 os.chdir(ca_folder())
4013 # TODO(vish): the shell scripts could all be done in python
4014- utils.execute('sh', 'genvpn.sh',
4015+ utils.execute('sh', genvpn_sh_path,
4016 project_id, _vpn_cert_subject(project_id))
4017 with open(csr_fn, "r") as csrfile:
4018 csr_text = csrfile.read()
4019@@ -263,6 +269,8 @@
4020 LOG.debug(_("Flags path: %s"), ca_folder)
4021 start = os.getcwd()
4022 # Change working dir to CA
4023+ if not os.path.exists(ca_folder):
4024+ os.makedirs(ca_folder)
4025 os.chdir(ca_folder)
4026 utils.execute('openssl', 'ca', '-batch', '-out', outbound, '-config',
4027 './openssl.cnf', '-infiles', inbound)
4028
4029=== modified file 'nova/db/api.py'
4030--- nova/db/api.py 2011-03-28 08:27:17 +0000
4031+++ nova/db/api.py 2011-04-12 11:01:25 +0000
4032@@ -1153,6 +1153,11 @@
4033 return IMPL.instance_type_get_all(context, inactive)
4034
4035
4036+def instance_type_get_by_id(context, id):
4037+ """Get instance type by id"""
4038+ return IMPL.instance_type_get_by_id(context, id)
4039+
4040+
4041 def instance_type_get_by_name(context, name):
4042 """Get instance type by name"""
4043 return IMPL.instance_type_get_by_name(context, name)
4044
4045=== modified file 'nova/db/sqlalchemy/api.py'
4046--- nova/db/sqlalchemy/api.py 2011-03-28 08:27:17 +0000
4047+++ nova/db/sqlalchemy/api.py 2011-04-12 11:01:25 +0000
4048@@ -660,7 +660,9 @@
4049 filter(models.FixedIp.instance_id != None).\
4050 filter_by(allocated=0).\
4051 update({'instance_id': None,
4052- 'leased': 0})
4053+ 'leased': 0,
4054+ 'updated_at': datetime.datetime.utcnow()},
4055+ synchronize_session='fetch')
4056 return result
4057
4058
4059@@ -829,6 +831,7 @@
4060 options(joinedload('volumes')).\
4061 options(joinedload_all('fixed_ip.network')).\
4062 options(joinedload('metadata')).\
4063+ options(joinedload('instance_type')).\
4064 filter_by(id=instance_id).\
4065 filter_by(deleted=can_read_deleted(context)).\
4066 first()
4067@@ -838,6 +841,7 @@
4068 options(joinedload_all('security_groups.rules')).\
4069 options(joinedload('volumes')).\
4070 options(joinedload('metadata')).\
4071+ options(joinedload('instance_type')).\
4072 filter_by(project_id=context.project_id).\
4073 filter_by(id=instance_id).\
4074 filter_by(deleted=False).\
4075@@ -857,6 +861,7 @@
4076 options(joinedload_all('fixed_ip.floating_ips')).\
4077 options(joinedload('security_groups')).\
4078 options(joinedload_all('fixed_ip.network')).\
4079+ options(joinedload('instance_type')).\
4080 filter_by(deleted=can_read_deleted(context)).\
4081 all()
4082
4083@@ -868,6 +873,7 @@
4084 options(joinedload_all('fixed_ip.floating_ips')).\
4085 options(joinedload('security_groups')).\
4086 options(joinedload_all('fixed_ip.network')).\
4087+ options(joinedload('instance_type')).\
4088 filter_by(deleted=can_read_deleted(context)).\
4089 filter_by(user_id=user_id).\
4090 all()
4091@@ -880,6 +886,7 @@
4092 options(joinedload_all('fixed_ip.floating_ips')).\
4093 options(joinedload('security_groups')).\
4094 options(joinedload_all('fixed_ip.network')).\
4095+ options(joinedload('instance_type')).\
4096 filter_by(host=host).\
4097 filter_by(deleted=can_read_deleted(context)).\
4098 all()
4099@@ -894,6 +901,7 @@
4100 options(joinedload_all('fixed_ip.floating_ips')).\
4101 options(joinedload('security_groups')).\
4102 options(joinedload_all('fixed_ip.network')).\
4103+ options(joinedload('instance_type')).\
4104 filter_by(project_id=project_id).\
4105 filter_by(deleted=can_read_deleted(context)).\
4106 all()
4107@@ -908,6 +916,7 @@
4108 options(joinedload_all('fixed_ip.floating_ips')).\
4109 options(joinedload('security_groups')).\
4110 options(joinedload_all('fixed_ip.network')).\
4111+ options(joinedload('instance_type')).\
4112 filter_by(reservation_id=reservation_id).\
4113 filter_by(deleted=can_read_deleted(context)).\
4114 all()
4115@@ -916,6 +925,7 @@
4116 options(joinedload_all('fixed_ip.floating_ips')).\
4117 options(joinedload('security_groups')).\
4118 options(joinedload_all('fixed_ip.network')).\
4119+ options(joinedload('instance_type')).\
4120 filter_by(project_id=context.project_id).\
4121 filter_by(reservation_id=reservation_id).\
4122 filter_by(deleted=False).\
4123@@ -928,6 +938,7 @@
4124 return session.query(models.Instance).\
4125 options(joinedload_all('fixed_ip.floating_ips')).\
4126 options(joinedload('security_groups')).\
4127+ options(joinedload('instance_type')).\
4128 filter_by(project_id=project_id).\
4129 filter_by(image_id=FLAGS.vpn_image_id).\
4130 filter_by(deleted=can_read_deleted(context)).\
4131@@ -2428,6 +2439,19 @@
4132
4133
4134 @require_context
4135+def instance_type_get_by_id(context, id):
4136+ """Returns a dict describing specific instance_type"""
4137+ session = get_session()
4138+ inst_type = session.query(models.InstanceTypes).\
4139+ filter_by(id=id).\
4140+ first()
4141+ if not inst_type:
4142+ raise exception.NotFound(_("No instance type with id %s") % id)
4143+ else:
4144+ return dict(inst_type)
4145+
4146+
4147+@require_context
4148 def instance_type_get_by_name(context, name):
4149 """Returns a dict describing specific instance_type"""
4150 session = get_session()
4151
4152=== added file 'nova/db/sqlalchemy/migrate_repo/versions/014_add_instance_type_id_to_instances.py'
4153--- nova/db/sqlalchemy/migrate_repo/versions/014_add_instance_type_id_to_instances.py 1970-01-01 00:00:00 +0000
4154+++ nova/db/sqlalchemy/migrate_repo/versions/014_add_instance_type_id_to_instances.py 2011-04-12 11:01:25 +0000
4155@@ -0,0 +1,84 @@
4156+# vim: tabstop=4 shiftwidth=4 softtabstop=4
4157+
4158+# Copyright 2010 OpenStack LLC.
4159+#
4160+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4161+# not use this file except in compliance with the License. You may obtain
4162+# a copy of the License at
4163+#
4164+# http://www.apache.org/licenses/LICENSE-2.0
4165+#
4166+# Unless required by applicable law or agreed to in writing, software
4167+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
4168+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
4169+# License for the specific language governing permissions and limitations
4170+# under the License.
4171+
4172+from sqlalchemy import *
4173+from sqlalchemy.sql import text
4174+from migrate import *
4175+
4176+#from nova import log as logging
4177+
4178+
4179+meta = MetaData()
4180+
4181+
4182+c_instance_type = Column('instance_type',
4183+ String(length=255, convert_unicode=False,
4184+ assert_unicode=None, unicode_error=None,
4185+ _warn_on_bytestring=False),
4186+ nullable=True)
4187+
4188+c_instance_type_id = Column('instance_type_id',
4189+ String(length=255, convert_unicode=False,
4190+ assert_unicode=None, unicode_error=None,
4191+ _warn_on_bytestring=False),
4192+ nullable=True)
4193+
4194+instance_types = Table('instance_types', meta,
4195+ Column('id', Integer(), primary_key=True, nullable=False),
4196+ Column('name',
4197+ String(length=255, convert_unicode=False, assert_unicode=None,
4198+ unicode_error=None, _warn_on_bytestring=False),
4199+ unique=True))
4200+
4201+
4202+def upgrade(migrate_engine):
4203+ # Upgrade operations go here. Don't create your own engine;
4204+ # bind migrate_engine to your metadata
4205+ meta.bind = migrate_engine
4206+
4207+ instances = Table('instances', meta, autoload=True,
4208+ autoload_with=migrate_engine)
4209+
4210+ instances.create_column(c_instance_type_id)
4211+
4212+ recs = migrate_engine.execute(instance_types.select())
4213+ for row in recs:
4214+ type_id = row[0]
4215+ type_name = row[1]
4216+ migrate_engine.execute(instances.update()\
4217+ .where(instances.c.instance_type == type_name)\
4218+ .values(instance_type_id=type_id))
4219+
4220+ instances.c.instance_type.drop()
4221+
4222+
4223+def downgrade(migrate_engine):
4224+ meta.bind = migrate_engine
4225+
4226+ instances = Table('instances', meta, autoload=True,
4227+ autoload_with=migrate_engine)
4228+
4229+ instances.create_column(c_instance_type)
4230+
4231+ recs = migrate_engine.execute(instance_types.select())
4232+ for row in recs:
4233+ type_id = row[0]
4234+ type_name = row[1]
4235+ migrate_engine.execute(instances.update()\
4236+ .where(instances.c.instance_type_id == type_id)\
4237+ .values(instance_type=type_name))
4238+
4239+ instances.c.instance_type_id.drop()
4240
4241=== removed file 'nova/db/sqlalchemy/migrate_repo/versions/014_diablo.py'
4242--- nova/db/sqlalchemy/migrate_repo/versions/014_diablo.py 2011-03-28 08:27:17 +0000
4243+++ nova/db/sqlalchemy/migrate_repo/versions/014_diablo.py 1970-01-01 00:00:00 +0000
4244@@ -1,92 +0,0 @@
4245-# vim: tabstop=4 shiftwidth=4 softtabstop=4
4246-
4247-# Copyright (c) 2011 NTT.
4248-# All Rights Reserved.
4249-#
4250-# Licensed under the Apache License, Version 2.0 (the "License"); you may
4251-# not use this file except in compliance with the License. You may obtain
4252-# a copy of the License at
4253-#
4254-# http://www.apache.org/licenses/LICENSE-2.0
4255-#
4256-# Unless required by applicable law or agreed to in writing, software
4257-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
4258-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
4259-# License for the specific language governing permissions and limitations
4260-# under the License.
4261-
4262-from sqlalchemy import *
4263-from migrate import *
4264-
4265-from nova import log as logging
4266-
4267-meta = MetaData()
4268-
4269-# Just for the ForeignKey and column creation to succeed, these are not the
4270-# actual definitions of instances or services.
4271-instances = Table('instances', meta,
4272- Column('id', Integer(), primary_key=True, nullable=False),
4273- )
4274-
4275-#
4276-# New Tables
4277-#
4278-instance_virtual_nic_association = Table(
4279- 'instance_virtual_nic_association', meta,
4280- Column('created_at', DateTime(timezone=False)),
4281- Column('updated_at', DateTime(timezone=False)),
4282- Column('deleted_at', DateTime(timezone=False)),
4283- Column('deleted', Boolean(create_constraint=True, name=None)),
4284- Column('id', Integer(), primary_key=True, nullable=False),
4285- Column('virtual_nic_id',
4286- String(length=255, convert_unicode=False, assert_unicode=None,
4287- unicode_error=None, _warn_on_bytestring=False),
4288- nullable=False),
4289- Column('instance_id', Integer(), ForeignKey('instances.id'),
4290- nullable=False),
4291- )
4292-
4293-project_network_service_association = Table(
4294- 'project_network_service_association', meta,
4295- Column('created_at', DateTime(timezone=False)),
4296- Column('updated_at', DateTime(timezone=False)),
4297- Column('deleted_at', DateTime(timezone=False)),
4298- Column('deleted', Boolean(create_constraint=True, name=None)),
4299- Column('id', Integer(), primary_key=True, nullable=False),
4300- Column('project_id',
4301- String(length=255, convert_unicode=False, assert_unicode=None,
4302- unicode_error=None, _warn_on_bytestring=False),
4303- nullable=False),
4304- Column('network_service',
4305- String(length=255, convert_unicode=False, assert_unicode=None,
4306- unicode_error=None, _warn_on_bytestring=False),
4307- nullable=False),
4308- UniqueConstraint('project_id')
4309- )
4310-
4311-def upgrade(migrate_engine):
4312- # Upgrade operations go here. Don't create your own engine;
4313- # bind migrate_engine to your metadata
4314- meta.bind = migrate_engine
4315-
4316- for table in (instance_virtual_nic_association,
4317- project_network_service_association):
4318- try:
4319- table.create()
4320- except Exception:
4321- logging.info(repr(table))
4322- logging.exception('Exception while creating table')
4323- raise
4324-
4325-def downgrade(migrate_engine):
4326- # Operations to reverse the above upgrade go here.
4327- meta.bind = migrate_engine
4328-
4329- for table in (project_network_service_association,
4330- instance_virtual_nic_association):
4331- try:
4332- table.drop()
4333- except Exception:
4334- logging.info(repr(table))
4335- logging.exception('Exception while dropping table')
4336- raise
4337
4338=== added file 'nova/db/sqlalchemy/migrate_repo/versions/015_diablo.py'
4339--- nova/db/sqlalchemy/migrate_repo/versions/015_diablo.py 1970-01-01 00:00:00 +0000
4340+++ nova/db/sqlalchemy/migrate_repo/versions/015_diablo.py 2011-04-12 11:01:25 +0000
4341@@ -0,0 +1,93 @@
4342+# vim: tabstop=4 shiftwidth=4 softtabstop=4
4343+
4344+# Copyright (c) 2011 NTT.
4345+# All Rights Reserved.
4346+#
4347+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4348+# not use this file except in compliance with the License. You may obtain
4349+# a copy of the License at
4350+#
4351+# http://www.apache.org/licenses/LICENSE-2.0
4352+#
4353+# Unless required by applicable law or agreed to in writing, software
4354+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
4355+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
4356+# License for the specific language governing permissions and limitations
4357+# under the License.
4358+
4359+from sqlalchemy import *
4360+from migrate import *
4361+
4362+from nova import log as logging
4363+
4364+meta = MetaData()
4365+
4366+# Just for the ForeignKey and column creation to succeed, these are not the
4367+# actual definitions of instances or services.
4368+instances = Table('instances', meta,
4369+ Column('id', Integer(), primary_key=True, nullable=False),
4370+ )
4371+
4372+#
4373+# New Tables
4374+#
4375+instance_virtual_nic_association = Table(
4376+ 'instance_virtual_nic_association', meta,
4377+ Column('created_at', DateTime(timezone=False)),
4378+ Column('updated_at', DateTime(timezone=False)),
4379+ Column('deleted_at', DateTime(timezone=False)),
4380+ Column('deleted', Boolean(create_constraint=True, name=None)),
4381+ Column('id', Integer(), primary_key=True, nullable=False),
4382+ Column('virtual_nic_id',
4383+ String(length=255, convert_unicode=False, assert_unicode=None,
4384+ unicode_error=None, _warn_on_bytestring=False),
4385+ nullable=False),
4386+ Column('instance_id', Integer(), ForeignKey('instances.id'),
4387+ nullable=False),
4388+ )
4389+
4390+project_network_service_association = Table(
4391+ 'project_network_service_association', meta,
4392+ Column('created_at', DateTime(timezone=False)),
4393+ Column('updated_at', DateTime(timezone=False)),
4394+ Column('deleted_at', DateTime(timezone=False)),
4395+ Column('deleted', Boolean(create_constraint=True, name=None)),
4396+ Column('id', Integer(), primary_key=True, nullable=False),
4397+ Column('project_id',
4398+ String(length=255, convert_unicode=False, assert_unicode=None,
4399+ unicode_error=None, _warn_on_bytestring=False),
4400+ nullable=False),
4401+ Column('network_service',
4402+ String(length=255, convert_unicode=False, assert_unicode=None,
4403+ unicode_error=None, _warn_on_bytestring=False),
4404+ nullable=False),
4405+ UniqueConstraint('project_id')
4406+ )
4407+
4408+def upgrade(migrate_engine):
4409+ # Upgrade operations go here. Don't create your own engine;
4410+ # bind migrate_engine to your metadata
4411+ meta.bind = migrate_engine
4412+ return
4413+
4414+ for table in (instance_virtual_nic_association,
4415+ project_network_service_association):
4416+ try:
4417+ table.create()
4418+ except Exception:
4419+ logging.info(repr(table))
4420+ logging.exception('Exception while creating table')
4421+ raise
4422+
4423+def downgrade(migrate_engine):
4424+ # Operations to reverse the above upgrade go here.
4425+ meta.bind = migrate_engine
4426+
4427+ for table in (project_network_service_association,
4428+ instance_virtual_nic_association):
4429+ try:
4430+ table.drop()
4431+ except Exception:
4432+ logging.info(repr(table))
4433+ logging.exception('Exception while dropping table')
4434+ raise
4435
4436=== modified file 'nova/db/sqlalchemy/models.py'
4437--- nova/db/sqlalchemy/models.py 2011-03-28 08:27:17 +0000
4438+++ nova/db/sqlalchemy/models.py 2011-04-12 11:01:25 +0000
4439@@ -209,7 +209,7 @@
4440 hostname = Column(String(255))
4441 host = Column(String(255)) # , ForeignKey('hosts.id'))
4442
4443- instance_type = Column(String(255))
4444+ instance_type_id = Column(String(255))
4445
4446 user_data = Column(Text)
4447
4448@@ -268,6 +268,12 @@
4449 rxtx_quota = Column(Integer, nullable=False, default=0)
4450 rxtx_cap = Column(Integer, nullable=False, default=0)
4451
4452+ instances = relationship(Instance,
4453+ backref=backref('instance_type', uselist=False),
4454+ foreign_keys=id,
4455+ primaryjoin='and_(Instance.instance_type_id == '
4456+ 'InstanceTypes.id)')
4457+
4458
4459 class Volume(BASE, NovaBase):
4460 """Represents a block storage device that can be attached to a vm."""
4461
4462=== added file 'nova/image/fake.py'
4463--- nova/image/fake.py 1970-01-01 00:00:00 +0000
4464+++ nova/image/fake.py 2011-04-12 11:01:25 +0000
4465@@ -0,0 +1,113 @@
4466+# vim: tabstop=4 shiftwidth=4 softtabstop=4
4467+
4468+# Copyright 2011 Justin Santa Barbara
4469+# All Rights Reserved.
4470+#
4471+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4472+# not use this file except in compliance with the License. You may obtain
4473+# a copy of the License at
4474+#
4475+# http://www.apache.org/licenses/LICENSE-2.0
4476+#
4477+# Unless required by applicable law or agreed to in writing, software
4478+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
4479+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
4480+# License for the specific language governing permissions and limitations
4481+# under the License.
4482+"""Implementation of an fake image service"""
4483+
4484+import copy
4485+import datetime
4486+
4487+from nova import exception
4488+from nova import flags
4489+from nova import log as logging
4490+from nova.image import service
4491+
4492+
4493+LOG = logging.getLogger('nova.image.fake')
4494+
4495+
4496+FLAGS = flags.FLAGS
4497+
4498+
4499+class FakeImageService(service.BaseImageService):
4500+ """Mock (fake) image service for unit testing."""
4501+
4502+ def __init__(self):
4503+ self.images = {}
4504+ # NOTE(justinsb): The OpenStack API can't upload an image?
4505+ # So, make sure we've got one..
4506+ timestamp = datetime.datetime(2011, 01, 01, 01, 02, 03)
4507+ image = {'id': '123456',
4508+ 'name': 'fakeimage123456',
4509+ 'created_at': timestamp,
4510+ 'updated_at': timestamp,
4511+ 'status': 'active',
4512+ 'container_format': 'ami',
4513+ 'disk_format': 'raw',
4514+ 'properties': {'kernel_id': FLAGS.null_kernel,
4515+ 'ramdisk_id': FLAGS.null_kernel}
4516+ }
4517+ self.create(None, image)
4518+ super(FakeImageService, self).__init__()
4519+
4520+ def index(self, context):
4521+ """Returns list of images."""
4522+ return copy.deepcopy(self.images.values())
4523+
4524+ def detail(self, context):
4525+ """Return list of detailed image information."""
4526+ return copy.deepcopy(self.images.values())
4527+
4528+ def show(self, context, image_id):
4529+ """Get data about specified image.
4530+
4531+ Returns a dict containing image data for the given opaque image id.
4532+
4533+ """
4534+ image_id = int(image_id)
4535+ image = self.images.get(image_id)
4536+ if image:
4537+ return copy.deepcopy(image)
4538+ LOG.warn("Unable to find image id %s. Have images: %s",
4539+ image_id, self.images)
4540+ raise exception.NotFound
4541+
4542+ def create(self, context, data):
4543+ """Store the image data and return the new image id.
4544+
4545+ :raises Duplicate if the image already exist.
4546+
4547+ """
4548+ image_id = int(data['id'])
4549+ if self.images.get(image_id):
4550+ raise exception.Duplicate()
4551+
4552+ self.images[image_id] = copy.deepcopy(data)
4553+
4554+ def update(self, context, image_id, data):
4555+ """Replace the contents of the given image with the new data.
4556+
4557+ :raises NotFound if the image does not exist.
4558+
4559+ """
4560+ image_id = int(image_id)
4561+ if not self.images.get(image_id):
4562+ raise exception.NotFound
4563+ self.images[image_id] = copy.deepcopy(data)
4564+
4565+ def delete(self, context, image_id):
4566+ """Delete the given image.
4567+
4568+ :raises NotFound if the image does not exist.
4569+
4570+ """
4571+ image_id = int(image_id)
4572+ removed = self.images.pop(image_id, None)
4573+ if not removed:
4574+ raise exception.NotFound
4575+
4576+ def delete_all(self):
4577+ """Clears out all images."""
4578+ self.images.clear()
4579
4580=== modified file 'nova/image/glance.py'
4581--- nova/image/glance.py 2011-03-24 21:50:27 +0000
4582+++ nova/image/glance.py 2011-04-12 11:01:25 +0000
4583@@ -151,6 +151,8 @@
4584
4585 :raises NotFound if the image does not exist.
4586 """
4587+ # NOTE(vish): show is to check if image is available
4588+ self.show(context, image_id)
4589 try:
4590 image_meta = self.client.update_image(image_id, image_meta, data)
4591 except glance_exception.NotFound:
4592@@ -165,6 +167,8 @@
4593
4594 :raises NotFound if the image does not exist.
4595 """
4596+ # NOTE(vish): show is to check if image is available
4597+ self.show(context, image_id)
4598 try:
4599 result = self.client.delete_image(image_id)
4600 except glance_exception.NotFound:
4601@@ -186,33 +190,6 @@
4602 image_meta = _convert_timestamps_to_datetimes(image_meta)
4603 return image_meta
4604
4605- @staticmethod
4606- def _is_image_available(context, image_meta):
4607- """
4608- Images are always available if they are public or if the user is an
4609- admin.
4610-
4611- Otherwise, we filter by project_id (if present) and then fall-back to
4612- images owned by user.
4613- """
4614- # FIXME(sirp): We should be filtering by user_id on the Glance side
4615- # for security; however, we can't do that until we get authn/authz
4616- # sorted out. Until then, filtering in Nova.
4617- if image_meta['is_public'] or context.is_admin:
4618- return True
4619-
4620- properties = image_meta['properties']
4621-
4622- if context.project_id and ('project_id' in properties):
4623- return str(properties['project_id']) == str(project_id)
4624-
4625- try:
4626- user_id = properties['user_id']
4627- except KeyError:
4628- return False
4629-
4630- return str(user_id) == str(context.user_id)
4631-
4632
4633 # utility functions
4634 def _convert_timestamps_to_datetimes(image_meta):
4635@@ -220,7 +197,7 @@
4636 Returns image with known timestamp fields converted to datetime objects
4637 """
4638 for attr in ['created_at', 'updated_at', 'deleted_at']:
4639- if image_meta.get(attr) is not None:
4640+ if image_meta.get(attr):
4641 image_meta[attr] = _parse_glance_iso8601_timestamp(
4642 image_meta[attr])
4643 return image_meta
4644@@ -230,8 +207,13 @@
4645 """
4646 Parse a subset of iso8601 timestamps into datetime objects
4647 """
4648- GLANCE_FMT = "%Y-%m-%dT%H:%M:%S"
4649- ISO_FMT = "%Y-%m-%dT%H:%M:%S.%f"
4650- # FIXME(sirp): Glance is not returning in ISO format, we should fix Glance
4651- # to do so, and then switch to parsing it here
4652- return datetime.datetime.strptime(timestamp, GLANCE_FMT)
4653+ iso_formats = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"]
4654+
4655+ for iso_format in iso_formats:
4656+ try:
4657+ return datetime.datetime.strptime(timestamp, iso_format)
4658+ except ValueError:
4659+ pass
4660+
4661+ raise ValueError(_("%(timestamp)s does not follow any of the "
4662+ "signatures: %(ISO_FORMATS)s") % locals())
4663
4664=== modified file 'nova/image/local.py'
4665--- nova/image/local.py 2011-03-23 05:50:53 +0000
4666+++ nova/image/local.py 2011-04-12 11:01:25 +0000
4667@@ -84,7 +84,10 @@
4668 def show(self, context, image_id):
4669 try:
4670 with open(self._path_to(image_id)) as metadata_file:
4671- return json.load(metadata_file)
4672+ image_meta = json.load(metadata_file)
4673+ if not self._is_image_available(context, image_meta):
4674+ raise exception.NotFound
4675+ return image_meta
4676 except (IOError, ValueError):
4677 raise exception.NotFound
4678
4679@@ -119,10 +122,15 @@
4680 image_path = self._path_to(image_id, None)
4681 if not os.path.exists(image_path):
4682 os.mkdir(image_path)
4683- return self.update(context, image_id, metadata, data)
4684+ return self._store(context, image_id, metadata, data)
4685
4686 def update(self, context, image_id, metadata, data=None):
4687 """Replace the contents of the given image with the new data."""
4688+ # NOTE(vish): show is to check if image is available
4689+ self.show(context, image_id)
4690+ return self._store(context, image_id, metadata, data)
4691+
4692+ def _store(self, context, image_id, metadata, data=None):
4693 metadata['id'] = image_id
4694 try:
4695 if data:
4696@@ -140,9 +148,11 @@
4697
4698 def delete(self, context, image_id):
4699 """Delete the given image.
4700- Raises OSError if the image does not exist.
4701+ Raises NotFound if the image does not exist.
4702
4703 """
4704+ # NOTE(vish): show is to check if image is available
4705+ self.show(context, image_id)
4706 try:
4707 shutil.rmtree(self._path_to(image_id, None))
4708 except (IOError, ValueError):
4709
4710=== modified file 'nova/image/s3.py'
4711--- nova/image/s3.py 2011-03-14 17:59:41 +0000
4712+++ nova/image/s3.py 2011-04-12 11:01:25 +0000
4713@@ -31,6 +31,7 @@
4714
4715 import boto.s3.connection
4716
4717+from nova import crypto
4718 from nova import exception
4719 from nova import flags
4720 from nova import utils
4721@@ -45,6 +46,7 @@
4722
4723
4724 class S3ImageService(service.BaseImageService):
4725+ """Wraps an existing image service to support s3 based register"""
4726 def __init__(self, service=None, *args, **kwargs):
4727 if service == None:
4728 service = utils.import_object(FLAGS.image_service)
4729@@ -57,52 +59,23 @@
4730 return image
4731
4732 def delete(self, context, image_id):
4733- # FIXME(vish): call to show is to check filter
4734- self.show(context, image_id)
4735 self.service.delete(context, image_id)
4736
4737 def update(self, context, image_id, metadata, data=None):
4738- # FIXME(vish): call to show is to check filter
4739- self.show(context, image_id)
4740 image = self.service.update(context, image_id, metadata, data)
4741 return image
4742
4743 def index(self, context):
4744- images = self.service.index(context)
4745- # FIXME(vish): index doesn't filter so we do it manually
4746- return self._filter(context, images)
4747+ return self.service.index(context)
4748
4749 def detail(self, context):
4750- images = self.service.detail(context)
4751- # FIXME(vish): detail doesn't filter so we do it manually
4752- return self._filter(context, images)
4753-
4754- @classmethod
4755- def _is_visible(cls, context, image):
4756- return (context.is_admin
4757- or context.project_id == image['properties']['owner_id']
4758- or image['properties']['is_public'] == 'True')
4759-
4760- @classmethod
4761- def _filter(cls, context, images):
4762- filtered = []
4763- for image in images:
4764- if not cls._is_visible(context, image):
4765- continue
4766- filtered.append(image)
4767- return filtered
4768+ return self.service.detail(context)
4769
4770 def show(self, context, image_id):
4771- image = self.service.show(context, image_id)
4772- if not self._is_visible(context, image):
4773- raise exception.NotFound
4774- return image
4775+ return self.service.show(context, image_id)
4776
4777 def show_by_name(self, context, name):
4778- image = self.service.show_by_name(context, name)
4779- if not self._is_visible(context, image):
4780- raise exception.NotFound
4781- return image
4782+ return self.service.show(context, name)
4783
4784 @staticmethod
4785 def _conn(context):
4786@@ -166,7 +139,7 @@
4787 arch = 'x86_64'
4788
4789 properties = metadata['properties']
4790- properties['owner_id'] = context.project_id
4791+ properties['project_id'] = context.project_id
4792 properties['architecture'] = arch
4793
4794 if kernel_id:
4795@@ -175,8 +148,6 @@
4796 if ramdisk_id:
4797 properties['ramdisk_id'] = ec2utils.ec2_id_to_id(ramdisk_id)
4798
4799- properties['is_public'] = False
4800- properties['type'] = image_type
4801 metadata.update({'disk_format': image_format,
4802 'container_format': image_format,
4803 'status': 'queued',
4804@@ -210,7 +181,7 @@
4805
4806 # FIXME(vish): grab key from common service so this can run on
4807 # any host.
4808- cloud_pk = os.path.join(FLAGS.ca_path, "private/cakey.pem")
4809+ cloud_pk = crypto.key_path(context.project_id)
4810
4811 decrypted_filename = os.path.join(image_path, 'image.tar.gz')
4812 self._decrypt_image(encrypted_filename, encrypted_key,
4813
4814=== modified file 'nova/image/service.py'
4815--- nova/image/service.py 2011-03-24 21:13:55 +0000
4816+++ nova/image/service.py 2011-04-12 11:01:25 +0000
4817@@ -136,6 +136,33 @@
4818 """
4819 raise NotImplementedError
4820
4821+ @staticmethod
4822+ def _is_image_available(context, image_meta):
4823+ """
4824+ Images are always available if they are public or if the user is an
4825+ admin.
4826+
4827+ Otherwise, we filter by project_id (if present) and then fall-back to
4828+ images owned by user.
4829+ """
4830+ # FIXME(sirp): We should be filtering by user_id on the Glance side
4831+ # for security; however, we can't do that until we get authn/authz
4832+ # sorted out. Until then, filtering in Nova.
4833+ if image_meta['is_public'] or context.is_admin:
4834+ return True
4835+
4836+ properties = image_meta['properties']
4837+
4838+ if context.project_id and ('project_id' in properties):
4839+ return str(properties['project_id']) == str(context.project_id)
4840+
4841+ try:
4842+ user_id = properties['user_id']
4843+ except KeyError:
4844+ return False
4845+
4846+ return str(user_id) == str(context.user_id)
4847+
4848 @classmethod
4849 def _translate_to_base(cls, metadata):
4850 """Return a metadata dictionary that is BaseImageService compliant.
4851
4852=== modified file 'nova/network/api.py'
4853--- nova/network/api.py 2011-02-22 23:50:42 +0000
4854+++ nova/network/api.py 2011-04-12 11:01:25 +0000
4855@@ -66,6 +66,21 @@
4856 if isinstance(fixed_ip, str) or isinstance(fixed_ip, unicode):
4857 fixed_ip = self.db.fixed_ip_get_by_address(context, fixed_ip)
4858 floating_ip = self.db.floating_ip_get_by_address(context, floating_ip)
4859+ # Check if the floating ip address is allocated
4860+ if floating_ip['project_id'] is None:
4861+ raise exception.ApiError(_("Address (%s) is not allocated") %
4862+ floating_ip['address'])
4863+ # Check if the floating ip address is allocated to the same project
4864+ if floating_ip['project_id'] != context.project_id:
4865+ LOG.warn(_("Address (%(address)s) is not allocated to your "
4866+ "project (%(project)s)"),
4867+ {'address': floating_ip['address'],
4868+ 'project': context.project_id})
4869+ raise exception.ApiError(_("Address (%(address)s) is not "
4870+ "allocated to your project"
4871+ "(%(project)s)") %
4872+ {'address': floating_ip['address'],
4873+ 'project': context.project_id})
4874 # NOTE(vish): Perhaps we should just pass this on to compute and
4875 # let compute communicate with network.
4876 host = fixed_ip['network']['host']
4877
4878=== modified file 'nova/network/linux_net.py'
4879--- nova/network/linux_net.py 2011-04-03 19:04:27 +0000
4880+++ nova/network/linux_net.py 2011-04-12 11:01:25 +0000
4881@@ -391,6 +391,12 @@
4882 'dev', FLAGS.public_interface)
4883
4884
4885+def ensure_metadata_ip():
4886+ """Sets up local metadata ip"""
4887+ _execute('sudo', 'ip', 'addr', 'add', '169.254.169.254/32',
4888+ 'scope', 'link', 'dev', 'lo', check_exit_code=False)
4889+
4890+
4891 def ensure_vlan_forward(public_ip, port, private_ip):
4892 """Sets up forwarding rules for vlan"""
4893 iptables_manager.ipv4['filter'].add_rule("FORWARD",
4894@@ -442,6 +448,7 @@
4895 return interface
4896
4897
4898+@utils.synchronized('ensure_bridge', external=True)
4899 def ensure_bridge(bridge, interface, net_attrs=None):
4900 """Create a bridge unless it already exists.
4901
4902@@ -495,6 +502,8 @@
4903 fields = line.split()
4904 if fields and fields[0] == "0.0.0.0" and fields[-1] == interface:
4905 gateway = fields[1]
4906+ _execute('sudo', 'route', 'del', 'default', 'gw', gateway,
4907+ 'dev', interface, check_exit_code=False)
4908 out, err = _execute('sudo', 'ip', 'addr', 'show', 'dev', interface,
4909 'scope', 'global')
4910 for line in out.split("\n"):
4911@@ -504,7 +513,7 @@
4912 _execute(*_ip_bridge_cmd('del', params, fields[-1]))
4913 _execute(*_ip_bridge_cmd('add', params, bridge))
4914 if gateway:
4915- _execute('sudo', 'route', 'add', '0.0.0.0', 'gw', gateway)
4916+ _execute('sudo', 'route', 'add', 'default', 'gw', gateway)
4917 out, err = _execute('sudo', 'brctl', 'addif', bridge, interface,
4918 check_exit_code=False)
4919
4920
4921=== modified file 'nova/network/manager.py'
4922--- nova/network/manager.py 2011-04-03 19:04:27 +0000
4923+++ nova/network/manager.py 2011-04-12 11:01:25 +0000
4924@@ -126,6 +126,7 @@
4925 standalone service.
4926 """
4927 self.driver.init_host()
4928+ self.driver.ensure_metadata_ip()
4929 # Set up networking for the projects for which we're already
4930 # the designated network host.
4931 ctxt = context.get_admin_context()
4932
4933=== added file 'nova/network/xenapi_net.py'
4934--- nova/network/xenapi_net.py 1970-01-01 00:00:00 +0000
4935+++ nova/network/xenapi_net.py 2011-04-12 11:01:25 +0000
4936@@ -0,0 +1,85 @@
4937+# vim: tabstop=4 shiftwidth=4 softtabstop=4
4938+
4939+# Copyright (c) 2011 Citrix Systems, Inc.
4940+# Copyright 2011 OpenStack LLC.
4941+#
4942+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4943+# not use this file except in compliance with the License. You may obtain
4944+# a copy of the License at
4945+#
4946+# http://www.apache.org/licenses/LICENSE-2.0
4947+#
4948+# Unless required by applicable law or agreed to in writing, software
4949+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
4950+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
4951+# License for the specific language governing permissions and limitations
4952+# under the License.
4953+
4954+"""
4955+Implements vlans, bridges, and iptables rules using linux utilities.
4956+"""
4957+
4958+import os
4959+
4960+from nova import db
4961+from nova import exception
4962+from nova import flags
4963+from nova import log as logging
4964+from nova import utils
4965+from nova.virt.xenapi_conn import XenAPISession
4966+from nova.virt.xenapi import network_utils
4967+
4968+LOG = logging.getLogger("nova.xenapi_net")
4969+
4970+FLAGS = flags.FLAGS
4971+
4972+
4973+def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None):
4974+ """Create a vlan and bridge unless they already exist."""
4975+ # Open xenapi session
4976+ LOG.debug("ENTERING ensure_vlan_bridge in xenapi net")
4977+ url = FLAGS.xenapi_connection_url
4978+ username = FLAGS.xenapi_connection_username
4979+ password = FLAGS.xenapi_connection_password
4980+ session = XenAPISession(url, username, password)
4981+ # Check whether bridge already exists
4982+ # Retrieve network whose name_label is "bridge"
4983+ network_ref = network_utils.NetworkHelper.find_network_with_name_label(
4984+ session,
4985+ bridge)
4986+ if network_ref == None:
4987+ # If bridge does not exists
4988+ # 1 - create network
4989+ description = "network for nova bridge %s" % bridge
4990+ network_rec = {'name_label': bridge,
4991+ 'name_description': description,
4992+ 'other_config': {}}
4993+ network_ref = session.call_xenapi('network.create', network_rec)
4994+ # 2 - find PIF for VLAN
4995+ expr = 'field "device" = "%s" and \
4996+ field "VLAN" = "-1"' % FLAGS.vlan_interface
4997+ pifs = session.call_xenapi('PIF.get_all_records_where', expr)
4998+ pif_ref = None
4999+ # Multiple PIF are ok: we are dealing with a pool
5000+ if len(pifs) == 0:
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to status/vote changes: