Merge lp:~midokura/nova/network-service into lp:~ntt-pf-lab/nova/network-service
- network-service
- Merge into 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 |
Related bugs: | |
Related blueprints: |
Refactor Networking
(Essential)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
NTT PF Lab. | Pending | ||
Review via email: mp+57303@code.launchpad.net |
Commit message
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.