Merge lp:~tr3buchet/rackspace-nova/rs-nova into lp:rackspace-nova
- rs-nova
- Merge into development
Proposed by
Trey Morris
Status: | Merged |
---|---|
Approved by: | Trey Morris |
Approved revision: | not available |
Merge reported by: | Trey Morris |
Merged at revision: | not available |
Proposed branch: | lp:~tr3buchet/rackspace-nova/rs-nova |
Merge into: | lp:rackspace-nova |
Diff against target: |
39242 lines (+20555/-7198) 245 files modified
.mailmap (+5/-0) Authors (+16/-2) MANIFEST.in (+1/-0) bin/instance-usage-audit (+116/-0) bin/nova-ajax-console-proxy (+3/-2) bin/nova-api (+34/-31) bin/nova-dhcpbridge (+9/-6) bin/nova-direct-api (+5/-2) bin/nova-manage (+206/-71) bin/nova-objectstore (+5/-2) bin/nova-vncproxy (+20/-2) contrib/nova.sh (+13/-7) doc/.autogenerated (+0/-283) doc/build/html/.buildinfo (+0/-4) doc/source/devref/distributed_scheduler.rst (+188/-0) doc/source/devref/index.rst (+1/-0) doc/source/devref/multinic.rst (+39/-0) doc/source/devref/zone.rst (+5/-5) doc/source/man/novamanage.rst (+2/-2) doc/source/runnova/managing.users.rst (+3/-3) nova/__init__.py (+5/-0) nova/api/direct.py (+9/-5) nova/api/ec2/__init__.py (+11/-0) nova/api/ec2/admin.py (+64/-4) nova/api/ec2/apirequest.py (+3/-75) nova/api/ec2/cloud.py (+240/-78) nova/api/ec2/ec2utils.py (+94/-0) nova/api/ec2/metadatarequesthandler.py (+13/-2) nova/api/openstack/__init__.py (+44/-49) nova/api/openstack/accounts.py (+21/-12) nova/api/openstack/auth.py (+12/-10) nova/api/openstack/backup_schedules.py (+22/-12) nova/api/openstack/common.py (+54/-51) nova/api/openstack/consoles.py (+8/-11) nova/api/openstack/contrib/__init__.py (+1/-1) nova/api/openstack/contrib/flavorextraspecs.py (+126/-0) nova/api/openstack/contrib/floating_ips.py (+173/-0) nova/api/openstack/contrib/hosts.py (+114/-0) nova/api/openstack/contrib/multinic.py (+125/-0) nova/api/openstack/contrib/volumes.py (+14/-17) nova/api/openstack/create_instance_helper.py (+354/-0) nova/api/openstack/extensions.py (+67/-47) nova/api/openstack/faults.py (+23/-16) nova/api/openstack/flavors.py (+25/-15) nova/api/openstack/image_metadata.py (+75/-13) nova/api/openstack/images.py (+303/-60) nova/api/openstack/ips.py (+34/-27) nova/api/openstack/limits.py (+40/-17) nova/api/openstack/notes.txt (+0/-3) nova/api/openstack/ratelimiting/__init__.py (+1/-1) nova/api/openstack/server_metadata.py (+58/-20) nova/api/openstack/servers.py (+187/-348) nova/api/openstack/shared_ip_groups.py (+14/-26) nova/api/openstack/users.py (+28/-17) nova/api/openstack/versions.py (+22/-26) nova/api/openstack/views/addresses.py (+6/-4) nova/api/openstack/views/flavors.py (+9/-7) nova/api/openstack/views/images.py (+40/-27) nova/api/openstack/views/limits.py (+23/-10) nova/api/openstack/views/servers.py (+28/-17) nova/api/openstack/wsgi.py (+473/-0) nova/api/openstack/zones.py (+97/-29) nova/auth/fakeldap.py (+24/-0) nova/auth/ldapdriver.py (+113/-14) nova/auth/manager.py (+19/-13) nova/auth/novarc.template (+4/-1) nova/compute/api.py (+502/-172) nova/compute/instance_types.py (+1/-1) nova/compute/manager.py (+431/-174) nova/compute/monitor.py (+2/-1) nova/compute/utils.py (+29/-0) nova/console/manager.py (+2/-2) nova/console/vmrc.py (+1/-1) nova/console/vmrc_manager.py (+2/-2) nova/context.py (+0/-1) nova/crypto.py (+2/-1) nova/db/api.py (+259/-45) nova/db/sqlalchemy/api.py (+993/-294) nova/db/sqlalchemy/migrate_repo/versions/016_make_quotas_key_and_value.py (+6/-6) nova/db/sqlalchemy/migrate_repo/versions/018_rename_server_management_url.py (+5/-24) nova/db/sqlalchemy/migrate_repo/versions/019_add_volume_snapshot_support.py (+70/-0) nova/db/sqlalchemy/migrate_repo/versions/020_add_snapshot_id_to_volumes.py (+47/-0) nova/db/sqlalchemy/migrate_repo/versions/021_rename_image_ids.py (+40/-0) nova/db/sqlalchemy/migrate_repo/versions/022_set_engine_mysql_innodb.py (+65/-0) nova/db/sqlalchemy/migrate_repo/versions/023_add_vm_mode_to_instances.py (+45/-0) nova/db/sqlalchemy/migrate_repo/versions/024_add_block_device_mapping.py (+87/-0) nova/db/sqlalchemy/migrate_repo/versions/025_add_uuid_to_instances.py (+43/-0) nova/db/sqlalchemy/migrate_repo/versions/026_add_agent_table.py (+73/-0) nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py (+74/-0) nova/db/sqlalchemy/migrate_repo/versions/028_add_instance_type_extra_specs.py (+67/-0) nova/db/sqlalchemy/migrate_repo/versions/029_add_zone_weight_offsets.py (+38/-0) nova/db/sqlalchemy/migrate_repo/versions/030_multi_nic.py (+125/-0) nova/db/sqlalchemy/migrate_repo/versions/031_fk_fixed_ips_virtual_interface_id.py (+56/-0) nova/db/sqlalchemy/migrate_repo/versions/031_sqlite_downgrade.sql (+48/-0) nova/db/sqlalchemy/migrate_repo/versions/031_sqlite_upgrade.sql (+48/-0) nova/db/sqlalchemy/models.py (+172/-45) nova/exception.py (+155/-29) nova/fakerabbit.py (+23/-8) nova/flags.py (+14/-6) nova/image/__init__.py (+98/-0) nova/image/fake.py (+89/-24) nova/image/glance.py (+41/-12) nova/image/local.py (+0/-165) nova/image/s3.py (+70/-29) nova/image/service.py (+2/-2) nova/log.py (+21/-0) nova/network/api.py (+82/-14) nova/network/linux_net.py (+15/-6) nova/network/manager.py (+558/-286) nova/network/vmwareapi_net.py (+5/-7) nova/network/xenapi_net.py (+6/-4) nova/notifier/api.py (+16/-5) nova/notifier/test_notifier.py (+28/-0) nova/objectstore/s3server.py (+1/-1) nova/quota.py (+78/-52) nova/rpc.py (+215/-71) nova/scheduler/api.py (+131/-49) nova/scheduler/driver.py (+11/-9) nova/scheduler/host_filter.py (+98/-37) nova/scheduler/least_cost.py (+174/-0) nova/scheduler/manager.py (+21/-2) nova/scheduler/simple.py (+12/-7) nova/scheduler/zone_aware_scheduler.py (+314/-52) nova/scheduler/zone_manager.py (+56/-8) nova/service.py (+142/-99) nova/test.py (+41/-30) nova/tests/__init__.py (+17/-9) nova/tests/api/__init__.py (+19/-0) nova/tests/api/openstack/__init__.py (+3/-0) nova/tests/api/openstack/contrib/__init__.py (+15/-0) nova/tests/api/openstack/contrib/test_floating_ips.py (+189/-0) nova/tests/api/openstack/contrib/test_multinic_xs.py (+115/-0) nova/tests/api/openstack/extensions/foxinsocks.py (+1/-3) nova/tests/api/openstack/extensions/test_flavors_extra_specs.py (+198/-0) nova/tests/api/openstack/fakes.py (+57/-17) nova/tests/api/openstack/test_api.py (+21/-0) nova/tests/api/openstack/test_auth.py (+65/-0) nova/tests/api/openstack/test_common.py (+137/-83) nova/tests/api/openstack/test_extensions.py (+15/-2) nova/tests/api/openstack/test_flavors.py (+31/-35) nova/tests/api/openstack/test_image_metadata.py (+161/-10) nova/tests/api/openstack/test_images.py (+1114/-152) nova/tests/api/openstack/test_limits.py (+140/-5) nova/tests/api/openstack/test_server_metadata.py (+78/-2) nova/tests/api/openstack/test_servers.py (+398/-127) nova/tests/api/openstack/test_wsgi.py (+346/-0) nova/tests/api/openstack/test_zones.py (+10/-6) nova/tests/api/test_wsgi.py (+0/-189) nova/tests/db/fakes.py (+334/-35) nova/tests/fake_flags.py (+14/-14) nova/tests/glance/stubs.py (+10/-9) nova/tests/image/__init__.py (+3/-0) nova/tests/image/test_glance.py (+2/-4) nova/tests/integrated/__init__.py (+2/-0) nova/tests/integrated/api/client.py (+20/-15) nova/tests/integrated/integrated_helpers.py (+21/-23) nova/tests/integrated/test_servers.py (+106/-0) nova/tests/integrated/test_xml.py (+2/-2) nova/tests/network/__init__.py (+0/-67) nova/tests/network/base.py (+0/-155) nova/tests/real_flags.py (+0/-26) nova/tests/scheduler/__init__.py (+19/-0) nova/tests/scheduler/test_host_filter.py (+240/-0) nova/tests/scheduler/test_least_cost_scheduler.py (+145/-0) nova/tests/scheduler/test_scheduler.py (+43/-21) nova/tests/scheduler/test_zone_aware_scheduler.py (+245/-35) nova/tests/test_adminapi.py (+107/-0) nova/tests/test_api.py (+24/-1) nova/tests/test_auth.py (+11/-0) nova/tests/test_cloud.py (+640/-62) nova/tests/test_compute.py (+139/-20) nova/tests/test_console.py (+0/-3) nova/tests/test_crypto.py (+83/-0) nova/tests/test_direct.py (+22/-21) nova/tests/test_exception.py (+63/-0) nova/tests/test_flags.py (+14/-0) nova/tests/test_flat_network.py (+0/-161) nova/tests/test_host_filter.py (+0/-208) nova/tests/test_hosts.py (+102/-0) nova/tests/test_instance_types_extra_specs.py (+165/-0) nova/tests/test_iptables_network.py (+164/-0) nova/tests/test_libvirt.py (+380/-69) nova/tests/test_metadata.py (+76/-0) nova/tests/test_middleware.py (+0/-1) nova/tests/test_misc.py (+13/-0) nova/tests/test_network.py (+267/-160) nova/tests/test_notifier.py (+22/-3) nova/tests/test_objectstore.py (+7/-2) nova/tests/test_quota.py (+83/-29) nova/tests/test_rpc.py (+107/-11) nova/tests/test_service.py (+83/-6) nova/tests/test_utils.py (+31/-0) nova/tests/test_vlan_network.py (+0/-242) nova/tests/test_vmwareapi.py (+276/-252) nova/tests/test_volume.py (+49/-2) nova/tests/test_wsgi.py (+95/-0) nova/tests/test_xenapi.py (+260/-66) nova/tests/test_zones.py (+175/-0) nova/tests/vmwareapi/db_fakes.py (+2/-2) nova/tests/xenapi/stubs.py (+67/-22) nova/twistd.py (+3/-3) nova/utils.py (+106/-21) nova/virt/connection.py (+1/-1) nova/virt/driver.py (+16/-4) nova/virt/fake.py (+63/-1) nova/virt/hyperv.py (+12/-3) nova/virt/images.py (+4/-12) nova/virt/libvirt.xml.template (+10/-1) nova/virt/libvirt/connection.py (+132/-731) nova/virt/libvirt/firewall.py (+820/-0) nova/virt/libvirt/netutils.py (+102/-0) nova/virt/vmwareapi/vm_util.py (+5/-1) nova/virt/vmwareapi/vmops.py (+19/-13) nova/virt/vmwareapi/vmware_images.py (+7/-15) nova/virt/vmwareapi_conn.py (+5/-1) nova/virt/xenapi/fake.py (+8/-8) nova/virt/xenapi/vm_utils.py (+183/-85) nova/virt/xenapi/vmops.py (+304/-113) nova/virt/xenapi/volume_utils.py (+22/-21) nova/virt/xenapi_conn.py (+10/-6) nova/vnc/__init__.py (+2/-0) nova/volume/api.py (+68/-4) nova/volume/driver.py (+116/-28) nova/volume/manager.py (+58/-5) nova/wsgi.py (+144/-333) plugins/xenserver/networking/etc/init.d/openvswitch-nova (+96/-0) plugins/xenserver/networking/etc/sysconfig/openvswitch-nova (+1/-0) plugins/xenserver/networking/etc/udev/rules.d/xen-openvswitch-nova.rules (+3/-0) plugins/xenserver/networking/etc/xensource/scripts/novalib.py (+40/-0) plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_base_flows.py (+62/-0) plugins/xenserver/networking/etc/xensource/scripts/ovs_configure_vif_flows.py (+180/-0) plugins/xenserver/networking/etc/xensource/scripts/vif_5.6-fp1.patch (+1/-5) plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py (+11/-19) plugins/xenserver/xenapi/contrib/build-rpm.sh (+20/-0) plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec (+37/-0) plugins/xenserver/xenapi/etc/xapi.d/plugins/agent (+34/-2) plugins/xenserver/xenapi/etc/xapi.d/plugins/glance (+74/-28) plugins/xenserver/xenapi/etc/xapi.d/plugins/migration (+1/-1) plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost (+44/-7) run_tests.py (+79/-20) run_tests.sh (+25/-8) tools/clean-vlans (+3/-3) tools/install_venv.py (+1/-1) tools/nova-debug (+13/-8) tools/pip-requires (+6/-7) |
To merge this branch: | bzr merge lp:~tr3buchet/rackspace-nova/rs-nova |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ozone | Pending | ||
Review via email: mp+68016@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 '.mailmap' |
2 | --- .mailmap 2011-05-11 19:16:37 +0000 |
3 | +++ .mailmap 2011-07-14 20:24:25 +0000 |
4 | @@ -47,3 +47,8 @@ |
5 | <vishvananda@gmail.com> <root@mirror.nasanebula.net> |
6 | <vishvananda@gmail.com> <root@ubuntu> |
7 | <vishvananda@gmail.com> <vishvananda@yahoo.com> |
8 | +<ilyaalekseyev@acm.org> <ialekseev@griddynamics.com> |
9 | +<ilyaalekseyev@acm.org> <ilya@oscloud.ru> |
10 | +<reldan@oscloud.ru> <enugaev@griddynamics.com> |
11 | +<kshileev@gmail.com> <kshileev@griddynamics.com> |
12 | +<nsokolov@griddynamics.com> <nsokolov@griddynamics.net> |
13 | |
14 | === modified file 'Authors' |
15 | --- Authors 2011-05-20 06:51:29 +0000 |
16 | +++ Authors 2011-07-14 20:24:25 +0000 |
17 | @@ -1,4 +1,6 @@ |
18 | Alex Meade <alex.meade@rackspace.com> |
19 | +Alexander Sakhnov <asakhnov@mirantis.com> |
20 | +Andrey Brindeyev <abrindeyev@griddynamics.com> |
21 | Andy Smith <code@term.ie> |
22 | Andy Southgate <andy.southgate@citrix.com> |
23 | Anne Gentle <anne@openstack.org> |
24 | @@ -16,18 +18,22 @@ |
25 | Chuck Short <zulcss@ubuntu.com> |
26 | Cory Wright <corywright@gmail.com> |
27 | Dan Prince <dan.prince@rackspace.com> |
28 | +Dave Walker <DaveWalker@ubuntu.com> |
29 | David Pravec <David.Pravec@danix.org> |
30 | Dean Troyer <dtroyer@gmail.com> |
31 | +Devendra Modium <dmodium@isi.edu> |
32 | Devin Carlen <devin.carlen@gmail.com> |
33 | Ed Leafe <ed@leafe.com> |
34 | -Eldar Nugaev <enugaev@griddynamics.com> |
35 | +Eldar Nugaev <reldan@oscloud.ru> |
36 | Eric Day <eday@oddments.org> |
37 | Eric Windisch <eric@cloudscaling.com> |
38 | Ewan Mellor <ewan.mellor@citrix.com> |
39 | Gabe Westmaas <gabe.westmaas@rackspace.com> |
40 | Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp> |
41 | Hisaki Ohara <hisaki.ohara@intel.com> |
42 | -Ilya Alekseyev <ialekseev@griddynamics.com> |
43 | +Ilya Alekseyev <ilyaalekseyev@acm.org> |
44 | +Isaku Yamahata <yamahata@valinux.co.jp> |
45 | +Jason Cannavale <jason.cannavale@rackspace.com> |
46 | Jason Koelker <jason@koelker.net> |
47 | Jay Pipes <jaypipes@gmail.com> |
48 | Jesse Andrews <anotherjesse@gmail.com> |
49 | @@ -39,6 +45,7 @@ |
50 | John Tran <jtran@attinteractive.com> |
51 | Jonathan Bryce <jbryce@jbryce.com> |
52 | Jordan Rinke <jordan@openstack.org> |
53 | +Joseph Suh <jsuh@isi.edu> |
54 | Josh Durgin <joshd@hq.newdream.net> |
55 | Josh Kearney <josh@jk0.org> |
56 | Josh Kleinpeter <josh@kleinpeter.org> |
57 | @@ -49,6 +56,7 @@ |
58 | Ken Pepple <ken.pepple@gmail.com> |
59 | Kevin Bringard <kbringard@attinteractive.com> |
60 | Kevin L. Mitchell <kevin.mitchell@rackspace.com> |
61 | +Kirill Shileev <kshileev@gmail.com> |
62 | Koji Iida <iida.koji@lab.ntt.co.jp> |
63 | Lorin Hochstein <lorin@isi.edu> |
64 | Lvov Maxim <usrleon@gmail.com> |
65 | @@ -56,14 +64,18 @@ |
66 | Masanori Itoh <itoumsn@nttdata.co.jp> |
67 | Matt Dietz <matt.dietz@rackspace.com> |
68 | Michael Gundlach <michael.gundlach@rackspace.com> |
69 | +Mike Scherbakov <mihgen@gmail.com> |
70 | +Mohammed Naser <mnaser@vexxhost.com> |
71 | Monsyne Dragon <mdragon@rackspace.com> |
72 | Monty Taylor <mordred@inaugust.com> |
73 | MORITA Kazutaka <morita.kazutaka@gmail.com> |
74 | Muneyuki Noguchi <noguchimn@nttdata.co.jp> |
75 | Nachi Ueno <ueno.nachi@lab.ntt.co.jp> |
76 | Naveed Massjouni <naveedm9@gmail.com> |
77 | +Nikolay Sokolov <nsokolov@griddynamics.com> |
78 | Nirmal Ranganathan <nirmal.ranganathan@rackspace.com> |
79 | Paul Voccio <paul@openstack.org> |
80 | +Renuka Apte <renuka.apte@citrix.com> |
81 | Ricardo Carrillo Cruz <emaildericky@gmail.com> |
82 | Rick Clark <rick@openstack.org> |
83 | Rick Harris <rconradharris@gmail.com> |
84 | @@ -73,6 +85,7 @@ |
85 | Salvatore Orlando <salvatore.orlando@eu.citrix.com> |
86 | Sandy Walsh <sandy.walsh@rackspace.com> |
87 | Sateesh Chodapuneedi <sateesh.chodapuneedi@citrix.com> |
88 | +Scott Moser <smoser@ubuntu.com> |
89 | Soren Hansen <soren.hansen@rackspace.com> |
90 | Thierry Carrez <thierry@openstack.org> |
91 | Todd Willey <todd@ansolabs.com> |
92 | @@ -80,6 +93,7 @@ |
93 | Tushar Patil <tushar.vitthal.patil@gmail.com> |
94 | Vasiliy Shlykov <vash@vasiliyshlykov.org> |
95 | Vishvananda Ishaya <vishvananda@gmail.com> |
96 | +Vivek Y S <vivek.ys@gmail.com> |
97 | William Wolf <throughnothing@gmail.com> |
98 | Yoshiaki Tamura <yoshi@midokura.jp> |
99 | Youcef Laribi <Youcef.Laribi@eu.citrix.com> |
100 | |
101 | === modified file 'MANIFEST.in' |
102 | --- MANIFEST.in 2011-05-20 19:21:04 +0000 |
103 | +++ MANIFEST.in 2011-07-14 20:24:25 +0000 |
104 | @@ -23,6 +23,7 @@ |
105 | include nova/console/xvp.conf.template |
106 | include nova/db/sqlalchemy/migrate_repo/migrate.cfg |
107 | include nova/db/sqlalchemy/migrate_repo/README |
108 | +include nova/db/sqlalchemy/migrate_repo/versions/*.sql |
109 | include nova/virt/interfaces.template |
110 | include nova/virt/libvirt*.xml.template |
111 | include nova/virt/cpuinfo.xml.template |
112 | |
113 | === added file 'bin/instance-usage-audit' |
114 | --- bin/instance-usage-audit 1970-01-01 00:00:00 +0000 |
115 | +++ bin/instance-usage-audit 2011-07-14 20:24:25 +0000 |
116 | @@ -0,0 +1,116 @@ |
117 | +#!/usr/bin/env python |
118 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
119 | + |
120 | +# Copyright (c) 2011 Openstack, LLC. |
121 | +# All Rights Reserved. |
122 | +# |
123 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
124 | +# not use this file except in compliance with the License. You may obtain |
125 | +# a copy of the License at |
126 | +# |
127 | +# http://www.apache.org/licenses/LICENSE-2.0 |
128 | +# |
129 | +# Unless required by applicable law or agreed to in writing, software |
130 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
131 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
132 | +# License for the specific language governing permissions and limitations |
133 | +# under the License. |
134 | + |
135 | +"""Cron script to generate usage notifications for instances neither created |
136 | + nor destroyed in a given time period. |
137 | + |
138 | + Together with the notifications generated by compute on instance |
139 | + create/delete/resize, over that ime period, this allows an external |
140 | + system consuming usage notification feeds to calculate instance usage |
141 | + for each tenant. |
142 | + |
143 | + Time periods are specified like so: |
144 | + <number>[mdy] |
145 | + |
146 | + 1m = previous month. If the script is run April 1, it will generate usages |
147 | + for March 1 thry March 31. |
148 | + 3m = 3 previous months. |
149 | + 90d = previous 90 days. |
150 | + 1y = previous year. If run on Jan 1, it generates usages for |
151 | + Jan 1 thru Dec 31 of the previous year. |
152 | +""" |
153 | + |
154 | +import datetime |
155 | +import gettext |
156 | +import os |
157 | +import sys |
158 | +import time |
159 | + |
160 | +# If ../nova/__init__.py exists, add ../ to Python search path, so that |
161 | +# it will override what happens to be installed in /usr/(local/)lib/python... |
162 | +POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), |
163 | + os.pardir, |
164 | + os.pardir)) |
165 | +if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'nova', '__init__.py')): |
166 | + sys.path.insert(0, POSSIBLE_TOPDIR) |
167 | + |
168 | +gettext.install('nova', unicode=1) |
169 | + |
170 | + |
171 | +from nova import context |
172 | +from nova import db |
173 | +from nova import exception |
174 | +from nova import flags |
175 | +from nova import log as logging |
176 | +from nova import utils |
177 | + |
178 | +from nova.notifier import api as notifier_api |
179 | + |
180 | +FLAGS = flags.FLAGS |
181 | +flags.DEFINE_string('instance_usage_audit_period', '1m', |
182 | + 'time period to generate instance usages for.') |
183 | + |
184 | + |
185 | +def time_period(period): |
186 | + today = datetime.date.today() |
187 | + unit = period[-1] |
188 | + if unit not in 'mdy': |
189 | + raise ValueError('Time period must be m, d, or y') |
190 | + n = int(period[:-1]) |
191 | + if unit == 'm': |
192 | + year = today.year - (n // 12) |
193 | + n = n % 12 |
194 | + if n >= today.month: |
195 | + year -= 1 |
196 | + month = 12 + (today.month - n) |
197 | + else: |
198 | + month = today.month - n |
199 | + begin = datetime.datetime(day=1, month=month, year=year) |
200 | + end = datetime.datetime(day=1, month=today.month, year=today.year) |
201 | + |
202 | + elif unit == 'y': |
203 | + begin = datetime.datetime(day=1, month=1, year=today.year - n) |
204 | + end = datetime.datetime(day=1, month=1, year=today.year) |
205 | + |
206 | + elif unit == 'd': |
207 | + b = today - datetime.timedelta(days=n) |
208 | + begin = datetime.datetime(day=b.day, month=b.month, year=b.year) |
209 | + end = datetime.datetime(day=today.day, |
210 | + month=today.month, |
211 | + year=today.year) |
212 | + |
213 | + return (begin, end) |
214 | + |
215 | +if __name__ == '__main__': |
216 | + utils.default_flagfile() |
217 | + flags.FLAGS(sys.argv) |
218 | + logging.setup() |
219 | + begin, end = time_period(FLAGS.instance_usage_audit_period) |
220 | + print "Creating usages for %s until %s" % (str(begin), str(end)) |
221 | + instances = db.instance_get_active_by_window(context.get_admin_context(), |
222 | + begin, |
223 | + end) |
224 | + print "%s instances" % len(instances) |
225 | + for instance_ref in instances: |
226 | + usage_info = utils.usage_from_instance(instance_ref, |
227 | + audit_period_begining=str(begin), |
228 | + audit_period_ending=str(end)) |
229 | + notifier_api.notify('compute.%s' % FLAGS.host, |
230 | + 'compute.instance.exists', |
231 | + notifier_api.INFO, |
232 | + usage_info) |
233 | |
234 | === modified file 'bin/nova-ajax-console-proxy' |
235 | --- bin/nova-ajax-console-proxy 2011-03-29 23:13:09 +0000 |
236 | +++ bin/nova-ajax-console-proxy 2011-07-14 20:24:25 +0000 |
237 | @@ -137,8 +137,9 @@ |
238 | utils.default_flagfile() |
239 | FLAGS(sys.argv) |
240 | logging.setup() |
241 | - server = wsgi.Server() |
242 | + acp_port = FLAGS.ajax_console_proxy_port |
243 | acp = AjaxConsoleProxy() |
244 | acp.register_listeners() |
245 | - server.start(acp, FLAGS.ajax_console_proxy_port, host='0.0.0.0') |
246 | + server = wsgi.Server("AJAX Console Proxy", acp, port=acp_port) |
247 | + server.start() |
248 | server.wait() |
249 | |
250 | === modified file 'bin/nova-api' |
251 | --- bin/nova-api 2011-03-18 13:56:05 +0000 |
252 | +++ bin/nova-api 2011-07-14 20:24:25 +0000 |
253 | @@ -1,5 +1,4 @@ |
254 | #!/usr/bin/env python |
255 | -# pylint: disable=C0103 |
256 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 |
257 | |
258 | # Copyright 2010 United States Government as represented by the |
259 | @@ -18,44 +17,48 @@ |
260 | # See the License for the specific language governing permissions and |
261 | # limitations under the License. |
262 | |
263 | -"""Starter script for Nova API.""" |
264 | - |
265 | -import gettext |
266 | +"""Starter script for Nova API. |
267 | + |
268 | +Starts both the EC2 and OpenStack APIs in separate processes. |
269 | + |
270 | +""" |
271 | + |
272 | import os |
273 | +import signal |
274 | import sys |
275 | |
276 | -# If ../nova/__init__.py exists, add ../ to Python search path, so that |
277 | -# it will override what happens to be installed in /usr/(local/)lib/python... |
278 | -possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), |
279 | - os.pardir, |
280 | - os.pardir)) |
281 | -if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): |
282 | + |
283 | +possible_topdir = os.path.normpath(os.path.join(os.path.abspath( |
284 | + sys.argv[0]), os.pardir, os.pardir)) |
285 | +if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")): |
286 | sys.path.insert(0, possible_topdir) |
287 | |
288 | -gettext.install('nova', unicode=1) |
289 | +import nova.service |
290 | +import nova.utils |
291 | |
292 | from nova import flags |
293 | -from nova import log as logging |
294 | -from nova import service |
295 | -from nova import utils |
296 | -from nova import version |
297 | -from nova import wsgi |
298 | - |
299 | - |
300 | -LOG = logging.getLogger('nova.api') |
301 | + |
302 | |
303 | FLAGS = flags.FLAGS |
304 | |
305 | + |
306 | +def main(): |
307 | + """Launch EC2 and OSAPI services.""" |
308 | + nova.utils.Bootstrapper.bootstrap_binary(sys.argv) |
309 | + |
310 | + launcher = nova.service.Launcher() |
311 | + |
312 | + for api in FLAGS.enabled_apis: |
313 | + service = nova.service.WSGIService(api) |
314 | + launcher.launch_service(service) |
315 | + |
316 | + signal.signal(signal.SIGTERM, lambda *_: launcher.stop()) |
317 | + |
318 | + try: |
319 | + launcher.wait() |
320 | + except KeyboardInterrupt: |
321 | + launcher.stop() |
322 | + |
323 | + |
324 | if __name__ == '__main__': |
325 | - utils.default_flagfile() |
326 | - FLAGS(sys.argv) |
327 | - logging.setup() |
328 | - LOG.audit(_("Starting nova-api node (version %s)"), |
329 | - version.version_string_with_vcs()) |
330 | - LOG.debug(_("Full set of FLAGS:")) |
331 | - for flag in FLAGS: |
332 | - flag_get = FLAGS.get(flag, None) |
333 | - LOG.debug("%(flag)s : %(flag_get)s" % locals()) |
334 | - |
335 | - service = service.serve_wsgi(service.ApiService) |
336 | - service.wait() |
337 | + sys.exit(main()) |
338 | |
339 | === modified file 'bin/nova-dhcpbridge' |
340 | --- bin/nova-dhcpbridge 2011-03-29 20:32:44 +0000 |
341 | +++ bin/nova-dhcpbridge 2011-07-14 20:24:25 +0000 |
342 | @@ -59,14 +59,12 @@ |
343 | LOG.debug(_("leasing ip")) |
344 | network_manager = utils.import_object(FLAGS.network_manager) |
345 | network_manager.lease_fixed_ip(context.get_admin_context(), |
346 | - mac, |
347 | ip_address) |
348 | else: |
349 | rpc.cast(context.get_admin_context(), |
350 | "%s.%s" % (FLAGS.network_topic, FLAGS.host), |
351 | {"method": "lease_fixed_ip", |
352 | - "args": {"mac": mac, |
353 | - "address": ip_address}}) |
354 | + "args": {"address": ip_address}}) |
355 | |
356 | |
357 | def old_lease(mac, ip_address, hostname, interface): |
358 | @@ -81,14 +79,12 @@ |
359 | LOG.debug(_("releasing ip")) |
360 | network_manager = utils.import_object(FLAGS.network_manager) |
361 | network_manager.release_fixed_ip(context.get_admin_context(), |
362 | - mac, |
363 | ip_address) |
364 | else: |
365 | rpc.cast(context.get_admin_context(), |
366 | "%s.%s" % (FLAGS.network_topic, FLAGS.host), |
367 | {"method": "release_fixed_ip", |
368 | - "args": {"mac": mac, |
369 | - "address": ip_address}}) |
370 | + "args": {"address": ip_address}}) |
371 | |
372 | |
373 | def init_leases(interface): |
374 | @@ -108,6 +104,13 @@ |
375 | interface = os.environ.get('DNSMASQ_INTERFACE', FLAGS.dnsmasq_interface) |
376 | if int(os.environ.get('TESTING', '0')): |
377 | from nova.tests import fake_flags |
378 | + |
379 | + #if FLAGS.fake_rabbit: |
380 | + # LOG.debug(_("leasing ip")) |
381 | + # network_manager = utils.import_object(FLAGS.network_manager) |
382 | + ## reload(fake_flags) |
383 | + # from nova.tests import fake_flags |
384 | + |
385 | action = argv[1] |
386 | if action in ['add', 'del', 'old']: |
387 | mac = argv[2] |
388 | |
389 | === modified file 'bin/nova-direct-api' |
390 | --- bin/nova-direct-api 2011-03-24 20:20:15 +0000 |
391 | +++ bin/nova-direct-api 2011-07-14 20:24:25 +0000 |
392 | @@ -93,6 +93,9 @@ |
393 | with_req = direct.PostParamsMiddleware(with_json) |
394 | with_auth = direct.DelegatedAuthMiddleware(with_req) |
395 | |
396 | - server = wsgi.Server() |
397 | - server.start(with_auth, FLAGS.direct_port, host=FLAGS.direct_host) |
398 | + server = wsgi.Server("Direct API", |
399 | + with_auth, |
400 | + host=FLAGS.direct_host, |
401 | + port=FLAGS.direct_port) |
402 | + server.start() |
403 | server.wait() |
404 | |
405 | === modified file 'bin/nova-manage' |
406 | --- bin/nova-manage 2011-05-20 09:29:54 +0000 |
407 | +++ bin/nova-manage 2011-07-14 20:24:25 +0000 |
408 | @@ -53,15 +53,14 @@ |
409 | CLI interface for nova management. |
410 | """ |
411 | |
412 | -import datetime |
413 | import gettext |
414 | import glob |
415 | import json |
416 | +import netaddr |
417 | import os |
418 | import sys |
419 | import time |
420 | |
421 | -import IPy |
422 | |
423 | # If ../nova/__init__.py exists, add ../ to Python search path, so that |
424 | # it will override what happens to be installed in /usr/(local/)lib/python... |
425 | @@ -78,6 +77,7 @@ |
426 | from nova import db |
427 | from nova import exception |
428 | from nova import flags |
429 | +from nova import image |
430 | from nova import log as logging |
431 | from nova import quota |
432 | from nova import rpc |
433 | @@ -96,8 +96,8 @@ |
434 | flags.DECLARE('vlan_start', 'nova.network.manager') |
435 | flags.DECLARE('vpn_start', 'nova.network.manager') |
436 | flags.DECLARE('fixed_range_v6', 'nova.network.manager') |
437 | -flags.DECLARE('images_path', 'nova.image.local') |
438 | -flags.DECLARE('libvirt_type', 'nova.virt.libvirt_conn') |
439 | +flags.DECLARE('gateway_v6', 'nova.network.manager') |
440 | +flags.DECLARE('libvirt_type', 'nova.virt.libvirt.connection') |
441 | flags.DEFINE_flag(flags.HelpFlag()) |
442 | flags.DEFINE_flag(flags.HelpshortFlag()) |
443 | flags.DEFINE_flag(flags.HelpXMLFlag()) |
444 | @@ -172,17 +172,23 @@ |
445 | def change(self, project_id, ip, port): |
446 | """Change the ip and port for a vpn. |
447 | |
448 | + this will update all networks associated with a project |
449 | + not sure if that's the desired behavior or not, patches accepted |
450 | + |
451 | args: project, ip, port""" |
452 | + # TODO(tr3buchet): perhaps this shouldn't update all networks |
453 | + # associated with a project in the future |
454 | project = self.manager.get_project(project_id) |
455 | if not project: |
456 | print 'No project %s' % (project_id) |
457 | return |
458 | - admin = context.get_admin_context() |
459 | - network_ref = db.project_get_network(admin, project_id) |
460 | - db.network_update(admin, |
461 | - network_ref['id'], |
462 | - {'vpn_public_address': ip, |
463 | - 'vpn_public_port': int(port)}) |
464 | + admin_context = context.get_admin_context() |
465 | + networks = db.project_get_networks(admin_context, project_id) |
466 | + for network in networks: |
467 | + db.network_update(admin_context, |
468 | + network['id'], |
469 | + {'vpn_public_address': ip, |
470 | + 'vpn_public_port': int(port)}) |
471 | |
472 | |
473 | class ShellCommands(object): |
474 | @@ -257,6 +263,11 @@ |
475 | """adds role to user |
476 | if project is specified, adds project specific role |
477 | arguments: user, role [project]""" |
478 | + if project: |
479 | + projobj = self.manager.get_project(project) |
480 | + if not projobj.has_member(user): |
481 | + print "%s not a member of %s" % (user, project) |
482 | + return |
483 | self.manager.add_role(user, role, project) |
484 | |
485 | def has(self, user, role, project=None): |
486 | @@ -403,8 +414,11 @@ |
487 | except (exception.UserNotFound, exception.ProjectNotFound) as ex: |
488 | print ex |
489 | raise |
490 | - with open(filename, 'w') as f: |
491 | - f.write(rc) |
492 | + if filename == "-": |
493 | + sys.stdout.write(rc) |
494 | + else: |
495 | + with open(filename, 'w') as f: |
496 | + f.write(rc) |
497 | |
498 | def list(self, username=None): |
499 | """Lists all projects |
500 | @@ -417,12 +431,16 @@ |
501 | arguments: project_id [key] [value]""" |
502 | ctxt = context.get_admin_context() |
503 | if key: |
504 | + if value.lower() == 'unlimited': |
505 | + value = None |
506 | try: |
507 | db.quota_update(ctxt, project_id, key, value) |
508 | except exception.ProjectQuotaNotFound: |
509 | db.quota_create(ctxt, project_id, key, value) |
510 | - project_quota = quota.get_quota(ctxt, project_id) |
511 | + project_quota = quota.get_project_quotas(ctxt, project_id) |
512 | for key, value in project_quota.iteritems(): |
513 | + if value is None: |
514 | + value = 'unlimited' |
515 | print '%s: %s' % (key, value) |
516 | |
517 | def remove(self, project_id, user_id): |
518 | @@ -437,20 +455,24 @@ |
519 | def scrub(self, project_id): |
520 | """Deletes data associated with project |
521 | arguments: project_id""" |
522 | - ctxt = context.get_admin_context() |
523 | - network_ref = db.project_get_network(ctxt, project_id) |
524 | - db.network_disassociate(ctxt, network_ref['id']) |
525 | - groups = db.security_group_get_by_project(ctxt, project_id) |
526 | + admin_context = context.get_admin_context() |
527 | + networks = db.project_get_networks(admin_context, project_id) |
528 | + for network in networks: |
529 | + db.network_disassociate(admin_context, network['id']) |
530 | + groups = db.security_group_get_by_project(admin_context, project_id) |
531 | for group in groups: |
532 | - db.security_group_destroy(ctxt, group['id']) |
533 | + db.security_group_destroy(admin_context, group['id']) |
534 | |
535 | def zipfile(self, project_id, user_id, filename='nova.zip'): |
536 | """Exports credentials for project to a zip file |
537 | arguments: project_id user_id [filename='nova.zip]""" |
538 | try: |
539 | zip_file = self.manager.get_credentials(user_id, project_id) |
540 | - with open(filename, 'w') as f: |
541 | - f.write(zip_file) |
542 | + if filename == "-": |
543 | + sys.stdout.write(zip_file) |
544 | + else: |
545 | + with open(filename, 'w') as f: |
546 | + f.write(zip_file) |
547 | except (exception.UserNotFound, exception.ProjectNotFound) as ex: |
548 | print ex |
549 | raise |
550 | @@ -496,7 +518,7 @@ |
551 | instance = fixed_ip['instance'] |
552 | hostname = instance['hostname'] |
553 | host = instance['host'] |
554 | - mac_address = instance['mac_address'] |
555 | + mac_address = fixed_ip['mac_address']['address'] |
556 | print "%-18s\t%-15s\t%-17s\t%-15s\t%s" % ( |
557 | fixed_ip['network']['cidr'], |
558 | fixed_ip['address'], |
559 | @@ -506,24 +528,24 @@ |
560 | class FloatingIpCommands(object): |
561 | """Class for managing floating ip.""" |
562 | |
563 | - def create(self, host, range): |
564 | - """Creates floating ips for host by range |
565 | - arguments: host ip_range""" |
566 | - for address in IPy.IP(range): |
567 | + def create(self, range): |
568 | + """Creates floating ips for zone by range |
569 | + arguments: ip_range""" |
570 | + for address in netaddr.IPNetwork(range): |
571 | db.floating_ip_create(context.get_admin_context(), |
572 | - {'address': str(address), |
573 | - 'host': host}) |
574 | + {'address': str(address)}) |
575 | |
576 | def delete(self, ip_range): |
577 | """Deletes floating ips by range |
578 | arguments: range""" |
579 | - for address in IPy.IP(ip_range): |
580 | + for address in netaddr.IPNetwork(ip_range): |
581 | db.floating_ip_destroy(context.get_admin_context(), |
582 | str(address)) |
583 | |
584 | def list(self, host=None): |
585 | """Lists all floating ips (optionally by host) |
586 | - arguments: [host]""" |
587 | + arguments: [host] |
588 | + Note: if host is given, only active floating IPs are returned""" |
589 | ctxt = context.get_admin_context() |
590 | if host is None: |
591 | floating_ips = db.floating_ip_get_all(ctxt) |
592 | @@ -532,7 +554,7 @@ |
593 | for floating_ip in floating_ips: |
594 | instance = None |
595 | if floating_ip['fixed_ip']: |
596 | - instance = floating_ip['fixed_ip']['instance']['ec2_id'] |
597 | + instance = floating_ip['fixed_ip']['instance']['hostname'] |
598 | print "%s\t%s\t%s" % (floating_ip['host'], |
599 | floating_ip['address'], |
600 | instance) |
601 | @@ -541,13 +563,23 @@ |
602 | class NetworkCommands(object): |
603 | """Class for managing networks.""" |
604 | |
605 | - def create(self, fixed_range=None, num_networks=None, |
606 | + def create(self, label=None, fixed_range=None, num_networks=None, |
607 | network_size=None, vlan_start=None, |
608 | - vpn_start=None, fixed_range_v6=None, label='public'): |
609 | + vpn_start=None, fixed_range_v6=None, gateway_v6=None, |
610 | + flat_network_bridge=None, bridge_interface=None): |
611 | """Creates fixed ips for host by range |
612 | - arguments: fixed_range=FLAG, [num_networks=FLAG], |
613 | + arguments: label, fixed_range, [num_networks=FLAG], |
614 | [network_size=FLAG], [vlan_start=FLAG], |
615 | - [vpn_start=FLAG], [fixed_range_v6=FLAG]""" |
616 | + [vpn_start=FLAG], [fixed_range_v6=FLAG], [gateway_v6=FLAG], |
617 | + [flat_network_bridge=FLAG], [bridge_interface=FLAG] |
618 | + If you wish to use a later argument fill in the gaps with 0s |
619 | + Ex: network create private 10.0.0.0/8 1 15 0 0 0 0 xenbr1 eth1 |
620 | + network create private 10.0.0.0/8 1 15 |
621 | + """ |
622 | + if not label: |
623 | + msg = _('a label (ex: public) is required to create networks.') |
624 | + print msg |
625 | + raise TypeError(msg) |
626 | if not fixed_range: |
627 | msg = _('Fixed range in the form of 10.0.0.0/8 is ' |
628 | 'required to create networks.') |
629 | @@ -563,31 +595,45 @@ |
630 | vpn_start = FLAGS.vpn_start |
631 | if not fixed_range_v6: |
632 | fixed_range_v6 = FLAGS.fixed_range_v6 |
633 | + if not flat_network_bridge: |
634 | + flat_network_bridge = FLAGS.flat_network_bridge |
635 | + if not bridge_interface: |
636 | + bridge_interface = FLAGS.flat_interface or FLAGS.vlan_interface |
637 | + if not gateway_v6: |
638 | + gateway_v6 = FLAGS.gateway_v6 |
639 | net_manager = utils.import_object(FLAGS.network_manager) |
640 | + |
641 | try: |
642 | net_manager.create_networks(context.get_admin_context(), |
643 | + label=label, |
644 | cidr=fixed_range, |
645 | num_networks=int(num_networks), |
646 | network_size=int(network_size), |
647 | vlan_start=int(vlan_start), |
648 | vpn_start=int(vpn_start), |
649 | cidr_v6=fixed_range_v6, |
650 | - label=label) |
651 | + gateway_v6=gateway_v6, |
652 | + bridge=flat_network_bridge, |
653 | + bridge_interface=bridge_interface) |
654 | except ValueError, e: |
655 | print e |
656 | raise e |
657 | |
658 | def list(self): |
659 | """List all created networks""" |
660 | - print "%-18s\t%-15s\t%-15s\t%-15s" % (_('network'), |
661 | - _('netmask'), |
662 | - _('start address'), |
663 | - 'DNS') |
664 | + print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s" % (_('network'), |
665 | + _('netmask'), |
666 | + _('start address'), |
667 | + _('DNS'), |
668 | + _('VlanID'), |
669 | + 'project') |
670 | for network in db.network_get_all(context.get_admin_context()): |
671 | - print "%-18s\t%-15s\t%-15s\t%-15s" % (network.cidr, |
672 | - network.netmask, |
673 | - network.dhcp_start, |
674 | - network.dns) |
675 | + print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s" % (network.cidr, |
676 | + network.netmask, |
677 | + network.dhcp_start, |
678 | + network.dns, |
679 | + network.vlan, |
680 | + network.project_id) |
681 | |
682 | def delete(self, fixed_range): |
683 | """Deletes a network""" |
684 | @@ -608,7 +654,7 @@ |
685 | :param host: show all instance on specified host. |
686 | :param instance: show specificed instance. |
687 | """ |
688 | - print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \ |
689 | + print "%-10s %-15s %-10s %-10s %-26s %-9s %-9s %-9s" \ |
690 | " %-10s %-10s %-10s %-5s" % ( |
691 | _('instance'), |
692 | _('node'), |
693 | @@ -630,14 +676,14 @@ |
694 | context.get_admin_context(), host) |
695 | |
696 | for instance in instances: |
697 | - print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \ |
698 | + print "%-10s %-15s %-10s %-10s %-26s %-9s %-9s %-9s" \ |
699 | " %-10s %-10s %-10s %-5d" % ( |
700 | instance['hostname'], |
701 | instance['host'], |
702 | - instance['instance_type'], |
703 | + instance['instance_type'].name, |
704 | instance['state_description'], |
705 | instance['launched_at'], |
706 | - instance['image_id'], |
707 | + instance['image_ref'], |
708 | instance['kernel_id'], |
709 | instance['ramdisk_id'], |
710 | instance['project_id'], |
711 | @@ -685,7 +731,7 @@ |
712 | """Show a list of all running services. Filter by host & service name. |
713 | args: [host] [service]""" |
714 | ctxt = context.get_admin_context() |
715 | - now = datetime.datetime.utcnow() |
716 | + now = utils.utcnow() |
717 | services = db.service_get_all(ctxt) |
718 | if host: |
719 | services = [s for s in services if s['host'] == host] |
720 | @@ -776,6 +822,28 @@ |
721 | {"method": "update_available_resource"}) |
722 | |
723 | |
724 | +class HostCommands(object): |
725 | + """List hosts""" |
726 | + |
727 | + def list(self, zone=None): |
728 | + """Show a list of all physical hosts. Filter by zone. |
729 | + args: [zone]""" |
730 | + print "%-25s\t%-15s" % (_('host'), |
731 | + _('zone')) |
732 | + ctxt = context.get_admin_context() |
733 | + now = utils.utcnow() |
734 | + services = db.service_get_all(ctxt) |
735 | + if zone: |
736 | + services = [s for s in services if s['availability_zone'] == zone] |
737 | + hosts = [] |
738 | + for srv in services: |
739 | + if not [h for h in hosts if h['host'] == srv['host']]: |
740 | + hosts.append(srv) |
741 | + |
742 | + for h in hosts: |
743 | + print "%-25s\t%-15s" % (h['host'], h['availability_zone']) |
744 | + |
745 | + |
746 | class DbCommands(object): |
747 | """Class for managing the database.""" |
748 | |
749 | @@ -869,7 +937,7 @@ |
750 | try: |
751 | instance_types.create(name, memory, vcpus, local_gb, |
752 | flavorid, swap, rxtx_quota, rxtx_cap) |
753 | - except exception.InvalidInputException: |
754 | + except exception.InvalidInput, e: |
755 | print "Must supply valid parameters to create instance_type" |
756 | print e |
757 | sys.exit(1) |
758 | @@ -932,7 +1000,7 @@ |
759 | """Methods for dealing with a cloud in an odd state""" |
760 | |
761 | def __init__(self, *args, **kwargs): |
762 | - self.image_service = utils.import_object(FLAGS.image_service) |
763 | + self.image_service = image.get_default_image_service() |
764 | |
765 | def _register(self, container_format, disk_format, |
766 | path, owner, name=None, is_public='T', |
767 | @@ -1051,16 +1119,6 @@ |
768 | machine_images = {} |
769 | other_images = {} |
770 | directory = os.path.abspath(directory) |
771 | - # NOTE(vish): If we're importing from the images path dir, attempt |
772 | - # to move the files out of the way before importing |
773 | - # so we aren't writing to the same directory. This |
774 | - # may fail if the dir was a mointpoint. |
775 | - if (FLAGS.image_service == 'nova.image.local.LocalImageService' |
776 | - and directory == os.path.abspath(FLAGS.images_path)): |
777 | - new_dir = "%s_bak" % directory |
778 | - os.rename(directory, new_dir) |
779 | - os.mkdir(directory) |
780 | - directory = new_dir |
781 | for fn in glob.glob("%s/*/info.json" % directory): |
782 | try: |
783 | image_path = os.path.join(fn.rpartition('/')[0], 'image') |
784 | @@ -1077,24 +1135,101 @@ |
785 | self._convert_images(machine_images) |
786 | |
787 | |
788 | +class AgentBuildCommands(object): |
789 | + """Class for managing agent builds.""" |
790 | + |
791 | + def create(self, os, architecture, version, url, md5hash, |
792 | + hypervisor='xen'): |
793 | + """Creates a new agent build. |
794 | + arguments: os architecture version url md5hash [hypervisor='xen']""" |
795 | + ctxt = context.get_admin_context() |
796 | + agent_build = db.agent_build_create(ctxt, |
797 | + {'hypervisor': hypervisor, |
798 | + 'os': os, |
799 | + 'architecture': architecture, |
800 | + 'version': version, |
801 | + 'url': url, |
802 | + 'md5hash': md5hash}) |
803 | + |
804 | + def delete(self, os, architecture, hypervisor='xen'): |
805 | + """Deletes an existing agent build. |
806 | + arguments: os architecture [hypervisor='xen']""" |
807 | + ctxt = context.get_admin_context() |
808 | + agent_build_ref = db.agent_build_get_by_triple(ctxt, |
809 | + hypervisor, os, architecture) |
810 | + db.agent_build_destroy(ctxt, agent_build_ref['id']) |
811 | + |
812 | + def list(self, hypervisor=None): |
813 | + """Lists all agent builds. |
814 | + arguments: <none>""" |
815 | + fmt = "%-10s %-8s %12s %s" |
816 | + ctxt = context.get_admin_context() |
817 | + by_hypervisor = {} |
818 | + for agent_build in db.agent_build_get_all(ctxt): |
819 | + buildlist = by_hypervisor.get(agent_build.hypervisor) |
820 | + if not buildlist: |
821 | + buildlist = by_hypervisor[agent_build.hypervisor] = [] |
822 | + |
823 | + buildlist.append(agent_build) |
824 | + |
825 | + for key, buildlist in by_hypervisor.iteritems(): |
826 | + if hypervisor and key != hypervisor: |
827 | + continue |
828 | + |
829 | + print "Hypervisor: %s" % key |
830 | + print fmt % ('-' * 10, '-' * 8, '-' * 12, '-' * 32) |
831 | + for agent_build in buildlist: |
832 | + print fmt % (agent_build.os, agent_build.architecture, |
833 | + agent_build.version, agent_build.md5hash) |
834 | + print ' %s' % agent_build.url |
835 | + |
836 | |
837 | + |
838 | + def modify(self, os, architecture, version, url, md5hash, |
839 | + hypervisor='xen'): |
840 | + """Update an existing agent build. |
841 | + arguments: os architecture version url md5hash [hypervisor='xen'] |
842 | + """ |
843 | + ctxt = context.get_admin_context() |
844 | + agent_build_ref = db.agent_build_get_by_triple(ctxt, |
845 | + hypervisor, os, architecture) |
846 | + db.agent_build_update(ctxt, agent_build_ref['id'], |
847 | + {'version': version, |
848 | + 'url': url, |
849 | + 'md5hash': md5hash}) |
850 | + |
851 | + |
852 | +class ConfigCommands(object): |
853 | + """Class for exposing the flags defined by flag_file(s).""" |
854 | + |
855 | + def __init__(self): |
856 | + pass |
857 | + |
858 | + def list(self): |
859 | + print FLAGS.FlagsIntoString() |
860 | + |
861 | + |
862 | CATEGORIES = [ |
863 | - ('user', UserCommands), |
864 | ('account', AccountCommands), |
865 | + ('agent', AgentBuildCommands), |
866 | + ('config', ConfigCommands), |
867 | + ('db', DbCommands), |
868 | + ('fixed', FixedIpCommands), |
869 | + ('flavor', InstanceTypeCommands), |
870 | + ('floating', FloatingIpCommands), |
871 | + ('host', HostCommands), |
872 | + ('instance_type', InstanceTypeCommands), |
873 | + ('image', ImageCommands), |
874 | + ('network', NetworkCommands), |
875 | ('project', ProjectCommands), |
876 | ('role', RoleCommands), |
877 | + ('service', ServiceCommands), |
878 | ('shell', ShellCommands), |
879 | - ('vpn', VpnCommands), |
880 | - ('fixed', FixedIpCommands), |
881 | - ('floating', FloatingIpCommands), |
882 | - ('network', NetworkCommands), |
883 | + ('user', UserCommands), |
884 | + ('version', VersionCommands), |
885 | ('vm', VmCommands), |
886 | - ('service', ServiceCommands), |
887 | - ('db', DbCommands), |
888 | ('volume', VolumeCommands), |
889 | - ('instance_type', InstanceTypeCommands), |
890 | - ('image', ImageCommands), |
891 | - ('flavor', InstanceTypeCommands), |
892 | - ('version', VersionCommands)] |
893 | + ('vpn', VpnCommands)] |
894 | |
895 | |
896 | def lazy_match(name, key_value_tuples): |
897 | |
898 | === modified file 'bin/nova-objectstore' |
899 | --- bin/nova-objectstore 2011-03-24 23:37:35 +0000 |
900 | +++ bin/nova-objectstore 2011-07-14 20:24:25 +0000 |
901 | @@ -50,6 +50,9 @@ |
902 | FLAGS(sys.argv) |
903 | logging.setup() |
904 | router = s3server.S3Application(FLAGS.buckets_path) |
905 | - server = wsgi.Server() |
906 | - server.start(router, FLAGS.s3_port, host=FLAGS.s3_host) |
907 | + server = wsgi.Server("S3 Objectstore", |
908 | + router, |
909 | + port=FLAGS.s3_port, |
910 | + host=FLAGS.s3_host) |
911 | + server.start() |
912 | server.wait() |
913 | |
914 | === modified file 'bin/nova-vncproxy' |
915 | --- bin/nova-vncproxy 2011-03-29 21:53:38 +0000 |
916 | +++ bin/nova-vncproxy 2011-07-14 20:24:25 +0000 |
917 | @@ -63,6 +63,19 @@ |
918 | flags.DEFINE_flag(flags.HelpXMLFlag()) |
919 | |
920 | |
921 | +def handle_flash_socket_policy(socket): |
922 | + LOG.info(_("Received connection on flash socket policy port")) |
923 | + |
924 | + fd = socket.makefile('rw') |
925 | + expected_command = "<policy-file-request/>" |
926 | + if expected_command in fd.read(len(expected_command) + 1): |
927 | + LOG.info(_("Received valid flash socket policy request")) |
928 | + fd.write('<?xml version="1.0"?><cross-domain-policy><allow-' |
929 | + 'access-from domain="*" to-ports="%d" /></cross-' |
930 | + 'domain-policy>' % (FLAGS.vncproxy_port)) |
931 | + fd.flush() |
932 | + socket.close() |
933 | + |
934 | if __name__ == "__main__": |
935 | utils.default_flagfile() |
936 | FLAGS(sys.argv) |
937 | @@ -96,6 +109,11 @@ |
938 | |
939 | service.serve() |
940 | |
941 | - server = wsgi.Server() |
942 | - server.start(with_auth, FLAGS.vncproxy_port, host=FLAGS.vncproxy_host) |
943 | + server = wsgi.Server("VNC Proxy", |
944 | + with_auth, |
945 | + host=FLAGS.vncproxy_host, |
946 | + port=FLAGS.vncproxy_port) |
947 | + server.start() |
948 | + server.start_tcp(handle_flash_socket_policy, 843, host=FLAGS.vncproxy_host) |
949 | + |
950 | server.wait() |
951 | |
952 | === modified file 'contrib/nova.sh' |
953 | --- contrib/nova.sh 2011-03-25 16:40:59 +0000 |
954 | +++ contrib/nova.sh 2011-07-14 20:24:25 +0000 |
955 | @@ -17,7 +17,7 @@ |
956 | HOST_IP=`LC_ALL=C ifconfig | grep -m 1 'inet addr:'| cut -d: -f2 | awk '{print $1}'` |
957 | fi |
958 | |
959 | -USE_MYSQL=${USE_MYSQL:-0} |
960 | +USE_MYSQL=${USE_MYSQL:-1} |
961 | INTERFACE=${INTERFACE:-eth0} |
962 | FLOATING_RANGE=${FLOATING_RANGE:-10.6.0.0/27} |
963 | FIXED_RANGE=${FIXED_RANGE:-10.0.0.0/24} |
964 | @@ -159,10 +159,6 @@ |
965 | mkdir -p $NOVA_DIR/instances |
966 | rm -rf $NOVA_DIR/networks |
967 | mkdir -p $NOVA_DIR/networks |
968 | - if [ ! -d "$NOVA_DIR/images" ]; then |
969 | - ln -s $DIR/images $NOVA_DIR/images |
970 | - fi |
971 | - |
972 | if [ "$TEST" == 1 ]; then |
973 | cd $NOVA_DIR |
974 | python $NOVA_DIR/run_tests.py |
975 | @@ -181,8 +177,18 @@ |
976 | # create some floating ips |
977 | $NOVA_DIR/bin/nova-manage floating create `hostname` $FLOATING_RANGE |
978 | |
979 | - # convert old images |
980 | - $NOVA_DIR/bin/nova-manage image convert $DIR/images |
981 | + if [ ! -d "$NOVA_DIR/images" ]; then |
982 | + if [ ! -d "$DIR/converted-images" ]; then |
983 | + # convert old images |
984 | + mkdir $DIR/converted-images |
985 | + ln -s $DIR/converted-images $NOVA_DIR/images |
986 | + $NOVA_DIR/bin/nova-manage image convert $DIR/images |
987 | + else |
988 | + ln -s $DIR/converted-images $NOVA_DIR/images |
989 | + fi |
990 | + |
991 | + fi |
992 | + |
993 | |
994 | # nova api crashes if we start it with a regular screen command, |
995 | # so send the start command by forcing text into the window. |
996 | |
997 | === removed file 'doc/.autogenerated' |
998 | --- doc/.autogenerated 2011-03-03 00:57:56 +0000 |
999 | +++ doc/.autogenerated 1970-01-01 00:00:00 +0000 |
1000 | @@ -1,283 +0,0 @@ |
1001 | -source/api/nova..adminclient.rst |
1002 | -source/api/nova..api.direct.rst |
1003 | -source/api/nova..api.ec2.admin.rst |
1004 | -source/api/nova..api.ec2.apirequest.rst |
1005 | -source/api/nova..api.ec2.cloud.rst |
1006 | -source/api/nova..api.ec2.metadatarequesthandler.rst |
1007 | -source/api/nova..api.openstack.auth.rst |
1008 | -source/api/nova..api.openstack.backup_schedules.rst |
1009 | -source/api/nova..api.openstack.common.rst |
1010 | -source/api/nova..api.openstack.consoles.rst |
1011 | -source/api/nova..api.openstack.faults.rst |
1012 | -source/api/nova..api.openstack.flavors.rst |
1013 | -source/api/nova..api.openstack.images.rst |
1014 | -source/api/nova..api.openstack.servers.rst |
1015 | -source/api/nova..api.openstack.shared_ip_groups.rst |
1016 | -source/api/nova..api.openstack.zones.rst |
1017 | -source/api/nova..auth.dbdriver.rst |
1018 | -source/api/nova..auth.fakeldap.rst |
1019 | -source/api/nova..auth.ldapdriver.rst |
1020 | -source/api/nova..auth.manager.rst |
1021 | -source/api/nova..auth.signer.rst |
1022 | -source/api/nova..cloudpipe.pipelib.rst |
1023 | -source/api/nova..compute.api.rst |
1024 | -source/api/nova..compute.instance_types.rst |
1025 | -source/api/nova..compute.manager.rst |
1026 | -source/api/nova..compute.monitor.rst |
1027 | -source/api/nova..compute.power_state.rst |
1028 | -source/api/nova..console.api.rst |
1029 | -source/api/nova..console.fake.rst |
1030 | -source/api/nova..console.manager.rst |
1031 | -source/api/nova..console.xvp.rst |
1032 | -source/api/nova..context.rst |
1033 | -source/api/nova..crypto.rst |
1034 | -source/api/nova..db.api.rst |
1035 | -source/api/nova..db.base.rst |
1036 | -source/api/nova..db.migration.rst |
1037 | -source/api/nova..db.sqlalchemy.api.rst |
1038 | -source/api/nova..db.sqlalchemy.migrate_repo.manage.rst |
1039 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.001_austin.rst |
1040 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.002_bexar.rst |
1041 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.003_add_label_to_networks.rst |
1042 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.004_add_zone_tables.rst |
1043 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.005_add_instance_metadata.rst |
1044 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.006_add_provider_data_to_volumes.rst |
1045 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.007_add_instance_types.rst |
1046 | -source/api/nova..db.sqlalchemy.migration.rst |
1047 | -source/api/nova..db.sqlalchemy.models.rst |
1048 | -source/api/nova..db.sqlalchemy.session.rst |
1049 | -source/api/nova..exception.rst |
1050 | -source/api/nova..fakememcache.rst |
1051 | -source/api/nova..fakerabbit.rst |
1052 | -source/api/nova..flags.rst |
1053 | -source/api/nova..image.glance.rst |
1054 | -source/api/nova..image.local.rst |
1055 | -source/api/nova..image.s3.rst |
1056 | -source/api/nova..image.service.rst |
1057 | -source/api/nova..log.rst |
1058 | -source/api/nova..manager.rst |
1059 | -source/api/nova..network.api.rst |
1060 | -source/api/nova..network.linux_net.rst |
1061 | -source/api/nova..network.manager.rst |
1062 | -source/api/nova..objectstore.bucket.rst |
1063 | -source/api/nova..objectstore.handler.rst |
1064 | -source/api/nova..objectstore.image.rst |
1065 | -source/api/nova..objectstore.stored.rst |
1066 | -source/api/nova..quota.rst |
1067 | -source/api/nova..rpc.rst |
1068 | -source/api/nova..scheduler.chance.rst |
1069 | -source/api/nova..scheduler.driver.rst |
1070 | -source/api/nova..scheduler.manager.rst |
1071 | -source/api/nova..scheduler.simple.rst |
1072 | -source/api/nova..scheduler.zone.rst |
1073 | -source/api/nova..service.rst |
1074 | -source/api/nova..test.rst |
1075 | -source/api/nova..tests.api.openstack.fakes.rst |
1076 | -source/api/nova..tests.api.openstack.test_adminapi.rst |
1077 | -source/api/nova..tests.api.openstack.test_api.rst |
1078 | -source/api/nova..tests.api.openstack.test_auth.rst |
1079 | -source/api/nova..tests.api.openstack.test_common.rst |
1080 | -source/api/nova..tests.api.openstack.test_faults.rst |
1081 | -source/api/nova..tests.api.openstack.test_flavors.rst |
1082 | -source/api/nova..tests.api.openstack.test_images.rst |
1083 | -source/api/nova..tests.api.openstack.test_ratelimiting.rst |
1084 | -source/api/nova..tests.api.openstack.test_servers.rst |
1085 | -source/api/nova..tests.api.openstack.test_shared_ip_groups.rst |
1086 | -source/api/nova..tests.api.openstack.test_zones.rst |
1087 | -source/api/nova..tests.api.test_wsgi.rst |
1088 | -source/api/nova..tests.db.fakes.rst |
1089 | -source/api/nova..tests.declare_flags.rst |
1090 | -source/api/nova..tests.fake_flags.rst |
1091 | -source/api/nova..tests.glance.stubs.rst |
1092 | -source/api/nova..tests.hyperv_unittest.rst |
1093 | -source/api/nova..tests.objectstore_unittest.rst |
1094 | -source/api/nova..tests.real_flags.rst |
1095 | -source/api/nova..tests.runtime_flags.rst |
1096 | -source/api/nova..tests.test_access.rst |
1097 | -source/api/nova..tests.test_api.rst |
1098 | -source/api/nova..tests.test_auth.rst |
1099 | -source/api/nova..tests.test_cloud.rst |
1100 | -source/api/nova..tests.test_compute.rst |
1101 | -source/api/nova..tests.test_console.rst |
1102 | -source/api/nova..tests.test_direct.rst |
1103 | -source/api/nova..tests.test_flags.rst |
1104 | -source/api/nova..tests.test_instance_types.rst |
1105 | -source/api/nova..tests.test_localization.rst |
1106 | -source/api/nova..tests.test_log.rst |
1107 | -source/api/nova..tests.test_middleware.rst |
1108 | -source/api/nova..tests.test_misc.rst |
1109 | -source/api/nova..tests.test_network.rst |
1110 | -source/api/nova..tests.test_quota.rst |
1111 | -source/api/nova..tests.test_rpc.rst |
1112 | -source/api/nova..tests.test_scheduler.rst |
1113 | -source/api/nova..tests.test_service.rst |
1114 | -source/api/nova..tests.test_test.rst |
1115 | -source/api/nova..tests.test_twistd.rst |
1116 | -source/api/nova..tests.test_utils.rst |
1117 | -source/api/nova..tests.test_virt.rst |
1118 | -source/api/nova..tests.test_volume.rst |
1119 | -source/api/nova..tests.test_xenapi.rst |
1120 | -source/api/nova..tests.xenapi.stubs.rst |
1121 | -source/api/nova..twistd.rst |
1122 | -source/api/nova..utils.rst |
1123 | -source/api/nova..version.rst |
1124 | -source/api/nova..virt.connection.rst |
1125 | -source/api/nova..virt.disk.rst |
1126 | -source/api/nova..virt.fake.rst |
1127 | -source/api/nova..virt.hyperv.rst |
1128 | -source/api/nova..virt.images.rst |
1129 | -source/api/nova..virt.libvirt_conn.rst |
1130 | -source/api/nova..virt.xenapi.fake.rst |
1131 | -source/api/nova..virt.xenapi.network_utils.rst |
1132 | -source/api/nova..virt.xenapi.vm_utils.rst |
1133 | -source/api/nova..virt.xenapi.vmops.rst |
1134 | -source/api/nova..virt.xenapi.volume_utils.rst |
1135 | -source/api/nova..virt.xenapi.volumeops.rst |
1136 | -source/api/nova..virt.xenapi_conn.rst |
1137 | -source/api/nova..volume.api.rst |
1138 | -source/api/nova..volume.driver.rst |
1139 | -source/api/nova..volume.manager.rst |
1140 | -source/api/nova..volume.san.rst |
1141 | -source/api/nova..wsgi.rst |
1142 | -source/api/autoindex.rst |
1143 | -source/api/nova..adminclient.rst |
1144 | -source/api/nova..api.direct.rst |
1145 | -source/api/nova..api.ec2.admin.rst |
1146 | -source/api/nova..api.ec2.apirequest.rst |
1147 | -source/api/nova..api.ec2.cloud.rst |
1148 | -source/api/nova..api.ec2.metadatarequesthandler.rst |
1149 | -source/api/nova..api.openstack.auth.rst |
1150 | -source/api/nova..api.openstack.backup_schedules.rst |
1151 | -source/api/nova..api.openstack.common.rst |
1152 | -source/api/nova..api.openstack.consoles.rst |
1153 | -source/api/nova..api.openstack.faults.rst |
1154 | -source/api/nova..api.openstack.flavors.rst |
1155 | -source/api/nova..api.openstack.images.rst |
1156 | -source/api/nova..api.openstack.servers.rst |
1157 | -source/api/nova..api.openstack.shared_ip_groups.rst |
1158 | -source/api/nova..api.openstack.zones.rst |
1159 | -source/api/nova..auth.dbdriver.rst |
1160 | -source/api/nova..auth.fakeldap.rst |
1161 | -source/api/nova..auth.ldapdriver.rst |
1162 | -source/api/nova..auth.manager.rst |
1163 | -source/api/nova..auth.signer.rst |
1164 | -source/api/nova..cloudpipe.pipelib.rst |
1165 | -source/api/nova..compute.api.rst |
1166 | -source/api/nova..compute.instance_types.rst |
1167 | -source/api/nova..compute.manager.rst |
1168 | -source/api/nova..compute.monitor.rst |
1169 | -source/api/nova..compute.power_state.rst |
1170 | -source/api/nova..console.api.rst |
1171 | -source/api/nova..console.fake.rst |
1172 | -source/api/nova..console.manager.rst |
1173 | -source/api/nova..console.xvp.rst |
1174 | -source/api/nova..context.rst |
1175 | -source/api/nova..crypto.rst |
1176 | -source/api/nova..db.api.rst |
1177 | -source/api/nova..db.base.rst |
1178 | -source/api/nova..db.migration.rst |
1179 | -source/api/nova..db.sqlalchemy.api.rst |
1180 | -source/api/nova..db.sqlalchemy.migrate_repo.manage.rst |
1181 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.001_austin.rst |
1182 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.002_bexar.rst |
1183 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.003_add_label_to_networks.rst |
1184 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.004_add_zone_tables.rst |
1185 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.005_add_instance_metadata.rst |
1186 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.006_add_provider_data_to_volumes.rst |
1187 | -source/api/nova..db.sqlalchemy.migrate_repo.versions.007_add_instance_types.rst |
1188 | -source/api/nova..db.sqlalchemy.migration.rst |
1189 | -source/api/nova..db.sqlalchemy.models.rst |
1190 | -source/api/nova..db.sqlalchemy.session.rst |
1191 | -source/api/nova..exception.rst |
1192 | -source/api/nova..fakememcache.rst |
1193 | -source/api/nova..fakerabbit.rst |
1194 | -source/api/nova..flags.rst |
1195 | -source/api/nova..image.glance.rst |
1196 | -source/api/nova..image.local.rst |
1197 | -source/api/nova..image.s3.rst |
1198 | -source/api/nova..image.service.rst |
1199 | -source/api/nova..log.rst |
1200 | -source/api/nova..manager.rst |
1201 | -source/api/nova..network.api.rst |
1202 | -source/api/nova..network.linux_net.rst |
1203 | -source/api/nova..network.manager.rst |
1204 | -source/api/nova..objectstore.bucket.rst |
1205 | -source/api/nova..objectstore.handler.rst |
1206 | -source/api/nova..objectstore.image.rst |
1207 | -source/api/nova..objectstore.stored.rst |
1208 | -source/api/nova..quota.rst |
1209 | -source/api/nova..rpc.rst |
1210 | -source/api/nova..scheduler.chance.rst |
1211 | -source/api/nova..scheduler.driver.rst |
1212 | -source/api/nova..scheduler.manager.rst |
1213 | -source/api/nova..scheduler.simple.rst |
1214 | -source/api/nova..scheduler.zone.rst |
1215 | -source/api/nova..service.rst |
1216 | -source/api/nova..test.rst |
1217 | -source/api/nova..tests.api.openstack.fakes.rst |
1218 | -source/api/nova..tests.api.openstack.test_adminapi.rst |
1219 | -source/api/nova..tests.api.openstack.test_api.rst |
1220 | -source/api/nova..tests.api.openstack.test_auth.rst |
1221 | -source/api/nova..tests.api.openstack.test_common.rst |
1222 | -source/api/nova..tests.api.openstack.test_faults.rst |
1223 | -source/api/nova..tests.api.openstack.test_flavors.rst |
1224 | -source/api/nova..tests.api.openstack.test_images.rst |
1225 | -source/api/nova..tests.api.openstack.test_ratelimiting.rst |
1226 | -source/api/nova..tests.api.openstack.test_servers.rst |
1227 | -source/api/nova..tests.api.openstack.test_shared_ip_groups.rst |
1228 | -source/api/nova..tests.api.openstack.test_zones.rst |
1229 | -source/api/nova..tests.api.test_wsgi.rst |
1230 | -source/api/nova..tests.db.fakes.rst |
1231 | -source/api/nova..tests.declare_flags.rst |
1232 | -source/api/nova..tests.fake_flags.rst |
1233 | -source/api/nova..tests.glance.stubs.rst |
1234 | -source/api/nova..tests.hyperv_unittest.rst |
1235 | -source/api/nova..tests.objectstore_unittest.rst |
1236 | -source/api/nova..tests.real_flags.rst |
1237 | -source/api/nova..tests.runtime_flags.rst |
1238 | -source/api/nova..tests.test_access.rst |
1239 | -source/api/nova..tests.test_api.rst |
1240 | -source/api/nova..tests.test_auth.rst |
1241 | -source/api/nova..tests.test_cloud.rst |
1242 | -source/api/nova..tests.test_compute.rst |
1243 | -source/api/nova..tests.test_console.rst |
1244 | -source/api/nova..tests.test_direct.rst |
1245 | -source/api/nova..tests.test_flags.rst |
1246 | -source/api/nova..tests.test_instance_types.rst |
1247 | -source/api/nova..tests.test_localization.rst |
1248 | -source/api/nova..tests.test_log.rst |
1249 | -source/api/nova..tests.test_middleware.rst |
1250 | -source/api/nova..tests.test_misc.rst |
1251 | -source/api/nova..tests.test_network.rst |
1252 | -source/api/nova..tests.test_quota.rst |
1253 | -source/api/nova..tests.test_rpc.rst |
1254 | -source/api/nova..tests.test_scheduler.rst |
1255 | -source/api/nova..tests.test_service.rst |
1256 | -source/api/nova..tests.test_test.rst |
1257 | -source/api/nova..tests.test_twistd.rst |
1258 | -source/api/nova..tests.test_utils.rst |
1259 | -source/api/nova..tests.test_virt.rst |
1260 | -source/api/nova..tests.test_volume.rst |
1261 | -source/api/nova..tests.test_xenapi.rst |
1262 | -source/api/nova..tests.xenapi.stubs.rst |
1263 | -source/api/nova..twistd.rst |
1264 | -source/api/nova..utils.rst |
1265 | -source/api/nova..version.rst |
1266 | -source/api/nova..virt.connection.rst |
1267 | -source/api/nova..virt.disk.rst |
1268 | -source/api/nova..virt.fake.rst |
1269 | -source/api/nova..virt.hyperv.rst |
1270 | -source/api/nova..virt.images.rst |
1271 | -source/api/nova..virt.libvirt_conn.rst |
1272 | -source/api/nova..virt.xenapi.fake.rst |
1273 | -source/api/nova..virt.xenapi.network_utils.rst |
1274 | -source/api/nova..virt.xenapi.vm_utils.rst |
1275 | -source/api/nova..virt.xenapi.vmops.rst |
1276 | -source/api/nova..virt.xenapi.volume_utils.rst |
1277 | -source/api/nova..virt.xenapi.volumeops.rst |
1278 | -source/api/nova..virt.xenapi_conn.rst |
1279 | -source/api/nova..volume.api.rst |
1280 | -source/api/nova..volume.driver.rst |
1281 | -source/api/nova..volume.manager.rst |
1282 | -source/api/nova..volume.san.rst |
1283 | -source/api/nova..wsgi.rst |
1284 | |
1285 | === removed directory 'doc/build/html' |
1286 | === removed file 'doc/build/html/.buildinfo' |
1287 | --- doc/build/html/.buildinfo 2011-02-21 20:30:20 +0000 |
1288 | +++ doc/build/html/.buildinfo 1970-01-01 00:00:00 +0000 |
1289 | @@ -1,4 +0,0 @@ |
1290 | -# Sphinx build info version 1 |
1291 | -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. |
1292 | -config: 2a2fe6198f4be4a4d6f289b09d16d74a |
1293 | -tags: fbb0d17656682115ca4d033fb2f83ba1 |
1294 | |
1295 | === added file 'doc/source/devref/distributed_scheduler.rst' |
1296 | --- doc/source/devref/distributed_scheduler.rst 1970-01-01 00:00:00 +0000 |
1297 | +++ doc/source/devref/distributed_scheduler.rst 2011-07-14 20:24:25 +0000 |
1298 | @@ -0,0 +1,188 @@ |
1299 | +.. |
1300 | + Copyright 2011 OpenStack LLC |
1301 | + All Rights Reserved. |
1302 | + |
1303 | + Licensed under the Apache License, Version 2.0 (the "License"); you may |
1304 | + not use this file except in compliance with the License. You may obtain |
1305 | + a copy of the License at |
1306 | + |
1307 | + http://www.apache.org/licenses/LICENSE-2.0 |
1308 | + |
1309 | + Unless required by applicable law or agreed to in writing, software |
1310 | + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
1311 | + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
1312 | + License for the specific language governing permissions and limitations |
1313 | + under the License. |
1314 | + |
1315 | + Source for illustrations in doc/source/image_src/zone_distsched_illustrations.odp |
1316 | + (OpenOffice Impress format) Illustrations are "exported" to png and then scaled |
1317 | + to 400x300 or 640x480 as needed and placed in the doc/source/images directory. |
1318 | + |
1319 | +Distributed Scheduler |
1320 | +===================== |
1321 | + |
1322 | +The Scheduler is akin to a Dating Service. Requests for the creation of new instances come in and the most applicable Compute nodes are selected from a large pool of potential candidates. In a small deployment we may be happy with the currently available Chance Scheduler which randomly selects a Host from the available pool. Or if you need something a little more fancy you may want to use the Availability Zone Scheduler, which selects Compute hosts from a logical partitioning of available hosts (within a single Zone). |
1323 | + |
1324 | + .. image:: /images/dating_service.png |
1325 | + |
1326 | +But for larger deployments a more complex scheduling algorithm is required. Additionally, if you are using Zones in your Nova setup, you'll need a scheduler that understand how to pass instance requests from Zone to Zone. |
1327 | + |
1328 | +This is the purpose of the Distributed Scheduler (DS). The DS utilizes the Capabilities of a Zone and its component services to make informed decisions on where a new instance should be created. When making this decision it consults not only all the Compute nodes in the current Zone, but the Compute nodes in each Child Zone. This continues recursively until the ideal host is found. |
1329 | + |
1330 | +So, how does this all work? |
1331 | + |
1332 | +This document will explain the strategy employed by the `ZoneAwareScheduler` and its derivations. You should read the :doc:`devguide/zones` documentation before reading this. |
1333 | + |
1334 | + .. image:: /images/zone_aware_scheduler.png |
1335 | + |
1336 | +Costs & Weights |
1337 | +--------------- |
1338 | +When deciding where to place an Instance, we compare a Weighted Cost for each Host. The Weighting, currently, is just the sum of each Cost. Costs are nothing more than integers from `0 - max_int`. Costs are computed by looking at the various Capabilities of the Host relative to the specs of the Instance being asked for. Trying to put a plain vanilla instance on a high performance host should have a very high cost. But putting a vanilla instance on a vanilla Host should have a low cost. |
1339 | + |
1340 | +Some Costs are more esoteric. Consider a rule that says we should prefer Hosts that don't already have an instance on it that is owned by the user requesting it (to mitigate against machine failures). Here we have to look at all the other Instances on the host to compute our cost. |
1341 | + |
1342 | +An example of some other costs might include selecting: |
1343 | + * a GPU-based host over a standard CPU |
1344 | + * a host with fast ethernet over a 10mbps line |
1345 | + * a host that can run Windows instances |
1346 | + * a host in the EU vs North America |
1347 | + * etc |
1348 | + |
1349 | +This Weight is computed for each Instance requested. If the customer asked for 1000 instances, the consumed resources on each Host are "virtually" depleted so the Cost can change accordingly. |
1350 | + |
1351 | + .. image:: /images/costs_weights.png |
1352 | + |
1353 | +nova.scheduler.zone_aware_scheduler.ZoneAwareScheduler |
1354 | +------------------------------------------------------ |
1355 | +As we explained in the Zones documentation, each Scheduler has a `ZoneManager` object that collects "Capabilities" about child Zones and each of the services running in the current Zone. The `ZoneAwareScheduler` uses this information to make its decisions. |
1356 | + |
1357 | +Here is how it works: |
1358 | + |
1359 | + 1. The compute nodes are filtered and the nodes remaining are weighed. |
1360 | + 2. Filtering the hosts is a simple matter of ensuring the compute node has ample resources (CPU, RAM, Disk, etc) to fulfil the request. |
1361 | + 3. Weighing of the remaining compute nodes assigns a number based on their suitability for the request. |
1362 | + 4. The same request is sent to each child Zone and step #1 is done there too. The resulting weighted list is returned to the parent. |
1363 | + 5. The parent Zone sorts and aggregates all the weights and a final build plan is constructed. |
1364 | + 6. The build plan is executed upon. Concurrently, instance create requests are sent to each of the selected hosts, be they local or in a child zone. Child Zones may forward the requests to their child Zones as needed. |
1365 | + |
1366 | + .. image:: /images/zone_aware_overview.png |
1367 | + |
1368 | +`ZoneAwareScheduler` by itself is not capable of handling all the provisioning itself. Derived classes are used to select which host filtering and weighing strategy will be used. |
1369 | + |
1370 | +Filtering and Weighing |
1371 | +---------------------- |
1372 | +The filtering (excluding compute nodes incapable of fulfilling the request) and weighing (computing the relative "fitness" of a compute node to fulfill the request) rules used are very subjective operations ... Service Providers will probably have a very different set of filtering and weighing rules than private cloud administrators. The filtering and weighing aspects of the `ZoneAwareScheduler` are flexible and extensible. |
1373 | + |
1374 | + .. image:: /images/filtering.png |
1375 | + |
1376 | +Requesting a new instance |
1377 | +------------------------- |
1378 | +Prior to the `ZoneAwareScheduler`, to request a new instance, a call was made to `nova.compute.api.create()`. The type of instance created depended on the value of the `InstanceType` record being passed in. The `InstanceType` determined the amount of disk, CPU, RAM and network required for the instance. Administrators can add new `InstanceType` records to suit their needs. For more complicated instance requests we need to go beyond the default fields in the `InstanceType` table. |
1379 | + |
1380 | +`nova.compute.api.create()` performed the following actions: |
1381 | + 1. it validated all the fields passed into it. |
1382 | + 2. it created an entry in the `Instance` table for each instance requested |
1383 | + 3. it put one `run_instance` message in the scheduler queue for each instance requested |
1384 | + 4. the schedulers picked off the messages and decided which compute node should handle the request. |
1385 | + 5. the `run_instance` message was forwarded to the compute node for processing and the instance is created. |
1386 | + 6. it returned a list of dicts representing each of the `Instance` records (even if the instance has not been activated yet). At least the `instance_ids` are valid. |
1387 | + |
1388 | + .. image:: /images/nova.compute.api.create.png |
1389 | + |
1390 | +Generally, the standard schedulers (like `ChanceScheduler` and `AvailabilityZoneScheduler`) only operate in the current Zone. They have no concept of child Zones. |
1391 | + |
1392 | +The problem with this approach is each request is scattered amongst each of the schedulers. If we are asking for 1000 instances, each scheduler gets the requests one-at-a-time. There is no possability of optimizing the requests to take into account all 1000 instances as a group. We call this Single-Shot vs. All-at-Once. |
1393 | + |
1394 | +For the `ZoneAwareScheduler` we need to use the All-at-Once approach. We need to consider all the hosts across all the Zones before deciding where they should reside. In order to handle this we have a new method `nova.compute.api.create_all_at_once()`. This method does things a little differently: |
1395 | + 1. it validates all the fields passed into it. |
1396 | + 2. it creates a single `reservation_id` for all of instances created. This is a UUID. |
1397 | + 3. it creates a single `run_instance` request in the scheduler queue |
1398 | + 4. a scheduler picks the message off the queue and works on it. |
1399 | + 5. the scheduler sends off an OS API `POST /zones/select` command to each child Zone. The `BODY` payload of the call contains the `request_spec`. |
1400 | + 6. the child Zones use the `request_spec` to compute a weighted list for each instance requested. No attempt to actually create an instance is done at this point. We're only estimating the suitability of the Zones. |
1401 | + 7. if the child Zone has its own child Zones, the `/zones/select` call will be sent down to them as well. |
1402 | + 8. Finally, when all the estimates have bubbled back to the Zone that initiated the call, all the results are merged, sorted and processed. |
1403 | + 9. Now the instances can be created. The initiating Zone either forwards the `run_instance` message to the local Compute node to do the work, or it issues a `POST /servers` call to the relevant child Zone. The parameters to the child Zone call are the same as what was passed in by the user. |
1404 | + 10. The `reservation_id` is passed back to the caller. Later we explain how the user can check on the status of the command with this `reservation_id`. |
1405 | + |
1406 | + .. image:: /images/nova.compute.api.create_all_at_once.png |
1407 | + |
1408 | +The Catch |
1409 | +--------- |
1410 | +This all seems pretty straightforward but, like most things, there's a catch. Zones are expected to operate in complete isolation from each other. Each Zone has its own AMQP service, database and set of Nova services. But, for security reasons Zones should never leak information about the architectural layout internally. That means Zones cannot leak information about hostnames or service IP addresses outside of its world. |
1411 | + |
1412 | +When `POST /zones/select` is called to estimate which compute node to use, time passes until the `POST /servers` call is issued. If we only passed the weight back from the `select` we would have to re-compute the appropriate compute node for the create command ... and we could end up with a different host. Somehow we need to remember the results of our computations and pass them outside of the Zone. Now, we could store this information in the local database and return a reference to it, but remember that the vast majority of weights are going to be ignored. Storing them in the database would result in a flood of disk access and then we have to clean up all these entries periodically. Recall that there are going to be many many `select` calls issued to child Zones asking for estimates. |
1413 | + |
1414 | +Instead, we take a rather innovative approach to the problem. We encrypt all the child zone internal details and pass them back the to parent Zone. If the parent zone decides to use a child Zone for the instance it simply passes the encrypted data back to the child during the `POST /servers` call as an extra parameter. The child Zone can then decrypt the hint and go directly to the Compute node previously selected. If the estimate isn't used, it is simply discarded by the parent. It's for this reason that it is so important that each Zone defines a unique encryption key via `--build_plan_encryption_key` |
1415 | + |
1416 | +In the case of nested child Zones, each Zone re-encrypts the weighted list results and passes those values to the parent. |
1417 | + |
1418 | +Throughout the `nova.api.openstack.servers`, `nova.api.openstack.zones`, `nova.compute.api.create*` and `nova.scheduler.zone_aware_scheduler` code you'll see references to `blob` and `child_blob`. These are the encrypted hints about which Compute node to use. |
1419 | + |
1420 | +Reservation IDs |
1421 | +--------------- |
1422 | + |
1423 | +NOTE: The features described in this section are related to the up-coming 'merge-4' branch. |
1424 | + |
1425 | +The OpenStack API allows a user to list all the instances they own via the `GET /servers/` command or the details on a particular instance via `GET /servers/###`. This mechanism is usually sufficient since OS API only allows for creating one instance at a time, unlike the EC2 API which allows you to specify a quantity of instances to be created. |
1426 | + |
1427 | +NOTE: currently the `GET /servers` command is not Zone-aware since all operations done in child Zones are done via a single administrative account. Therefore, asking a child Zone to `GET /servers` would return all the active instances ... and that would not be what the user intended. Later, when the Keystone Auth system is integrated with Nova, this functionality will be enabled. |
1428 | + |
1429 | +We could use the OS API 1.1 Extensions mechanism to accept a `num_instances` parameter, but this would result in a different return code. Instead of getting back an `Instance` record, we would be getting back a `reservation_id`. So, instead, we've implemented a new command `POST /zones/boot` command which is nearly identical to `POST /servers` except that it takes a `num_instances` parameter and returns a `reservation_id`. Perhaps in OS API 2.x we can unify these approaches. |
1430 | + |
1431 | +Finally, we need to give the user a way to get information on each of the instances created under this `reservation_id`. Fortunately, this is still possible with the existing `GET /servers` command, so long as we add a new optional `reservation_id` parameter. |
1432 | + |
1433 | +`python-novaclient` will be extended to support both of these changes. |
1434 | + |
1435 | +Host Filter |
1436 | +----------- |
1437 | + |
1438 | +As we mentioned earlier, filtering hosts is a very deployment-specific process. Service Providers may have a different set of criteria for filtering Compute nodes than a University. To faciliate this the `nova.scheduler.host_filter` module supports a variety of filtering strategies as well as an easy means for plugging in your own algorithms. |
1439 | + |
1440 | +The filter used is determined by the `--default_host_filter` flag, which points to a Python Class. By default this flag is set to `nova.scheduler.host_filter.AllHostsFilter` which simply returns all available hosts. But there are others: |
1441 | + |
1442 | + * `nova.scheduler.host_filter.InstanceTypeFilter` provides host filtering based on the memory and disk size specified in the `InstanceType` record passed into `run_instance`. |
1443 | + |
1444 | + * `nova.scheduler.host_filter.JSONFilter` filters hosts based on simple JSON expression grammar. Using a LISP-like JSON structure the caller can request instances based on criteria well beyond what `InstanceType` specifies. See `nova.tests.test_host_filter` for examples. |
1445 | + |
1446 | +To create your own `HostFilter` the user simply has to derive from `nova.scheduler.host_filter.HostFilter` and implement two methods: `instance_type_to_filter` and `filter_hosts`. Since Nova is currently dependent on the `InstanceType` structure, the `instance_type_to_filter` method should take an `InstanceType` and turn it into an internal data structure usable by your filter. This is for backward compatibility with existing OpenStack and EC2 API calls. If you decide to create your own call for creating instances not based on `Flavors` or `InstanceTypes` you can ignore this method. The real work is done in `filter_hosts` which must return a list of host tuples for each appropriate host. The set of all available hosts is in the `ZoneManager` object passed into the call as well as the filter query. The host tuple contains (`<hostname>`, `<additional data>`) where `<additional data>` is whatever you want it to be. |
1447 | + |
1448 | +Cost Scheduler Weighing |
1449 | +----------------------- |
1450 | +Every `ZoneAwareScheduler` derivation must also override the `weigh_hosts` method. This takes the list of filtered hosts (generated by the `filter_hosts` method) and returns a list of weight dicts. The weight dicts must contain two keys: `weight` and `hostname` where `weight` is simply an integer (lower is better) and `hostname` is the name of the host. The list does not need to be sorted, this will be done by the `ZoneAwareScheduler` base class when all the results have been assembled. |
1451 | + |
1452 | +Simple Zone Aware Scheduling |
1453 | +---------------------------- |
1454 | +The easiest way to get started with the `ZoneAwareScheduler` is to use the `nova.scheduler.host_filter.HostFilterScheduler`. This scheduler uses the default Host Filter and the `weight_hosts` method simply returns a weight of 1 for all hosts. But, from this, you can see calls being routed from Zone to Zone and follow the flow of things. |
1455 | + |
1456 | +The `--scheduler_driver` flag is how you specify the scheduler class name. |
1457 | + |
1458 | +Flags |
1459 | +----- |
1460 | + |
1461 | +All this Zone and Distributed Scheduler stuff can seem a little daunting to configure, but it's actually not too bad. Here are some of the main flags you should set in your `nova.conf` file: |
1462 | + |
1463 | +:: |
1464 | + |
1465 | + --allow_admin_api=true |
1466 | + --enable_zone_routing=true |
1467 | + --zone_name=zone1 |
1468 | + --build_plan_encryption_key=c286696d887c9aa0611bbb3e2025a45b |
1469 | + --scheduler_driver=nova.scheduler.host_filter.HostFilterScheduler |
1470 | + --default_host_filter=nova.scheduler.host_filter.AllHostsFilter |
1471 | + |
1472 | +`--allow_admin_api` must be set for OS API to enable the new `/zones/*` commands. |
1473 | +`--enable_zone_routing` must be set for OS API commands such as `create()`, `pause()` and `delete()` to get routed from Zone to Zone when looking for instances. |
1474 | +`--zone_name` is only required in child Zones. The default Zone name is `nova`, but you may want to name your child Zones something useful. Duplicate Zone names are not an issue. |
1475 | +`build_plan_encryption_key` is the SHA-256 key for encrypting/decrypting the Host information when it leaves a Zone. Be sure to change this key for each Zone you create. Do not duplicate keys. |
1476 | +`scheduler_driver` is the real workhorse of the operation. For Distributed Scheduler, you need to specify a class derived from `nova.scheduler.zone_aware_scheduler.ZoneAwareScheduler`. |
1477 | +`default_host_filter` is the host filter to be used for filtering candidate Compute nodes. |
1478 | + |
1479 | +Some optional flags which are handy for debugging are: |
1480 | + |
1481 | +:: |
1482 | + |
1483 | + --connection_type=fake |
1484 | + --verbose |
1485 | + |
1486 | +Using the `Fake` virtualization driver is handy when you're setting this stuff up so you're not dealing with a million possible issues at once. When things seem to working correctly, switch back to whatever hypervisor your deployment uses. |
1487 | |
1488 | === modified file 'doc/source/devref/index.rst' |
1489 | --- doc/source/devref/index.rst 2011-01-04 22:58:08 +0000 |
1490 | +++ doc/source/devref/index.rst 2011-07-14 20:24:25 +0000 |
1491 | @@ -35,6 +35,7 @@ |
1492 | .. toctree:: |
1493 | :maxdepth: 3 |
1494 | |
1495 | + zone |
1496 | rabbit |
1497 | |
1498 | API Reference |
1499 | |
1500 | === added file 'doc/source/devref/multinic.rst' |
1501 | --- doc/source/devref/multinic.rst 1970-01-01 00:00:00 +0000 |
1502 | +++ doc/source/devref/multinic.rst 2011-07-14 20:24:25 +0000 |
1503 | @@ -0,0 +1,39 @@ |
1504 | +MultiNic |
1505 | +======== |
1506 | + |
1507 | +What is it |
1508 | +---------- |
1509 | + |
1510 | +Multinic allows an instance to have more than one vif connected to it. Each vif is representative of a separate network with its own IP block. |
1511 | + |
1512 | +Managers |
1513 | +-------- |
1514 | + |
1515 | +Each of the network managers are designed to run independently of the compute manager. They expose a common API for the compute manager to call to determine and configure the network(s) for an instance. Direct calls to either the network api or especially the DB should be avoided by the virt layers. |
1516 | + |
1517 | +On startup a manager looks in the networks table for networks it is assigned and configures itself to support that network. Using the periodic task, they will claim new networks that have no host set. Only one network per network-host will be claimed at a time. This allows for psuedo-loadbalancing if there are multiple network-hosts running. |
1518 | + |
1519 | +Flat Manager |
1520 | +------------ |
1521 | + |
1522 | + .. image:: /images/multinic_flat.png |
1523 | + |
1524 | +The Flat manager is most similar to a traditional switched network environment. It assumes that the IP routing, DNS, DHCP (possibly) and bridge creation is handled by something else. That is it makes no attempt to configure any of this. It does keep track of a range of IPs for the instances that are connected to the network to be allocated. |
1525 | + |
1526 | +Each instance will get a fixed IP from each network's pool. The guest operating system may be configured to gather this information through an agent or by the hypervisor injecting the files, or it may ignore it completely and come up with only a layer 2 connection. |
1527 | + |
1528 | +Flat manager requires at least one nova-network process running that will listen to the API queue and respond to queries. It does not need to sit on any of the networks but it does keep track of the IPs it hands out to instances. |
1529 | + |
1530 | +FlatDHCP Manager |
1531 | +---------------- |
1532 | + |
1533 | + .. image:: /images/multinic_dhcp.png |
1534 | + |
1535 | +FlatDHCP manager builds on the the Flat manager adding dnsmask (DNS and DHCP) and radvd (Router Advertisement) servers on the bridge for that network. The services run on the host that is assigned to that nework. The FlatDHCP manager will create its bridge as specified when the network was created on the network-host when the network host starts up or when a new network gets allocated to that host. Compute nodes will also create the bridges as necessary and connect instance VIFs to them. |
1536 | + |
1537 | +VLAN Manager |
1538 | +------------ |
1539 | + |
1540 | + .. image:: /images/multinic_vlan.png |
1541 | + |
1542 | +The VLAN manager sets up forwarding to/from a cloudpipe instance in addition to providing dnsmask (DNS and DHCP) and radvd (Router Advertisement) services for each network. The manager will create its bridge as specified when the network was created on the network-host when the network host starts up or when a new network gets allocated to that host. Compute nodes will also create the bridges as necessary and conenct instance VIFs to them. |
1543 | |
1544 | === modified file 'doc/source/devref/zone.rst' |
1545 | --- doc/source/devref/zone.rst 2011-04-08 18:45:42 +0000 |
1546 | +++ doc/source/devref/zone.rst 2011-07-14 20:24:25 +0000 |
1547 | @@ -17,11 +17,11 @@ |
1548 | Zones |
1549 | ===== |
1550 | |
1551 | -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. |
1552 | +A Nova deployment is called a Zone. A Zone allows you to partition your deployments into logical groups for load balancing and instance distribution. 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. |
1553 | |
1554 | 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. |
1555 | |
1556 | -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. |
1557 | +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 and, in those cases, the grandchild's internal structure would not be known to the grand-parent. |
1558 | |
1559 | Zones share nothing. They communicate via the public OpenStack API only. No database, queue, user or project definition is shared between Zones. |
1560 | |
1561 | @@ -34,7 +34,7 @@ |
1562 | |
1563 | key=value;value;value, key=value;value;value |
1564 | |
1565 | -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. |
1566 | +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. |
1567 | |
1568 | Flow within a Zone |
1569 | ------------------ |
1570 | @@ -47,7 +47,7 @@ |
1571 | |
1572 | 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. |
1573 | |
1574 | -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"). |
1575 | +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"). |
1576 | |
1577 | Zone administrative functions |
1578 | ----------------------------- |
1579 | @@ -99,7 +99,7 @@ |
1580 | export NOVA_URL="http://192.168.2.120:8774/v1.0/" |
1581 | |
1582 | |
1583 | -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. |
1584 | +This equates to a POST operation to `.../zones/` to add a new zone. No connection attempt to the child zone is done with 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. |
1585 | |
1586 | Getting a list of child Zones |
1587 | ----------------------------- |
1588 | |
1589 | === added directory 'doc/source/image_src' |
1590 | === added file 'doc/source/image_src/multinic_1.odg' |
1591 | Binary files doc/source/image_src/multinic_1.odg 1970-01-01 00:00:00 +0000 and doc/source/image_src/multinic_1.odg 2011-07-14 20:24:25 +0000 differ |
1592 | === added file 'doc/source/image_src/multinic_2.odg' |
1593 | Binary files doc/source/image_src/multinic_2.odg 1970-01-01 00:00:00 +0000 and doc/source/image_src/multinic_2.odg 2011-07-14 20:24:25 +0000 differ |
1594 | === added file 'doc/source/image_src/multinic_3.odg' |
1595 | Binary files doc/source/image_src/multinic_3.odg 1970-01-01 00:00:00 +0000 and doc/source/image_src/multinic_3.odg 2011-07-14 20:24:25 +0000 differ |
1596 | === added file 'doc/source/image_src/zones_distsched_illustrations.odp' |
1597 | Binary files doc/source/image_src/zones_distsched_illustrations.odp 1970-01-01 00:00:00 +0000 and doc/source/image_src/zones_distsched_illustrations.odp 2011-07-14 20:24:25 +0000 differ |
1598 | === added file 'doc/source/images/costs_weights.png' |
1599 | Binary files doc/source/images/costs_weights.png 1970-01-01 00:00:00 +0000 and doc/source/images/costs_weights.png 2011-07-14 20:24:25 +0000 differ |
1600 | === added file 'doc/source/images/dating_service.png' |
1601 | Binary files doc/source/images/dating_service.png 1970-01-01 00:00:00 +0000 and doc/source/images/dating_service.png 2011-07-14 20:24:25 +0000 differ |
1602 | === added file 'doc/source/images/filtering.png' |
1603 | Binary files doc/source/images/filtering.png 1970-01-01 00:00:00 +0000 and doc/source/images/filtering.png 2011-07-14 20:24:25 +0000 differ |
1604 | === added file 'doc/source/images/multinic_dhcp.png' |
1605 | Binary files doc/source/images/multinic_dhcp.png 1970-01-01 00:00:00 +0000 and doc/source/images/multinic_dhcp.png 2011-07-14 20:24:25 +0000 differ |
1606 | === added file 'doc/source/images/multinic_flat.png' |
1607 | Binary files doc/source/images/multinic_flat.png 1970-01-01 00:00:00 +0000 and doc/source/images/multinic_flat.png 2011-07-14 20:24:25 +0000 differ |
1608 | === added file 'doc/source/images/multinic_vlan.png' |
1609 | Binary files doc/source/images/multinic_vlan.png 1970-01-01 00:00:00 +0000 and doc/source/images/multinic_vlan.png 2011-07-14 20:24:25 +0000 differ |
1610 | === added file 'doc/source/images/nova.compute.api.create.png' |
1611 | Binary files doc/source/images/nova.compute.api.create.png 1970-01-01 00:00:00 +0000 and doc/source/images/nova.compute.api.create.png 2011-07-14 20:24:25 +0000 differ |
1612 | === added file 'doc/source/images/nova.compute.api.create_all_at_once.png' |
1613 | Binary files doc/source/images/nova.compute.api.create_all_at_once.png 1970-01-01 00:00:00 +0000 and doc/source/images/nova.compute.api.create_all_at_once.png 2011-07-14 20:24:25 +0000 differ |
1614 | === added file 'doc/source/images/zone_aware_overview.png' |
1615 | Binary files doc/source/images/zone_aware_overview.png 1970-01-01 00:00:00 +0000 and doc/source/images/zone_aware_overview.png 2011-07-14 20:24:25 +0000 differ |
1616 | === added file 'doc/source/images/zone_aware_scheduler.png' |
1617 | Binary files doc/source/images/zone_aware_scheduler.png 1970-01-01 00:00:00 +0000 and doc/source/images/zone_aware_scheduler.png 2011-07-14 20:24:25 +0000 differ |
1618 | === modified file 'doc/source/man/novamanage.rst' |
1619 | --- doc/source/man/novamanage.rst 2011-04-07 18:25:44 +0000 |
1620 | +++ doc/source/man/novamanage.rst 2011-07-14 20:24:25 +0000 |
1621 | @@ -6,7 +6,7 @@ |
1622 | control and manage cloud computer instances and images |
1623 | ------------------------------------------------------ |
1624 | |
1625 | -:Author: nova@lists.launchpad.net |
1626 | +:Author: openstack@lists.launchpad.net |
1627 | :Date: 2010-11-16 |
1628 | :Copyright: OpenStack LLC |
1629 | :Version: 0.1 |
1630 | @@ -121,7 +121,7 @@ |
1631 | nova-manage role <action> [<argument>] |
1632 | ``nova-manage role add <username> <rolename> <(optional) projectname>`` |
1633 | |
1634 | - Add a user to either a global or project-based role with the indicated <rolename> assigned to the named user. Role names can be one of the following five roles: admin, itsec, projectmanager, netadmin, developer. If you add the project name as the last argument then the role is assigned just for that project, otherwise the user is assigned the named role for all projects. |
1635 | + Add a user to either a global or project-based role with the indicated <rolename> assigned to the named user. Role names can be one of the following five roles: cloudadmin, itsec, sysadmin, netadmin, developer. If you add the project name as the last argument then the role is assigned just for that project, otherwise the user is assigned the named role for all projects. |
1636 | |
1637 | ``nova-manage role has <username> <projectname>`` |
1638 | Checks the user or project and responds with True if the user has a global role with a particular project. |
1639 | |
1640 | === modified file 'doc/source/runnova/managing.users.rst' |
1641 | --- doc/source/runnova/managing.users.rst 2011-02-21 20:30:20 +0000 |
1642 | +++ doc/source/runnova/managing.users.rst 2011-07-14 20:24:25 +0000 |
1643 | @@ -38,11 +38,11 @@ |
1644 | |
1645 | Nova’s rights management system employs the RBAC model and currently supports the following five roles: |
1646 | |
1647 | -* **Cloud Administrator.** (admin) Users of this class enjoy complete system access. |
1648 | +* **Cloud Administrator.** (cloudadmin) Users of this class enjoy complete system access. |
1649 | * **IT Security.** (itsec) This role is limited to IT security personnel. It permits role holders to quarantine instances. |
1650 | -* **Project Manager.** (projectmanager)The default for project owners, this role affords users the ability to add other users to a project, interact with project images, and launch and terminate instances. |
1651 | +* **System Administrator.** (sysadmin) The default for project owners, this role affords users the ability to add other users to a project, interact with project images, and launch and terminate instances. |
1652 | * **Network Administrator.** (netadmin) Users with this role are permitted to allocate and assign publicly accessible IP addresses as well as create and modify firewall rules. |
1653 | -* **Developer.** This is a general purpose role that is assigned to users by default. |
1654 | +* **Developer.** (developer) This is a general purpose role that is assigned to users by default. |
1655 | |
1656 | RBAC management is exposed through the dashboard for simplified user management. |
1657 | |
1658 | |
1659 | === modified file 'nova/__init__.py' |
1660 | --- nova/__init__.py 2011-01-20 08:14:42 +0000 |
1661 | +++ nova/__init__.py 2011-07-14 20:24:25 +0000 |
1662 | @@ -30,3 +30,8 @@ |
1663 | .. moduleauthor:: Manish Singh <yosh@gimp.org> |
1664 | .. moduleauthor:: Andy Smith <andy@anarkystic.com> |
1665 | """ |
1666 | + |
1667 | +import gettext |
1668 | + |
1669 | + |
1670 | +gettext.install("nova", unicode=1) |
1671 | |
1672 | === modified file 'nova/api/direct.py' |
1673 | --- nova/api/direct.py 2011-04-11 16:34:19 +0000 |
1674 | +++ nova/api/direct.py 2011-07-14 20:24:25 +0000 |
1675 | @@ -42,6 +42,7 @@ |
1676 | from nova import flags |
1677 | from nova import utils |
1678 | from nova import wsgi |
1679 | +import nova.api.openstack.wsgi |
1680 | |
1681 | |
1682 | # Global storage for registering modules. |
1683 | @@ -251,7 +252,7 @@ |
1684 | return self._methods[method] |
1685 | |
1686 | |
1687 | -class ServiceWrapper(wsgi.Controller): |
1688 | +class ServiceWrapper(object): |
1689 | """Wrapper to dynamically povide a WSGI controller for arbitrary objects. |
1690 | |
1691 | With lightweight introspection allows public methods on the object to |
1692 | @@ -265,7 +266,7 @@ |
1693 | def __init__(self, service_handle): |
1694 | self.service_handle = service_handle |
1695 | |
1696 | - @webob.dec.wsgify(RequestClass=wsgi.Request) |
1697 | + @webob.dec.wsgify(RequestClass=nova.api.openstack.wsgi.Request) |
1698 | def __call__(self, req): |
1699 | arg_dict = req.environ['wsgiorg.routing_args'][1] |
1700 | action = arg_dict['action'] |
1701 | @@ -289,8 +290,11 @@ |
1702 | |
1703 | try: |
1704 | content_type = req.best_match_content_type() |
1705 | - default_xmlns = self.get_default_xmlns(req) |
1706 | - return self._serialize(result, content_type, default_xmlns) |
1707 | + serializer = { |
1708 | + 'application/xml': nova.api.openstack.wsgi.XMLDictSerializer(), |
1709 | + 'application/json': nova.api.openstack.wsgi.JSONDictSerializer(), |
1710 | + }[content_type] |
1711 | + return serializer.serialize(result) |
1712 | except: |
1713 | raise exception.Error("returned non-serializable type: %s" |
1714 | % result) |
1715 | @@ -320,7 +324,7 @@ |
1716 | |
1717 | def __init__(self, proxy): |
1718 | self._proxy = proxy |
1719 | - if not self.__doc__: |
1720 | + if not self.__doc__: # pylint: disable=E0203 |
1721 | self.__doc__ = proxy.__doc__ |
1722 | if not self._allowed: |
1723 | self._allowed = [] |
1724 | |
1725 | === modified file 'nova/api/ec2/__init__.py' |
1726 | --- nova/api/ec2/__init__.py 2011-04-29 22:06:18 +0000 |
1727 | +++ nova/api/ec2/__init__.py 2011-07-14 20:24:25 +0000 |
1728 | @@ -242,6 +242,7 @@ |
1729 | 'CreateKeyPair': ['all'], |
1730 | 'DeleteKeyPair': ['all'], |
1731 | 'DescribeSecurityGroups': ['all'], |
1732 | + 'ImportPublicKey': ['all'], |
1733 | 'AuthorizeSecurityGroupIngress': ['netadmin'], |
1734 | 'RevokeSecurityGroupIngress': ['netadmin'], |
1735 | 'CreateSecurityGroup': ['netadmin'], |
1736 | @@ -327,6 +328,12 @@ |
1737 | ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x') |
1738 | message = _('Volume %s not found') % ec2_id |
1739 | return self._error(req, context, type(ex).__name__, message) |
1740 | + except exception.SnapshotNotFound as ex: |
1741 | + LOG.info(_('SnapshotNotFound raised: %s'), unicode(ex), |
1742 | + context=context) |
1743 | + ec2_id = ec2utils.id_to_ec2_id(ex.snapshot_id, 'snap-%08x') |
1744 | + message = _('Snapshot %s not found') % ec2_id |
1745 | + return self._error(req, context, type(ex).__name__, message) |
1746 | except exception.NotFound as ex: |
1747 | LOG.info(_('NotFound raised: %s'), unicode(ex), context=context) |
1748 | return self._error(req, context, type(ex).__name__, unicode(ex)) |
1749 | @@ -338,6 +345,10 @@ |
1750 | else: |
1751 | return self._error(req, context, type(ex).__name__, |
1752 | unicode(ex)) |
1753 | + except exception.KeyPairExists as ex: |
1754 | + LOG.debug(_('KeyPairExists raised: %s'), unicode(ex), |
1755 | + context=context) |
1756 | + return self._error(req, context, type(ex).__name__, unicode(ex)) |
1757 | except Exception as ex: |
1758 | extra = {'environment': req.environ} |
1759 | LOG.exception(_('Unexpected error raised: %s'), unicode(ex), |
1760 | |
1761 | === modified file 'nova/api/ec2/admin.py' |
1762 | --- nova/api/ec2/admin.py 2011-04-19 16:19:52 +0000 |
1763 | +++ nova/api/ec2/admin.py 2011-07-14 20:24:25 +0000 |
1764 | @@ -22,7 +22,10 @@ |
1765 | |
1766 | import base64 |
1767 | import datetime |
1768 | +import netaddr |
1769 | +import urllib |
1770 | |
1771 | +from nova import compute |
1772 | from nova import db |
1773 | from nova import exception |
1774 | from nova import flags |
1775 | @@ -118,6 +121,9 @@ |
1776 | def __str__(self): |
1777 | return 'AdminController' |
1778 | |
1779 | + def __init__(self): |
1780 | + self.compute_api = compute.API() |
1781 | + |
1782 | def describe_instance_types(self, context, **_kwargs): |
1783 | """Returns all active instance types data (vcpus, memory, etc.)""" |
1784 | return {'instanceTypeSet': [instance_dict(v) for v in |
1785 | @@ -305,7 +311,7 @@ |
1786 | * Volume Count |
1787 | """ |
1788 | services = db.service_get_all(context, False) |
1789 | - now = datetime.datetime.utcnow() |
1790 | + now = utils.utcnow() |
1791 | hosts = [] |
1792 | rv = [] |
1793 | for host in [service['host'] for service in services]: |
1794 | @@ -326,6 +332,60 @@ |
1795 | now)) |
1796 | return {'hosts': rv} |
1797 | |
1798 | - def describe_host(self, _context, name, **_kwargs): |
1799 | - """Returns status info for single node.""" |
1800 | - return host_dict(db.host_get(name)) |
1801 | + def _provider_fw_rule_exists(self, context, rule): |
1802 | + # TODO(todd): we call this repeatedly, can we filter by protocol? |
1803 | + for old_rule in db.provider_fw_rule_get_all(context): |
1804 | + if all([rule[k] == old_rule[k] for k in ('cidr', 'from_port', |
1805 | + 'to_port', 'protocol')]): |
1806 | + return True |
1807 | + return False |
1808 | + |
1809 | + def block_external_addresses(self, context, cidr): |
1810 | + """Add provider-level firewall rules to block incoming traffic.""" |
1811 | + LOG.audit(_('Blocking traffic to all projects incoming from %s'), |
1812 | + cidr, context=context) |
1813 | + cidr = urllib.unquote(cidr).decode() |
1814 | + # raise if invalid |
1815 | + netaddr.IPNetwork(cidr) |
1816 | + rule = {'cidr': cidr} |
1817 | + tcp_rule = rule.copy() |
1818 | + tcp_rule.update({'protocol': 'tcp', 'from_port': 1, 'to_port': 65535}) |
1819 | + udp_rule = rule.copy() |
1820 | + udp_rule.update({'protocol': 'udp', 'from_port': 1, 'to_port': 65535}) |
1821 | + icmp_rule = rule.copy() |
1822 | + icmp_rule.update({'protocol': 'icmp', 'from_port': -1, |
1823 | + 'to_port': None}) |
1824 | + rules_added = 0 |
1825 | + if not self._provider_fw_rule_exists(context, tcp_rule): |
1826 | + db.provider_fw_rule_create(context, tcp_rule) |
1827 | + rules_added += 1 |
1828 | + if not self._provider_fw_rule_exists(context, udp_rule): |
1829 | + db.provider_fw_rule_create(context, udp_rule) |
1830 | + rules_added += 1 |
1831 | + if not self._provider_fw_rule_exists(context, icmp_rule): |
1832 | + db.provider_fw_rule_create(context, icmp_rule) |
1833 | + rules_added += 1 |
1834 | + if not rules_added: |
1835 | + raise exception.ApiError(_('Duplicate rule')) |
1836 | + self.compute_api.trigger_provider_fw_rules_refresh(context) |
1837 | + return {'status': 'OK', 'message': 'Added %s rules' % rules_added} |
1838 | + |
1839 | + def describe_external_address_blocks(self, context): |
1840 | + blocks = db.provider_fw_rule_get_all(context) |
1841 | + # NOTE(todd): use a set since we have icmp/udp/tcp rules with same cidr |
1842 | + blocks = set([b.cidr for b in blocks]) |
1843 | + blocks = [{'cidr': b} for b in blocks] |
1844 | + return {'externalIpBlockInfo': |
1845 | + list(sorted(blocks, key=lambda k: k['cidr']))} |
1846 | + |
1847 | + def remove_external_address_block(self, context, cidr): |
1848 | + LOG.audit(_('Removing ip block from %s'), cidr, context=context) |
1849 | + cidr = urllib.unquote(cidr).decode() |
1850 | + # raise if invalid |
1851 | + netaddr.IPNetwork(cidr) |
1852 | + rules = db.provider_fw_rule_get_all_by_cidr(context, cidr) |
1853 | + for rule in rules: |
1854 | + db.provider_fw_rule_destroy(context, rule['id']) |
1855 | + if rules: |
1856 | + self.compute_api.trigger_provider_fw_rules_refresh(context) |
1857 | + return {'status': 'OK', 'message': 'Deleted %s rules' % len(rules)} |
1858 | |
1859 | === modified file 'nova/api/ec2/apirequest.py' |
1860 | --- nova/api/ec2/apirequest.py 2011-04-18 20:53:09 +0000 |
1861 | +++ nova/api/ec2/apirequest.py 2011-07-14 20:24:25 +0000 |
1862 | @@ -21,22 +21,15 @@ |
1863 | """ |
1864 | |
1865 | import datetime |
1866 | -import re |
1867 | # TODO(termie): replace minidom with etree |
1868 | from xml.dom import minidom |
1869 | |
1870 | from nova import log as logging |
1871 | +from nova.api.ec2 import ec2utils |
1872 | |
1873 | LOG = logging.getLogger("nova.api.request") |
1874 | |
1875 | |
1876 | -_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') |
1877 | - |
1878 | - |
1879 | -def _camelcase_to_underscore(str): |
1880 | - return _c2u.sub(r'_\1', str).lower().strip('_') |
1881 | - |
1882 | - |
1883 | def _underscore_to_camelcase(str): |
1884 | return ''.join([x[:1].upper() + x[1:] for x in str.split('_')]) |
1885 | |
1886 | @@ -51,59 +44,6 @@ |
1887 | return datetimeobj.strftime("%Y-%m-%dT%H:%M:%SZ") |
1888 | |
1889 | |
1890 | -def _try_convert(value): |
1891 | - """Return a non-string from a string or unicode, if possible. |
1892 | - |
1893 | - ============= ===================================================== |
1894 | - When value is returns |
1895 | - ============= ===================================================== |
1896 | - zero-length '' |
1897 | - 'None' None |
1898 | - 'True' True |
1899 | - 'False' False |
1900 | - '0', '-0' 0 |
1901 | - 0xN, -0xN int from hex (postitive) (N is any number) |
1902 | - 0bN, -0bN int from binary (positive) (N is any number) |
1903 | - * try conversion to int, float, complex, fallback value |
1904 | - |
1905 | - """ |
1906 | - if len(value) == 0: |
1907 | - return '' |
1908 | - if value == 'None': |
1909 | - return None |
1910 | - if value == 'True': |
1911 | - return True |
1912 | - if value == 'False': |
1913 | - return False |
1914 | - valueneg = value[1:] if value[0] == '-' else value |
1915 | - if valueneg == '0': |
1916 | - return 0 |
1917 | - if valueneg == '': |
1918 | - return value |
1919 | - if valueneg[0] == '0': |
1920 | - if valueneg[1] in 'xX': |
1921 | - return int(value, 16) |
1922 | - elif valueneg[1] in 'bB': |
1923 | - return int(value, 2) |
1924 | - else: |
1925 | - try: |
1926 | - return int(value, 8) |
1927 | - except ValueError: |
1928 | - pass |
1929 | - try: |
1930 | - return int(value) |
1931 | - except ValueError: |
1932 | - pass |
1933 | - try: |
1934 | - return float(value) |
1935 | - except ValueError: |
1936 | - pass |
1937 | - try: |
1938 | - return complex(value) |
1939 | - except ValueError: |
1940 | - return value |
1941 | - |
1942 | - |
1943 | class APIRequest(object): |
1944 | def __init__(self, controller, action, version, args): |
1945 | self.controller = controller |
1946 | @@ -114,7 +54,7 @@ |
1947 | def invoke(self, context): |
1948 | try: |
1949 | method = getattr(self.controller, |
1950 | - _camelcase_to_underscore(self.action)) |
1951 | + ec2utils.camelcase_to_underscore(self.action)) |
1952 | except AttributeError: |
1953 | controller = self.controller |
1954 | action = self.action |
1955 | @@ -125,19 +65,7 @@ |
1956 | # and reraise as 400 error. |
1957 | raise Exception(_error) |
1958 | |
1959 | - args = {} |
1960 | - for key, value in self.args.items(): |
1961 | - parts = key.split(".") |
1962 | - key = _camelcase_to_underscore(parts[0]) |
1963 | - if isinstance(value, str) or isinstance(value, unicode): |
1964 | - # NOTE(vish): Automatically convert strings back |
1965 | - # into their respective values |
1966 | - value = _try_convert(value) |
1967 | - if len(parts) > 1: |
1968 | - d = args.get(key, {}) |
1969 | - d[parts[1]] = value |
1970 | - value = d |
1971 | - args[key] = value |
1972 | + args = ec2utils.dict_from_dotted_str(self.args.items()) |
1973 | |
1974 | for key in args.keys(): |
1975 | # NOTE(vish): Turn numeric dict keys into lists |
1976 | |
1977 | === modified file 'nova/api/ec2/cloud.py' |
1978 | --- nova/api/ec2/cloud.py 2011-05-20 06:51:29 +0000 |
1979 | +++ nova/api/ec2/cloud.py 2011-07-14 20:24:25 +0000 |
1980 | @@ -23,8 +23,7 @@ |
1981 | """ |
1982 | |
1983 | import base64 |
1984 | -import datetime |
1985 | -import IPy |
1986 | +import netaddr |
1987 | import os |
1988 | import urllib |
1989 | import tempfile |
1990 | @@ -40,6 +39,7 @@ |
1991 | from nova import ipv6 |
1992 | from nova import log as logging |
1993 | from nova import network |
1994 | +from nova import rpc |
1995 | from nova import utils |
1996 | from nova import volume |
1997 | from nova.api.ec2 import ec2utils |
1998 | @@ -86,8 +86,7 @@ |
1999 | self.volume_api = volume.API() |
2000 | self.compute_api = compute.API( |
2001 | network_api=self.network_api, |
2002 | - volume_api=self.volume_api, |
2003 | - hostname_factory=ec2utils.id_to_ec2_id) |
2004 | + volume_api=self.volume_api) |
2005 | self.setup() |
2006 | |
2007 | def __str__(self): |
2008 | @@ -121,8 +120,8 @@ |
2009 | result = {} |
2010 | for instance in self.compute_api.get_all(context, |
2011 | project_id=project_id): |
2012 | - if instance['fixed_ip']: |
2013 | - line = '%s slots=%d' % (instance['fixed_ip']['address'], |
2014 | + if instance['fixed_ips']: |
2015 | + line = '%s slots=%d' % (instance['fixed_ips'][0]['address'], |
2016 | instance['vcpus']) |
2017 | key = str(instance['key_name']) |
2018 | if key in result: |
2019 | @@ -137,6 +136,13 @@ |
2020 | return services[0]['availability_zone'] |
2021 | return 'unknown zone' |
2022 | |
2023 | + def _get_image_state(self, image): |
2024 | + # NOTE(vish): fallback status if image_state isn't set |
2025 | + state = image.get('status') |
2026 | + if state == 'active': |
2027 | + state = 'available' |
2028 | + return image['properties'].get('image_state', state) |
2029 | + |
2030 | def get_metadata(self, address): |
2031 | ctxt = context.get_admin_context() |
2032 | instance_ref = self.compute_api.get_all(ctxt, fixed_ip=address) |
2033 | @@ -145,7 +151,7 @@ |
2034 | |
2035 | # This ensures that all attributes of the instance |
2036 | # are populated. |
2037 | - instance_ref = db.instance_get(ctxt, instance_ref['id']) |
2038 | + instance_ref = db.instance_get(ctxt, instance_ref[0]['id']) |
2039 | |
2040 | mpi = self._get_mpi_data(ctxt, instance_ref['project_id']) |
2041 | if instance_ref['key_name']: |
2042 | @@ -159,7 +165,10 @@ |
2043 | floating_ip = db.instance_get_floating_address(ctxt, |
2044 | instance_ref['id']) |
2045 | ec2_id = ec2utils.id_to_ec2_id(instance_ref['id']) |
2046 | - image_ec2_id = self.image_ec2_id(instance_ref['image_id']) |
2047 | + image_ec2_id = self.image_ec2_id(instance_ref['image_ref']) |
2048 | + security_groups = db.security_group_get_by_instance(ctxt, |
2049 | + instance_ref['id']) |
2050 | + security_groups = [x['name'] for x in security_groups] |
2051 | data = { |
2052 | 'user-data': base64.b64decode(instance_ref['user_data']), |
2053 | 'meta-data': { |
2054 | @@ -183,7 +192,7 @@ |
2055 | 'public-ipv4': floating_ip or '', |
2056 | 'public-keys': keys, |
2057 | 'reservation-id': instance_ref['reservation_id'], |
2058 | - 'security-groups': '', |
2059 | + 'security-groups': security_groups, |
2060 | 'mpi': mpi}} |
2061 | |
2062 | for image_type in ['kernel', 'ramdisk']: |
2063 | @@ -235,7 +244,7 @@ |
2064 | 'zoneState': 'available'}]} |
2065 | |
2066 | services = db.service_get_all(context, False) |
2067 | - now = datetime.datetime.utcnow() |
2068 | + now = utils.utcnow() |
2069 | hosts = [] |
2070 | for host in [service['host'] for service in services]: |
2071 | if not host in hosts: |
2072 | @@ -283,14 +292,50 @@ |
2073 | owner=None, |
2074 | restorable_by=None, |
2075 | **kwargs): |
2076 | - return {'snapshotSet': [{'snapshotId': 'fixme', |
2077 | - 'volumeId': 'fixme', |
2078 | - 'status': 'fixme', |
2079 | - 'startTime': 'fixme', |
2080 | - 'progress': 'fixme', |
2081 | - 'ownerId': 'fixme', |
2082 | - 'volumeSize': 0, |
2083 | - 'description': 'fixme'}]} |
2084 | + if snapshot_id: |
2085 | + snapshots = [] |
2086 | + for ec2_id in snapshot_id: |
2087 | + internal_id = ec2utils.ec2_id_to_id(ec2_id) |
2088 | + snapshot = self.volume_api.get_snapshot( |
2089 | + context, |
2090 | + snapshot_id=internal_id) |
2091 | + snapshots.append(snapshot) |
2092 | + else: |
2093 | + snapshots = self.volume_api.get_all_snapshots(context) |
2094 | + snapshots = [self._format_snapshot(context, s) for s in snapshots] |
2095 | + return {'snapshotSet': snapshots} |
2096 | + |
2097 | + def _format_snapshot(self, context, snapshot): |
2098 | + s = {} |
2099 | + s['snapshotId'] = ec2utils.id_to_ec2_id(snapshot['id'], 'snap-%08x') |
2100 | + s['volumeId'] = ec2utils.id_to_ec2_id(snapshot['volume_id'], |
2101 | + 'vol-%08x') |
2102 | + s['status'] = snapshot['status'] |
2103 | + s['startTime'] = snapshot['created_at'] |
2104 | + s['progress'] = snapshot['progress'] |
2105 | + s['ownerId'] = snapshot['project_id'] |
2106 | + s['volumeSize'] = snapshot['volume_size'] |
2107 | + s['description'] = snapshot['display_description'] |
2108 | + |
2109 | + s['display_name'] = snapshot['display_name'] |
2110 | + s['display_description'] = snapshot['display_description'] |
2111 | + return s |
2112 | + |
2113 | + def create_snapshot(self, context, volume_id, **kwargs): |
2114 | + LOG.audit(_("Create snapshot of volume %s"), volume_id, |
2115 | + context=context) |
2116 | + volume_id = ec2utils.ec2_id_to_id(volume_id) |
2117 | + snapshot = self.volume_api.create_snapshot( |
2118 | + context, |
2119 | + volume_id=volume_id, |
2120 | + name=kwargs.get('display_name'), |
2121 | + description=kwargs.get('display_description')) |
2122 | + return self._format_snapshot(context, snapshot) |
2123 | + |
2124 | + def delete_snapshot(self, context, snapshot_id, **kwargs): |
2125 | + snapshot_id = ec2utils.ec2_id_to_id(snapshot_id) |
2126 | + self.volume_api.delete_snapshot(context, snapshot_id=snapshot_id) |
2127 | + return True |
2128 | |
2129 | def describe_key_pairs(self, context, key_name=None, **kwargs): |
2130 | key_pairs = db.key_pair_get_all_by_user(context, context.user_id) |
2131 | @@ -348,15 +393,21 @@ |
2132 | pass |
2133 | return True |
2134 | |
2135 | - def describe_security_groups(self, context, group_name=None, **kwargs): |
2136 | + def describe_security_groups(self, context, group_name=None, group_id=None, |
2137 | + **kwargs): |
2138 | self.compute_api.ensure_default_security_group(context) |
2139 | - if group_name: |
2140 | + if group_name or group_id: |
2141 | groups = [] |
2142 | - for name in group_name: |
2143 | - group = db.security_group_get_by_name(context, |
2144 | - context.project_id, |
2145 | - name) |
2146 | - groups.append(group) |
2147 | + if group_name: |
2148 | + for name in group_name: |
2149 | + group = db.security_group_get_by_name(context, |
2150 | + context.project_id, |
2151 | + name) |
2152 | + groups.append(group) |
2153 | + if group_id: |
2154 | + for gid in group_id: |
2155 | + group = db.security_group_get(context, gid) |
2156 | + groups.append(group) |
2157 | elif context.is_admin: |
2158 | groups = db.security_group_get_all(context) |
2159 | else: |
2160 | @@ -409,7 +460,7 @@ |
2161 | elif cidr_ip: |
2162 | # If this fails, it throws an exception. This is what we want. |
2163 | cidr_ip = urllib.unquote(cidr_ip).decode() |
2164 | - IPy.IP(cidr_ip) |
2165 | + netaddr.IPNetwork(cidr_ip) |
2166 | values['cidr'] = cidr_ip |
2167 | else: |
2168 | values['cidr'] = '0.0.0.0/0' |
2169 | @@ -454,13 +505,26 @@ |
2170 | return True |
2171 | return False |
2172 | |
2173 | - def revoke_security_group_ingress(self, context, group_name, **kwargs): |
2174 | - LOG.audit(_("Revoke security group ingress %s"), group_name, |
2175 | - context=context) |
2176 | + def revoke_security_group_ingress(self, context, group_name=None, |
2177 | + group_id=None, **kwargs): |
2178 | + if not group_name and not group_id: |
2179 | + err = "Not enough parameters, need group_name or group_id" |
2180 | + raise exception.ApiError(_(err)) |
2181 | self.compute_api.ensure_default_security_group(context) |
2182 | - security_group = db.security_group_get_by_name(context, |
2183 | - context.project_id, |
2184 | - group_name) |
2185 | + notfound = exception.SecurityGroupNotFound |
2186 | + if group_name: |
2187 | + security_group = db.security_group_get_by_name(context, |
2188 | + context.project_id, |
2189 | + group_name) |
2190 | + if not security_group: |
2191 | + raise notfound(security_group_id=group_name) |
2192 | + if group_id: |
2193 | + security_group = db.security_group_get(context, group_id) |
2194 | + if not security_group: |
2195 | + raise notfound(security_group_id=group_id) |
2196 | + |
2197 | + msg = "Revoke security group ingress %s" |
2198 | + LOG.audit(_(msg), security_group['name'], context=context) |
2199 | |
2200 | criteria = self._revoke_rule_args_to_dict(context, **kwargs) |
2201 | if criteria is None: |
2202 | @@ -475,7 +539,7 @@ |
2203 | if match: |
2204 | db.security_group_rule_destroy(context, rule['id']) |
2205 | self.compute_api.trigger_security_group_rules_refresh(context, |
2206 | - security_group['id']) |
2207 | + security_group_id=security_group['id']) |
2208 | return True |
2209 | raise exception.ApiError(_("No rule for the specified parameters.")) |
2210 | |
2211 | @@ -483,14 +547,26 @@ |
2212 | # Unfortunately, it seems Boto is using an old API |
2213 | # for these operations, so support for newer API versions |
2214 | # is sketchy. |
2215 | - def authorize_security_group_ingress(self, context, group_name, **kwargs): |
2216 | - LOG.audit(_("Authorize security group ingress %s"), group_name, |
2217 | - context=context) |
2218 | + def authorize_security_group_ingress(self, context, group_name=None, |
2219 | + group_id=None, **kwargs): |
2220 | + if not group_name and not group_id: |
2221 | + err = "Not enough parameters, need group_name or group_id" |
2222 | + raise exception.ApiError(_(err)) |
2223 | self.compute_api.ensure_default_security_group(context) |
2224 | - security_group = db.security_group_get_by_name(context, |
2225 | - context.project_id, |
2226 | - group_name) |
2227 | + notfound = exception.SecurityGroupNotFound |
2228 | + if group_name: |
2229 | + security_group = db.security_group_get_by_name(context, |
2230 | + context.project_id, |
2231 | + group_name) |
2232 | + if not security_group: |
2233 | + raise notfound(security_group_id=group_name) |
2234 | + if group_id: |
2235 | + security_group = db.security_group_get(context, group_id) |
2236 | + if not security_group: |
2237 | + raise notfound(security_group_id=group_id) |
2238 | |
2239 | + msg = "Authorize security group ingress %s" |
2240 | + LOG.audit(_(msg), security_group['name'], context=context) |
2241 | values = self._revoke_rule_args_to_dict(context, **kwargs) |
2242 | if values is None: |
2243 | raise exception.ApiError(_("Not enough parameters to build a " |
2244 | @@ -504,7 +580,7 @@ |
2245 | security_group_rule = db.security_group_rule_create(context, values) |
2246 | |
2247 | self.compute_api.trigger_security_group_rules_refresh(context, |
2248 | - security_group['id']) |
2249 | + security_group_id=security_group['id']) |
2250 | |
2251 | return True |
2252 | |
2253 | @@ -540,11 +616,23 @@ |
2254 | return {'securityGroupSet': [self._format_security_group(context, |
2255 | group_ref)]} |
2256 | |
2257 | - def delete_security_group(self, context, group_name, **kwargs): |
2258 | + def delete_security_group(self, context, group_name=None, group_id=None, |
2259 | + **kwargs): |
2260 | + if not group_name and not group_id: |
2261 | + err = "Not enough parameters, need group_name or group_id" |
2262 | + raise exception.ApiError(_(err)) |
2263 | + notfound = exception.SecurityGroupNotFound |
2264 | + if group_name: |
2265 | + security_group = db.security_group_get_by_name(context, |
2266 | + context.project_id, |
2267 | + group_name) |
2268 | + if not security_group: |
2269 | + raise notfound(security_group_id=group_name) |
2270 | + elif group_id: |
2271 | + security_group = db.security_group_get(context, group_id) |
2272 | + if not security_group: |
2273 | + raise notfound(security_group_id=group_id) |
2274 | LOG.audit(_("Delete security group %s"), group_name, context=context) |
2275 | - security_group = db.security_group_get_by_name(context, |
2276 | - context.project_id, |
2277 | - group_name) |
2278 | db.security_group_destroy(context, security_group.id) |
2279 | return True |
2280 | |
2281 | @@ -559,7 +647,7 @@ |
2282 | instance_id = ec2utils.ec2_id_to_id(ec2_id) |
2283 | output = self.compute_api.get_console_output( |
2284 | context, instance_id=instance_id) |
2285 | - now = datetime.datetime.utcnow() |
2286 | + now = utils.utcnow() |
2287 | return {"InstanceId": ec2_id, |
2288 | "Timestamp": now, |
2289 | "output": base64.b64encode(output)} |
2290 | @@ -619,16 +707,30 @@ |
2291 | 'volumeId': v['volumeId']}] |
2292 | else: |
2293 | v['attachmentSet'] = [{}] |
2294 | + if volume.get('snapshot_id') != None: |
2295 | + v['snapshotId'] = ec2utils.id_to_ec2_id(volume['snapshot_id'], |
2296 | + 'snap-%08x') |
2297 | + else: |
2298 | + v['snapshotId'] = None |
2299 | |
2300 | v['display_name'] = volume['display_name'] |
2301 | v['display_description'] = volume['display_description'] |
2302 | return v |
2303 | |
2304 | - def create_volume(self, context, size, **kwargs): |
2305 | - LOG.audit(_("Create volume of %s GB"), size, context=context) |
2306 | + def create_volume(self, context, **kwargs): |
2307 | + size = kwargs.get('size') |
2308 | + if kwargs.get('snapshot_id') != None: |
2309 | + snapshot_id = ec2utils.ec2_id_to_id(kwargs['snapshot_id']) |
2310 | + LOG.audit(_("Create volume from snapshot %s"), snapshot_id, |
2311 | + context=context) |
2312 | + else: |
2313 | + snapshot_id = None |
2314 | + LOG.audit(_("Create volume of %s GB"), size, context=context) |
2315 | + |
2316 | volume = self.volume_api.create( |
2317 | context, |
2318 | size=size, |
2319 | + snapshot_id=snapshot_id, |
2320 | name=kwargs.get('display_name'), |
2321 | description=kwargs.get('display_description')) |
2322 | # TODO(vish): Instance should be None at db layer instead of |
2323 | @@ -724,27 +826,27 @@ |
2324 | instances = self.compute_api.get_all(context, **kwargs) |
2325 | for instance in instances: |
2326 | if not context.is_admin: |
2327 | - if instance['image_id'] == str(FLAGS.vpn_image_id): |
2328 | + if instance['image_ref'] == str(FLAGS.vpn_image_id): |
2329 | continue |
2330 | i = {} |
2331 | instance_id = instance['id'] |
2332 | ec2_id = ec2utils.id_to_ec2_id(instance_id) |
2333 | i['instanceId'] = ec2_id |
2334 | - i['imageId'] = self.image_ec2_id(instance['image_id']) |
2335 | + i['imageId'] = self.image_ec2_id(instance['image_ref']) |
2336 | i['instanceState'] = { |
2337 | 'code': instance['state'], |
2338 | 'name': instance['state_description']} |
2339 | fixed_addr = None |
2340 | floating_addr = None |
2341 | - if instance['fixed_ip']: |
2342 | - fixed_addr = instance['fixed_ip']['address'] |
2343 | - if instance['fixed_ip']['floating_ips']: |
2344 | - fixed = instance['fixed_ip'] |
2345 | + if instance['fixed_ips']: |
2346 | + fixed = instance['fixed_ips'][0] |
2347 | + fixed_addr = fixed['address'] |
2348 | + if fixed['floating_ips']: |
2349 | floating_addr = fixed['floating_ips'][0]['address'] |
2350 | - if instance['fixed_ip']['network'] and 'use_v6' in kwargs: |
2351 | + if fixed['network'] and 'use_v6' in kwargs: |
2352 | i['dnsNameV6'] = ipv6.to_global( |
2353 | - instance['fixed_ip']['network']['cidr_v6'], |
2354 | - instance['mac_address'], |
2355 | + fixed['network']['cidr_v6'], |
2356 | + fixed['virtual_interface']['address'], |
2357 | instance['project_id']) |
2358 | |
2359 | i['privateDnsName'] = fixed_addr |
2360 | @@ -816,8 +918,15 @@ |
2361 | |
2362 | def allocate_address(self, context, **kwargs): |
2363 | LOG.audit(_("Allocate address"), context=context) |
2364 | - public_ip = self.network_api.allocate_floating_ip(context) |
2365 | - return {'publicIp': public_ip} |
2366 | + try: |
2367 | + public_ip = self.network_api.allocate_floating_ip(context) |
2368 | + return {'publicIp': public_ip} |
2369 | + except rpc.RemoteError as ex: |
2370 | + # NOTE(tr3buchet) - why does this block exist? |
2371 | + if ex.exc_type == 'NoMoreFloatingIps': |
2372 | + raise exception.NoMoreFloatingIps() |
2373 | + else: |
2374 | + raise |
2375 | |
2376 | def release_address(self, context, public_ip, **kwargs): |
2377 | LOG.audit(_("Release address %s"), public_ip, context=context) |
2378 | @@ -846,10 +955,39 @@ |
2379 | if kwargs.get('ramdisk_id'): |
2380 | ramdisk = self._get_image(context, kwargs['ramdisk_id']) |
2381 | kwargs['ramdisk_id'] = ramdisk['id'] |
2382 | + for bdm in kwargs.get('block_device_mapping', []): |
2383 | + # NOTE(yamahata) |
2384 | + # BlockDevicedMapping.<N>.DeviceName |
2385 | + # BlockDevicedMapping.<N>.Ebs.SnapshotId |
2386 | + # BlockDevicedMapping.<N>.Ebs.VolumeSize |
2387 | + # BlockDevicedMapping.<N>.Ebs.DeleteOnTermination |
2388 | + # BlockDevicedMapping.<N>.VirtualName |
2389 | + # => remove .Ebs and allow volume id in SnapshotId |
2390 | + ebs = bdm.pop('ebs', None) |
2391 | + if ebs: |
2392 | + ec2_id = ebs.pop('snapshot_id') |
2393 | + id = ec2utils.ec2_id_to_id(ec2_id) |
2394 | + if ec2_id.startswith('snap-'): |
2395 | + bdm['snapshot_id'] = id |
2396 | + elif ec2_id.startswith('vol-'): |
2397 | + bdm['volume_id'] = id |
2398 | + ebs.setdefault('delete_on_termination', True) |
2399 | + bdm.update(ebs) |
2400 | + |
2401 | + image = self._get_image(context, kwargs['image_id']) |
2402 | + |
2403 | + if image: |
2404 | + image_state = self._get_image_state(image) |
2405 | + else: |
2406 | + raise exception.ImageNotFound(image_id=kwargs['image_id']) |
2407 | + |
2408 | + if image_state != 'available': |
2409 | + raise exception.ApiError(_('Image must be available')) |
2410 | + |
2411 | instances = self.compute_api.create(context, |
2412 | instance_type=instance_types.get_instance_type_by_name( |
2413 | kwargs.get('instance_type', None)), |
2414 | - image_id=self._get_image(context, kwargs['image_id'])['id'], |
2415 | + image_href=self._get_image(context, kwargs['image_id'])['id'], |
2416 | min_count=int(kwargs.get('min_count', max_count)), |
2417 | max_count=max_count, |
2418 | kernel_id=kwargs.get('kernel_id'), |
2419 | @@ -860,37 +998,54 @@ |
2420 | user_data=kwargs.get('user_data'), |
2421 | security_group=kwargs.get('security_group'), |
2422 | availability_zone=kwargs.get('placement', {}).get( |
2423 | - 'AvailabilityZone')) |
2424 | + 'AvailabilityZone'), |
2425 | + block_device_mapping=kwargs.get('block_device_mapping', {})) |
2426 | return self._format_run_instances(context, |
2427 | instances[0]['reservation_id']) |
2428 | |
2429 | + def _do_instance(self, action, context, ec2_id): |
2430 | + instance_id = ec2utils.ec2_id_to_id(ec2_id) |
2431 | + action(context, instance_id=instance_id) |
2432 | + |
2433 | + def _do_instances(self, action, context, instance_id): |
2434 | + for ec2_id in instance_id: |
2435 | + self._do_instance(action, context, ec2_id) |
2436 | + |
2437 | def terminate_instances(self, context, instance_id, **kwargs): |
2438 | """Terminate each instance in instance_id, which is a list of ec2 ids. |
2439 | instance_id is a kwarg so its name cannot be modified.""" |
2440 | LOG.debug(_("Going to start terminating instances")) |
2441 | - for ec2_id in instance_id: |
2442 | - instance_id = ec2utils.ec2_id_to_id(ec2_id) |
2443 | - self.compute_api.delete(context, instance_id=instance_id) |
2444 | + self._do_instances(self.compute_api.delete, context, instance_id) |
2445 | return True |
2446 | |
2447 | def reboot_instances(self, context, instance_id, **kwargs): |
2448 | """instance_id is a list of instance ids""" |
2449 | LOG.audit(_("Reboot instance %r"), instance_id, context=context) |
2450 | - for ec2_id in instance_id: |
2451 | - instance_id = ec2utils.ec2_id_to_id(ec2_id) |
2452 | - self.compute_api.reboot(context, instance_id=instance_id) |
2453 | + self._do_instances(self.compute_api.reboot, context, instance_id) |
2454 | + return True |
2455 | + |
2456 | + def stop_instances(self, context, instance_id, **kwargs): |
2457 | + """Stop each instances in instance_id. |
2458 | + Here instance_id is a list of instance ids""" |
2459 | + LOG.debug(_("Going to stop instances")) |
2460 | + self._do_instances(self.compute_api.stop, context, instance_id) |
2461 | + return True |
2462 | + |
2463 | + def start_instances(self, context, instance_id, **kwargs): |
2464 | + """Start each instances in instance_id. |
2465 | + Here instance_id is a list of instance ids""" |
2466 | + LOG.debug(_("Going to start instances")) |
2467 | + self._do_instances(self.compute_api.start, context, instance_id) |
2468 | return True |
2469 | |
2470 | def rescue_instance(self, context, instance_id, **kwargs): |
2471 | """This is an extension to the normal ec2_api""" |
2472 | - instance_id = ec2utils.ec2_id_to_id(instance_id) |
2473 | - self.compute_api.rescue(context, instance_id=instance_id) |
2474 | + self._do_instance(self.compute_api.rescue, contect, instnace_id) |
2475 | return True |
2476 | |
2477 | def unrescue_instance(self, context, instance_id, **kwargs): |
2478 | """This is an extension to the normal ec2_api""" |
2479 | - instance_id = ec2utils.ec2_id_to_id(instance_id) |
2480 | - self.compute_api.unrescue(context, instance_id=instance_id) |
2481 | + self._do_instance(self.compute_api.unrescue, context, instance_id) |
2482 | return True |
2483 | |
2484 | def update_instance(self, context, instance_id, **kwargs): |
2485 | @@ -901,7 +1056,8 @@ |
2486 | changes[field] = kwargs[field] |
2487 | if changes: |
2488 | instance_id = ec2utils.ec2_id_to_id(instance_id) |
2489 | - self.compute_api.update(context, instance_id=instance_id, **kwargs) |
2490 | + self.compute_api.update(context, instance_id=instance_id, |
2491 | + **changes) |
2492 | return True |
2493 | |
2494 | @staticmethod |
2495 | @@ -925,17 +1081,26 @@ |
2496 | def image_ec2_id(image_id, image_type='ami'): |
2497 | """Returns image ec2_id using id and three letter type.""" |
2498 | template = image_type + '-%08x' |
2499 | - return ec2utils.id_to_ec2_id(int(image_id), template=template) |
2500 | + try: |
2501 | + return ec2utils.id_to_ec2_id(int(image_id), template=template) |
2502 | + except ValueError: |
2503 | + #TODO(wwolf): once we have ec2_id -> glance_id mapping |
2504 | + # in place, this wont be necessary |
2505 | + return "ami-00000000" |
2506 | |
2507 | def _get_image(self, context, ec2_id): |
2508 | try: |
2509 | internal_id = ec2utils.ec2_id_to_id(ec2_id) |
2510 | - return self.image_service.show(context, internal_id) |
2511 | + image = self.image_service.show(context, internal_id) |
2512 | except (exception.InvalidEc2Id, exception.ImageNotFound): |
2513 | try: |
2514 | return self.image_service.show_by_name(context, ec2_id) |
2515 | except exception.NotFound: |
2516 | raise exception.ImageNotFound(image_id=ec2_id) |
2517 | + image_type = ec2_id.split('-')[0] |
2518 | + if self._image_type(image.get('container_format')) != image_type: |
2519 | + raise exception.ImageNotFound(image_id=ec2_id) |
2520 | + return image |
2521 | |
2522 | def _format_image(self, image): |
2523 | """Convert from format defined by BaseImageService to S3 format.""" |
2524 | @@ -956,11 +1121,8 @@ |
2525 | get('image_location'), name) |
2526 | else: |
2527 | i['imageLocation'] = image['properties'].get('image_location') |
2528 | - # NOTE(vish): fallback status if image_state isn't set |
2529 | - state = image.get('status') |
2530 | - if state == 'active': |
2531 | - state = 'available' |
2532 | - i['imageState'] = image['properties'].get('image_state', state) |
2533 | + |
2534 | + i['imageState'] = self._get_image_state(image) |
2535 | i['displayName'] = name |
2536 | i['description'] = image.get('description') |
2537 | display_mapping = {'aki': 'kernel', |
2538 | |
2539 | === modified file 'nova/api/ec2/ec2utils.py' |
2540 | --- nova/api/ec2/ec2utils.py 2011-05-11 18:02:01 +0000 |
2541 | +++ nova/api/ec2/ec2utils.py 2011-07-14 20:24:25 +0000 |
2542 | @@ -16,6 +16,8 @@ |
2543 | # License for the specific language governing permissions and limitations |
2544 | # under the License. |
2545 | |
2546 | +import re |
2547 | + |
2548 | from nova import exception |
2549 | |
2550 | |
2551 | @@ -30,3 +32,95 @@ |
2552 | def id_to_ec2_id(instance_id, template='i-%08x'): |
2553 | """Convert an instance ID (int) to an ec2 ID (i-[base 16 number])""" |
2554 | return template % instance_id |
2555 | + |
2556 | + |
2557 | +_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') |
2558 | + |
2559 | + |
2560 | +def camelcase_to_underscore(str): |
2561 | + return _c2u.sub(r'_\1', str).lower().strip('_') |
2562 | + |
2563 | + |
2564 | +def _try_convert(value): |
2565 | + """Return a non-string from a string or unicode, if possible. |
2566 | + |
2567 | + ============= ===================================================== |
2568 | + When value is returns |
2569 | + ============= ===================================================== |
2570 | + zero-length '' |
2571 | + 'None' None |
2572 | + 'True' True case insensitive |
2573 | + 'False' False case insensitive |
2574 | + '0', '-0' 0 |
2575 | + 0xN, -0xN int from hex (postitive) (N is any number) |
2576 | + 0bN, -0bN int from binary (positive) (N is any number) |
2577 | + * try conversion to int, float, complex, fallback value |
2578 | + |
2579 | + """ |
2580 | + if len(value) == 0: |
2581 | + return '' |
2582 | + if value == 'None': |
2583 | + return None |
2584 | + lowered_value = value.lower() |
2585 | + if lowered_value == 'true': |
2586 | + return True |
2587 | + if lowered_value == 'false': |
2588 | + return False |
2589 | + valueneg = value[1:] if value[0] == '-' else value |
2590 | + if valueneg == '0': |
2591 | + return 0 |
2592 | + if valueneg == '': |
2593 | + return value |
2594 | + if valueneg[0] == '0': |
2595 | + if valueneg[1] in 'xX': |
2596 | + return int(value, 16) |
2597 | + elif valueneg[1] in 'bB': |
2598 | + return int(value, 2) |
2599 | + else: |
2600 | + try: |
2601 | + return int(value, 8) |
2602 | + except ValueError: |
2603 | + pass |
2604 | + try: |
2605 | + return int(value) |
2606 | + except ValueError: |
2607 | + pass |
2608 | + try: |
2609 | + return float(value) |
2610 | + except ValueError: |
2611 | + pass |
2612 | + try: |
2613 | + return complex(value) |
2614 | + except ValueError: |
2615 | + return value |
2616 | + |
2617 | + |
2618 | +def dict_from_dotted_str(items): |
2619 | + """parse multi dot-separated argument into dict. |
2620 | + EBS boot uses multi dot-separeted arguments like |
2621 | + BlockDeviceMapping.1.DeviceName=snap-id |
2622 | + Convert the above into |
2623 | + {'block_device_mapping': {'1': {'device_name': snap-id}}} |
2624 | + """ |
2625 | + args = {} |
2626 | + for key, value in items: |
2627 | + parts = key.split(".") |
2628 | + key = camelcase_to_underscore(parts[0]) |
2629 | + if isinstance(value, str) or isinstance(value, unicode): |
2630 | + # NOTE(vish): Automatically convert strings back |
2631 | + # into their respective values |
2632 | + value = _try_convert(value) |
2633 | + |
2634 | + if len(parts) > 1: |
2635 | + d = args.get(key, {}) |
2636 | + args[key] = d |
2637 | + for k in parts[1:-1]: |
2638 | + k = camelcase_to_underscore(k) |
2639 | + v = d.get(k, {}) |
2640 | + d[k] = v |
2641 | + d = v |
2642 | + d[camelcase_to_underscore(parts[-1])] = value |
2643 | + else: |
2644 | + args[key] = value |
2645 | + |
2646 | + return args |
2647 | |
2648 | === modified file 'nova/api/ec2/metadatarequesthandler.py' |
2649 | --- nova/api/ec2/metadatarequesthandler.py 2011-03-03 16:04:33 +0000 |
2650 | +++ nova/api/ec2/metadatarequesthandler.py 2011-07-14 20:24:25 +0000 |
2651 | @@ -23,6 +23,7 @@ |
2652 | |
2653 | from nova import log as logging |
2654 | from nova import flags |
2655 | +from nova import utils |
2656 | from nova import wsgi |
2657 | from nova.api.ec2 import cloud |
2658 | |
2659 | @@ -34,6 +35,9 @@ |
2660 | class MetadataRequestHandler(wsgi.Application): |
2661 | """Serve metadata from the EC2 API.""" |
2662 | |
2663 | + def __init__(self): |
2664 | + self.cc = cloud.CloudController() |
2665 | + |
2666 | def print_data(self, data): |
2667 | if isinstance(data, dict): |
2668 | output = '' |
2669 | @@ -67,11 +71,18 @@ |
2670 | |
2671 | @webob.dec.wsgify(RequestClass=wsgi.Request) |
2672 | def __call__(self, req): |
2673 | - cc = cloud.CloudController() |
2674 | remote_address = req.remote_addr |
2675 | if FLAGS.use_forwarded_for: |
2676 | remote_address = req.headers.get('X-Forwarded-For', remote_address) |
2677 | - meta_data = cc.get_metadata(remote_address) |
2678 | + try: |
2679 | + meta_data = self.cc.get_metadata(remote_address) |
2680 | + except Exception: |
2681 | + LOG.exception(_('Failed to get metadata for ip: %s'), |
2682 | + remote_address) |
2683 | + msg = _('An unknown error has occurred. ' |
2684 | + 'Please try your request again.') |
2685 | + exc = webob.exc.HTTPInternalServerError(explanation=unicode(msg)) |
2686 | + return exc |
2687 | if meta_data is None: |
2688 | LOG.error(_('Failed to get metadata for ip: %s'), remote_address) |
2689 | raise webob.exc.HTTPNotFound() |
2690 | |
2691 | === modified file 'nova/api/openstack/__init__.py' |
2692 | --- nova/api/openstack/__init__.py 2011-05-13 15:17:19 +0000 |
2693 | +++ nova/api/openstack/__init__.py 2011-07-14 20:24:25 +0000 |
2694 | @@ -26,7 +26,7 @@ |
2695 | |
2696 | from nova import flags |
2697 | from nova import log as logging |
2698 | -from nova import wsgi |
2699 | +from nova import wsgi as base_wsgi |
2700 | from nova.api.openstack import accounts |
2701 | from nova.api.openstack import faults |
2702 | from nova.api.openstack import backup_schedules |
2703 | @@ -40,6 +40,7 @@ |
2704 | from nova.api.openstack import server_metadata |
2705 | from nova.api.openstack import shared_ip_groups |
2706 | from nova.api.openstack import users |
2707 | +from nova.api.openstack import wsgi |
2708 | from nova.api.openstack import zones |
2709 | |
2710 | |
2711 | @@ -50,7 +51,7 @@ |
2712 | 'When True, this API service will accept admin operations.') |
2713 | |
2714 | |
2715 | -class FaultWrapper(wsgi.Middleware): |
2716 | +class FaultWrapper(base_wsgi.Middleware): |
2717 | """Calls down the middleware stack, making exceptions into faults.""" |
2718 | |
2719 | @webob.dec.wsgify(RequestClass=wsgi.Request) |
2720 | @@ -63,7 +64,7 @@ |
2721 | return faults.Fault(exc) |
2722 | |
2723 | |
2724 | -class APIRouter(wsgi.Router): |
2725 | +class APIRouter(base_wsgi.Router): |
2726 | """ |
2727 | Routes requests on the OpenStack API to the appropriate controller |
2728 | and method. |
2729 | @@ -80,7 +81,9 @@ |
2730 | self._setup_routes(mapper) |
2731 | super(APIRouter, self).__init__(mapper) |
2732 | |
2733 | - def _setup_routes(self, mapper): |
2734 | + def _setup_routes(self, mapper, version): |
2735 | + """Routes common to all versions.""" |
2736 | + |
2737 | server_members = self.server_members |
2738 | server_members['action'] = 'POST' |
2739 | if FLAGS.allow_admin_api: |
2740 | @@ -97,21 +100,41 @@ |
2741 | server_members['reset_network'] = 'POST' |
2742 | server_members['inject_network_info'] = 'POST' |
2743 | |
2744 | - mapper.resource("zone", "zones", controller=zones.Controller(), |
2745 | - collection={'detail': 'GET', 'info': 'GET', |
2746 | - 'select': 'GET'}) |
2747 | - |
2748 | - mapper.resource("user", "users", controller=users.Controller(), |
2749 | + mapper.resource("user", "users", |
2750 | + controller=users.create_resource(), |
2751 | collection={'detail': 'GET'}) |
2752 | |
2753 | mapper.resource("account", "accounts", |
2754 | - controller=accounts.Controller(), |
2755 | + controller=accounts.create_resource(), |
2756 | collection={'detail': 'GET'}) |
2757 | |
2758 | + mapper.resource("zone", "zones", |
2759 | + controller=zones.create_resource(version), |
2760 | + collection={'detail': 'GET', |
2761 | + 'info': 'GET', |
2762 | + 'select': 'POST', |
2763 | + 'boot': 'POST'}) |
2764 | + |
2765 | mapper.resource("console", "consoles", |
2766 | - controller=consoles.Controller(), |
2767 | - parent_resource=dict(member_name='server', |
2768 | - collection_name='servers')) |
2769 | + controller=consoles.create_resource(), |
2770 | + parent_resource=dict(member_name='server', |
2771 | + collection_name='servers')) |
2772 | + |
2773 | + mapper.resource("server", "servers", |
2774 | + controller=servers.create_resource(version), |
2775 | + collection={'detail': 'GET'}, |
2776 | + member=self.server_members) |
2777 | + |
2778 | + mapper.resource("image", "images", |
2779 | + controller=images.create_resource(version), |
2780 | + collection={'detail': 'GET'}) |
2781 | + |
2782 | + mapper.resource("limit", "limits", |
2783 | + controller=limits.create_resource(version)) |
2784 | + |
2785 | + mapper.resource("flavor", "flavors", |
2786 | + controller=flavors.create_resource(version), |
2787 | + collection={'detail': 'GET'}) |
2788 | |
2789 | super(APIRouter, self).__init__(mapper) |
2790 | |
2791 | @@ -120,33 +143,21 @@ |
2792 | """Define routes specific to OpenStack API V1.0.""" |
2793 | |
2794 | def _setup_routes(self, mapper): |
2795 | - super(APIRouterV10, self)._setup_routes(mapper) |
2796 | - mapper.resource("server", "servers", |
2797 | - controller=servers.ControllerV10(), |
2798 | - collection={'detail': 'GET'}, |
2799 | - member=self.server_members) |
2800 | - |
2801 | + super(APIRouterV10, self)._setup_routes(mapper, '1.0') |
2802 | mapper.resource("image", "images", |
2803 | - controller=images.ControllerV10(), |
2804 | - collection={'detail': 'GET'}) |
2805 | - |
2806 | - mapper.resource("flavor", "flavors", |
2807 | - controller=flavors.ControllerV10(), |
2808 | + controller=images.create_resource('1.0'), |
2809 | collection={'detail': 'GET'}) |
2810 | |
2811 | mapper.resource("shared_ip_group", "shared_ip_groups", |
2812 | collection={'detail': 'GET'}, |
2813 | - controller=shared_ip_groups.Controller()) |
2814 | + controller=shared_ip_groups.create_resource()) |
2815 | |
2816 | mapper.resource("backup_schedule", "backup_schedule", |
2817 | - controller=backup_schedules.Controller(), |
2818 | + controller=backup_schedules.create_resource(), |
2819 | parent_resource=dict(member_name='server', |
2820 | collection_name='servers')) |
2821 | |
2822 | - mapper.resource("limit", "limits", |
2823 | - controller=limits.LimitsControllerV10()) |
2824 | - |
2825 | - mapper.resource("ip", "ips", controller=ips.Controller(), |
2826 | + mapper.resource("ip", "ips", controller=ips.create_resource(), |
2827 | collection=dict(public='GET', private='GET'), |
2828 | parent_resource=dict(member_name='server', |
2829 | collection_name='servers')) |
2830 | @@ -156,29 +167,13 @@ |
2831 | """Define routes specific to OpenStack API V1.1.""" |
2832 | |
2833 | def _setup_routes(self, mapper): |
2834 | - super(APIRouterV11, self)._setup_routes(mapper) |
2835 | - mapper.resource("server", "servers", |
2836 | - controller=servers.ControllerV11(), |
2837 | - collection={'detail': 'GET'}, |
2838 | - member=self.server_members) |
2839 | - |
2840 | - mapper.resource("image", "images", |
2841 | - controller=images.ControllerV11(), |
2842 | - collection={'detail': 'GET'}) |
2843 | - |
2844 | + super(APIRouterV11, self)._setup_routes(mapper, '1.1') |
2845 | mapper.resource("image_meta", "meta", |
2846 | - controller=image_metadata.Controller(), |
2847 | + controller=image_metadata.create_resource(), |
2848 | parent_resource=dict(member_name='image', |
2849 | collection_name='images')) |
2850 | |
2851 | mapper.resource("server_meta", "meta", |
2852 | - controller=server_metadata.Controller(), |
2853 | + controller=server_metadata.create_resource(), |
2854 | parent_resource=dict(member_name='server', |
2855 | collection_name='servers')) |
2856 | - |
2857 | - mapper.resource("flavor", "flavors", |
2858 | - controller=flavors.ControllerV11(), |
2859 | - collection={'detail': 'GET'}) |
2860 | - |
2861 | - mapper.resource("limit", "limits", |
2862 | - controller=limits.LimitsControllerV11()) |
2863 | |
2864 | === modified file 'nova/api/openstack/accounts.py' |
2865 | --- nova/api/openstack/accounts.py 2011-04-27 21:03:05 +0000 |
2866 | +++ nova/api/openstack/accounts.py 2011-07-14 20:24:25 +0000 |
2867 | @@ -20,8 +20,9 @@ |
2868 | from nova import log as logging |
2869 | |
2870 | from nova.auth import manager |
2871 | -from nova.api.openstack import common |
2872 | from nova.api.openstack import faults |
2873 | +from nova.api.openstack import wsgi |
2874 | + |
2875 | |
2876 | FLAGS = flags.FLAGS |
2877 | LOG = logging.getLogger('nova.api.openstack') |
2878 | @@ -34,12 +35,7 @@ |
2879 | manager=account.project_manager_id) |
2880 | |
2881 | |
2882 | -class Controller(common.OpenstackController): |
2883 | - |
2884 | - _serialization_metadata = { |
2885 | - 'application/xml': { |
2886 | - "attributes": { |
2887 | - "account": ["id", "name", "description", "manager"]}}} |
2888 | +class Controller(object): |
2889 | |
2890 | def __init__(self): |
2891 | self.manager = manager.AuthManager() |
2892 | @@ -66,20 +62,33 @@ |
2893 | self.manager.delete_project(id) |
2894 | return {} |
2895 | |
2896 | - def create(self, req): |
2897 | + def create(self, req, body): |
2898 | """We use update with create-or-update semantics |
2899 | because the id comes from an external source""" |
2900 | raise faults.Fault(webob.exc.HTTPNotImplemented()) |
2901 | |
2902 | - def update(self, req, id): |
2903 | + def update(self, req, id, body): |
2904 | """This is really create or update.""" |
2905 | self._check_admin(req.environ['nova.context']) |
2906 | - env = self._deserialize(req.body, req.get_content_type()) |
2907 | - description = env['account'].get('description') |
2908 | - manager = env['account'].get('manager') |
2909 | + description = body['account'].get('description') |
2910 | + manager = body['account'].get('manager') |
2911 | try: |
2912 | account = self.manager.get_project(id) |
2913 | self.manager.modify_project(id, manager, description) |
2914 | except exception.NotFound: |
2915 | account = self.manager.create_project(id, manager, description) |
2916 | return dict(account=_translate_keys(account)) |
2917 | + |
2918 | + |
2919 | +def create_resource(): |
2920 | + metadata = { |
2921 | + "attributes": { |
2922 | + "account": ["id", "name", "description", "manager"], |
2923 | + }, |
2924 | + } |
2925 | + |
2926 | + body_serializers = { |
2927 | + 'application/xml': wsgi.XMLDictSerializer(metadata=metadata), |
2928 | + } |
2929 | + serializer = wsgi.ResponseSerializer(body_serializers) |
2930 | + return wsgi.Resource(Controller(), serializer=serializer) |
2931 | |
2932 | === modified file 'nova/api/openstack/auth.py' |
2933 | --- nova/api/openstack/auth.py 2011-05-17 19:12:48 +0000 |
2934 | +++ nova/api/openstack/auth.py 2011-07-14 20:24:25 +0000 |
2935 | @@ -13,9 +13,8 @@ |
2936 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
2937 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
2938 | # License for the specific language governing permissions and limitations |
2939 | -# under the License.import datetime |
2940 | +# under the License. |
2941 | |
2942 | -import datetime |
2943 | import hashlib |
2944 | import time |
2945 | |
2946 | @@ -50,19 +49,22 @@ |
2947 | if not self.has_authentication(req): |
2948 | return self.authenticate(req) |
2949 | user = self.get_user_by_authentication(req) |
2950 | - accounts = self.auth.get_projects(user=user) |
2951 | if not user: |
2952 | token = req.headers["X-Auth-Token"] |
2953 | msg = _("%(user)s could not be found with token '%(token)s'") |
2954 | LOG.warn(msg % locals()) |
2955 | return faults.Fault(webob.exc.HTTPUnauthorized()) |
2956 | |
2957 | - if accounts: |
2958 | - #we are punting on this til auth is settled, |
2959 | - #and possibly til api v1.1 (mdragon) |
2960 | - account = accounts[0] |
2961 | - else: |
2962 | - return faults.Fault(webob.exc.HTTPUnauthorized()) |
2963 | + try: |
2964 | + account = req.headers["X-Auth-Project-Id"] |
2965 | + except KeyError: |
2966 | + # FIXME(usrleon): It needed only for compatibility |
2967 | + # while osapi clients don't use this header |
2968 | + accounts = self.auth.get_projects(user=user) |
2969 | + if accounts: |
2970 | + account = accounts[0] |
2971 | + else: |
2972 | + return faults.Fault(webob.exc.HTTPUnauthorized()) |
2973 | |
2974 | if not self.auth.is_admin(user) and \ |
2975 | not self.auth.is_project_member(user, account): |
2976 | @@ -127,7 +129,7 @@ |
2977 | except exception.NotFound: |
2978 | return None |
2979 | if token: |
2980 | - delta = datetime.datetime.utcnow() - token['created_at'] |
2981 | + delta = utils.utcnow() - token['created_at'] |
2982 | if delta.days >= 2: |
2983 | self.db.auth_token_destroy(ctxt, token['token_hash']) |
2984 | else: |
2985 | |
2986 | === modified file 'nova/api/openstack/backup_schedules.py' |
2987 | --- nova/api/openstack/backup_schedules.py 2011-03-30 17:05:06 +0000 |
2988 | +++ nova/api/openstack/backup_schedules.py 2011-07-14 20:24:25 +0000 |
2989 | @@ -19,9 +19,8 @@ |
2990 | |
2991 | from webob import exc |
2992 | |
2993 | -from nova.api.openstack import common |
2994 | from nova.api.openstack import faults |
2995 | -import nova.image.service |
2996 | +from nova.api.openstack import wsgi |
2997 | |
2998 | |
2999 | def _translate_keys(inst): |
3000 | @@ -29,30 +28,41 @@ |
3001 | return dict(backupSchedule=inst) |
3002 | |
3003 | |
3004 | -class Controller(common.OpenstackController): |
3005 | +class Controller(object): |
3006 | """ The backup schedule API controller for the Openstack API """ |
3007 | |
3008 | - _serialization_metadata = { |
3009 | - 'application/xml': { |
3010 | - 'attributes': { |
3011 | - 'backupSchedule': []}}} |
3012 | - |
3013 | def __init__(self): |
3014 | pass |
3015 | |
3016 | - def index(self, req, server_id): |
3017 | + def index(self, req, server_id, **kwargs): |
3018 | """ Returns the list of backup schedules for a given instance """ |
3019 | return faults.Fault(exc.HTTPNotImplemented()) |
3020 | |
3021 | - def show(self, req, server_id, id): |
3022 | + def show(self, req, server_id, id, **kwargs): |
3023 | """ Returns a single backup schedule for a given instance """ |
3024 | return faults.Fault(exc.HTTPNotImplemented()) |
3025 | |
3026 | - def create(self, req, server_id): |
3027 | + def create(self, req, server_id, **kwargs): |
3028 | """ No actual update method required, since the existing API allows |
3029 | both create and update through a POST """ |
3030 | return faults.Fault(exc.HTTPNotImplemented()) |
3031 | |
3032 | - def delete(self, req, server_id, id): |
3033 | + def delete(self, req, server_id, id, **kwargs): |
3034 | """ Deletes an existing backup schedule """ |
3035 | return faults.Fault(exc.HTTPNotImplemented()) |
3036 | + |
3037 | + |
3038 | +def create_resource(): |
3039 | + metadata = { |
3040 | + 'attributes': { |
3041 | + 'backupSchedule': [], |
3042 | + }, |
3043 | + } |
3044 | + |
3045 | + body_serializers = { |
3046 | + 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10, |
3047 | + metadata=metadata), |
3048 | + } |
3049 | + |
3050 | + serializer = wsgi.ResponseSerializer(body_serializers) |
3051 | + return wsgi.Resource(Controller(), serializer=serializer) |
3052 | |
3053 | === modified file 'nova/api/openstack/common.py' |
3054 | --- nova/api/openstack/common.py 2011-05-02 20:14:41 +0000 |
3055 | +++ nova/api/openstack/common.py 2011-07-14 20:24:25 +0000 |
3056 | @@ -23,12 +23,9 @@ |
3057 | from nova import exception |
3058 | from nova import flags |
3059 | from nova import log as logging |
3060 | -from nova import wsgi |
3061 | |
3062 | |
3063 | LOG = logging.getLogger('nova.api.openstack.common') |
3064 | - |
3065 | - |
3066 | FLAGS = flags.FLAGS |
3067 | |
3068 | |
3069 | @@ -36,6 +33,34 @@ |
3070 | XML_NS_V11 = 'http://docs.openstack.org/compute/api/v1.1' |
3071 | |
3072 | |
3073 | +def get_pagination_params(request): |
3074 | + """Return marker, limit tuple from request. |
3075 | + |
3076 | + :param request: `wsgi.Request` possibly containing 'marker' and 'limit' |
3077 | + GET variables. 'marker' is the id of the last element |
3078 | + the client has seen, and 'limit' is the maximum number |
3079 | + of items to return. If 'limit' is not specified, 0, or |
3080 | + > max_limit, we default to max_limit. Negative values |
3081 | + for either marker or limit will cause |
3082 | + exc.HTTPBadRequest() exceptions to be raised. |
3083 | + |
3084 | + """ |
3085 | + params = {} |
3086 | + for param in ['marker', 'limit']: |
3087 | + if not param in request.GET: |
3088 | + continue |
3089 | + try: |
3090 | + params[param] = int(request.GET[param]) |
3091 | + except ValueError: |
3092 | + msg = _('%s param must be an integer') % param |
3093 | + raise webob.exc.HTTPBadRequest(msg) |
3094 | + if params[param] < 0: |
3095 | + msg = _('%s param must be positive') % param |
3096 | + raise webob.exc.HTTPBadRequest(msg) |
3097 | + |
3098 | + return params |
3099 | + |
3100 | + |
3101 | def limited(items, request, max_limit=FLAGS.osapi_max_limit): |
3102 | """ |
3103 | Return a slice of items according to requested offset and limit. |
3104 | @@ -72,19 +97,10 @@ |
3105 | |
3106 | def limited_by_marker(items, request, max_limit=FLAGS.osapi_max_limit): |
3107 | """Return a slice of items according to the requested marker and limit.""" |
3108 | - |
3109 | - try: |
3110 | - marker = int(request.GET.get('marker', 0)) |
3111 | - except ValueError: |
3112 | - raise webob.exc.HTTPBadRequest(_('marker param must be an integer')) |
3113 | - |
3114 | - try: |
3115 | - limit = int(request.GET.get('limit', max_limit)) |
3116 | - except ValueError: |
3117 | - raise webob.exc.HTTPBadRequest(_('limit param must be an integer')) |
3118 | - |
3119 | - if limit < 0: |
3120 | - raise webob.exc.HTTPBadRequest(_('limit param must be positive')) |
3121 | + params = get_pagination_params(request) |
3122 | + |
3123 | + limit = params.get('limit', max_limit) |
3124 | + marker = params.get('marker') |
3125 | |
3126 | limit = min(max_limit, limit) |
3127 | start_index = 0 |
3128 | @@ -100,34 +116,6 @@ |
3129 | return items[start_index:range_end] |
3130 | |
3131 | |
3132 | -def get_image_id_from_image_hash(image_service, context, image_hash): |
3133 | - """Given an Image ID Hash, return an objectstore Image ID. |
3134 | - |
3135 | - image_service - reference to objectstore compatible image service. |
3136 | - context - security context for image service requests. |
3137 | - image_hash - hash of the image ID. |
3138 | - """ |
3139 | - |
3140 | - # FIX(sandy): This is terribly inefficient. It pulls all images |
3141 | - # from objectstore in order to find the match. ObjectStore |
3142 | - # should have a numeric counterpart to the string ID. |
3143 | - try: |
3144 | - items = image_service.detail(context) |
3145 | - except NotImplementedError: |
3146 | - items = image_service.index(context) |
3147 | - for image in items: |
3148 | - image_id = image['id'] |
3149 | - try: |
3150 | - if abs(hash(image_id)) == int(image_hash): |
3151 | - return image_id |
3152 | - except ValueError: |
3153 | - msg = _("Requested image_id has wrong format: %s," |
3154 | - "should have numerical format") % image_id |
3155 | - LOG.error(msg) |
3156 | - raise Exception(msg) |
3157 | - raise exception.ImageNotFound(image_id=image_hash) |
3158 | - |
3159 | - |
3160 | def get_id_from_href(href): |
3161 | """Return the id portion of a url as an int. |
3162 | |
3163 | @@ -145,10 +133,25 @@ |
3164 | return int(urlparse(href).path.split('/')[-1]) |
3165 | except: |
3166 | LOG.debug(_("Error extracting id from href: %s") % href) |
3167 | - raise webob.exc.HTTPBadRequest(_('could not parse id from href')) |
3168 | - |
3169 | - |
3170 | -class OpenstackController(wsgi.Controller): |
3171 | - def get_default_xmlns(self, req): |
3172 | - # Use V10 by default |
3173 | - return XML_NS_V10 |
3174 | + raise ValueError(_('could not parse id from href')) |
3175 | + |
3176 | + |
3177 | +def remove_version_from_href(href): |
3178 | + """Removes the api version from the href. |
3179 | + |
3180 | + Given: 'http://www.nova.com/v1.1/123' |
3181 | + Returns: 'http://www.nova.com/123' |
3182 | + |
3183 | + """ |
3184 | + try: |
3185 | + #matches /v#.# |
3186 | + new_href = re.sub(r'[/][v][0-9]*.[0-9]*', '', href) |
3187 | + except: |
3188 | + LOG.debug(_("Error removing version from href: %s") % href) |
3189 | + msg = _('could not parse version from href') |
3190 | + raise ValueError(msg) |
3191 | + |
3192 | + if new_href == href: |
3193 | + msg = _('href does not contain version') |
3194 | + raise ValueError(msg) |
3195 | + return new_href |
3196 | |
3197 | === modified file 'nova/api/openstack/consoles.py' |
3198 | --- nova/api/openstack/consoles.py 2011-03-30 17:05:06 +0000 |
3199 | +++ nova/api/openstack/consoles.py 2011-07-14 20:24:25 +0000 |
3200 | @@ -19,8 +19,8 @@ |
3201 | |
3202 | from nova import console |
3203 | from nova import exception |
3204 | -from nova.api.openstack import common |
3205 | from nova.api.openstack import faults |
3206 | +from nova.api.openstack import wsgi |
3207 | |
3208 | |
3209 | def _translate_keys(cons): |
3210 | @@ -43,17 +43,11 @@ |
3211 | return dict(console=info) |
3212 | |
3213 | |
3214 | -class Controller(common.OpenstackController): |
3215 | - """The Consoles Controller for the Openstack API""" |
3216 | - |
3217 | - _serialization_metadata = { |
3218 | - 'application/xml': { |
3219 | - 'attributes': { |
3220 | - 'console': []}}} |
3221 | +class Controller(object): |
3222 | + """The Consoles controller for the Openstack API""" |
3223 | |
3224 | def __init__(self): |
3225 | self.console_api = console.API() |
3226 | - super(Controller, self).__init__() |
3227 | |
3228 | def index(self, req, server_id): |
3229 | """Returns a list of consoles for this instance""" |
3230 | @@ -63,9 +57,8 @@ |
3231 | return dict(consoles=[_translate_keys(console) |
3232 | for console in consoles]) |
3233 | |
3234 | - def create(self, req, server_id): |
3235 | + def create(self, req, server_id, body): |
3236 | """Creates a new console""" |
3237 | - #info = self._deserialize(req.body, req.get_content_type()) |
3238 | self.console_api.create_console( |
3239 | req.environ['nova.context'], |
3240 | int(server_id)) |
3241 | @@ -94,3 +87,7 @@ |
3242 | except exception.NotFound: |
3243 | return faults.Fault(exc.HTTPNotFound()) |
3244 | return exc.HTTPAccepted() |
3245 | + |
3246 | + |
3247 | +def create_resource(): |
3248 | + return wsgi.Resource(Controller()) |
3249 | |
3250 | === modified file 'nova/api/openstack/contrib/__init__.py' |
3251 | --- nova/api/openstack/contrib/__init__.py 2011-03-30 01:16:09 +0000 |
3252 | +++ nova/api/openstack/contrib/__init__.py 2011-07-14 20:24:25 +0000 |
3253 | @@ -13,7 +13,7 @@ |
3254 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
3255 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
3256 | # License for the specific language governing permissions and limitations |
3257 | -# under the License.import datetime |
3258 | +# under the License. |
3259 | |
3260 | """Contrib contains extensions that are shipped with nova. |
3261 | |
3262 | |
3263 | === added file 'nova/api/openstack/contrib/flavorextraspecs.py' |
3264 | --- nova/api/openstack/contrib/flavorextraspecs.py 1970-01-01 00:00:00 +0000 |
3265 | +++ nova/api/openstack/contrib/flavorextraspecs.py 2011-07-14 20:24:25 +0000 |
3266 | @@ -0,0 +1,126 @@ |
3267 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
3268 | + |
3269 | +# Copyright 2011 University of Southern California |
3270 | +# All Rights Reserved. |
3271 | +# |
3272 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
3273 | +# not use this file except in compliance with the License. You may obtain |
3274 | +# a copy of the License at |
3275 | +# |
3276 | +# http://www.apache.org/licenses/LICENSE-2.0 |
3277 | +# |
3278 | +# Unless required by applicable law or agreed to in writing, software |
3279 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
3280 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
3281 | +# License for the specific language governing permissions and limitations |
3282 | +# under the License. |
3283 | + |
3284 | +""" The instance type extra specs extension""" |
3285 | + |
3286 | +from webob import exc |
3287 | + |
3288 | +from nova import db |
3289 | +from nova import quota |
3290 | +from nova.api.openstack import extensions |
3291 | +from nova.api.openstack import faults |
3292 | +from nova.api.openstack import wsgi |
3293 | + |
3294 | + |
3295 | +class FlavorExtraSpecsController(object): |
3296 | + """ The flavor extra specs API controller for the Openstack API """ |
3297 | + |
3298 | + def _get_extra_specs(self, context, flavor_id): |
3299 | + extra_specs = db.api.instance_type_extra_specs_get(context, flavor_id) |
3300 | + specs_dict = {} |
3301 | + for key, value in extra_specs.iteritems(): |
3302 | + specs_dict[key] = value |
3303 | + return dict(extra_specs=specs_dict) |
3304 | + |
3305 | + def _check_body(self, body): |
3306 | + if body == None or body == "": |
3307 | + expl = _('No Request Body') |
3308 | + raise exc.HTTPBadRequest(explanation=expl) |
3309 | + |
3310 | + def index(self, req, flavor_id): |
3311 | + """ Returns the list of extra specs for a givenflavor """ |
3312 | + context = req.environ['nova.context'] |
3313 | + return self._get_extra_specs(context, flavor_id) |
3314 | + |
3315 | + def create(self, req, flavor_id, body): |
3316 | + self._check_body(body) |
3317 | + context = req.environ['nova.context'] |
3318 | + specs = body.get('extra_specs') |
3319 | + try: |
3320 | + db.api.instance_type_extra_specs_update_or_create(context, |
3321 | + flavor_id, |
3322 | + specs) |
3323 | + except quota.QuotaError as error: |
3324 | + self._handle_quota_error(error) |
3325 | + return body |
3326 | + |
3327 | + def update(self, req, flavor_id, id, body): |
3328 | + self._check_body(body) |
3329 | + context = req.environ['nova.context'] |
3330 | + if not id in body: |
3331 | + expl = _('Request body and URI mismatch') |
3332 | + raise exc.HTTPBadRequest(explanation=expl) |
3333 | + if len(body) > 1: |
3334 | + expl = _('Request body contains too many items') |
3335 | + raise exc.HTTPBadRequest(explanation=expl) |
3336 | + try: |
3337 | + db.api.instance_type_extra_specs_update_or_create(context, |
3338 | + flavor_id, |
3339 | + body) |
3340 | + except quota.QuotaError as error: |
3341 | + self._handle_quota_error(error) |
3342 | + |
3343 | + return body |
3344 | + |
3345 | + def show(self, req, flavor_id, id): |
3346 | + """ Return a single extra spec item """ |
3347 | + context = req.environ['nova.context'] |
3348 | + specs = self._get_extra_specs(context, flavor_id) |
3349 | + if id in specs['extra_specs']: |
3350 | + return {id: specs['extra_specs'][id]} |
3351 | + else: |
3352 | + return faults.Fault(exc.HTTPNotFound()) |
3353 | + |
3354 | + def delete(self, req, flavor_id, id): |
3355 | + """ Deletes an existing extra spec """ |
3356 | + context = req.environ['nova.context'] |
3357 | + db.api.instance_type_extra_specs_delete(context, flavor_id, id) |
3358 | + |
3359 | + def _handle_quota_error(self, error): |
3360 | + """Reraise quota errors as api-specific http exceptions.""" |
3361 | + if error.code == "MetadataLimitExceeded": |
3362 | + raise exc.HTTPBadRequest(explanation=error.message) |
3363 | + raise error |
3364 | + |
3365 | + |
3366 | +class Flavorextraspecs(extensions.ExtensionDescriptor): |
3367 | + |
3368 | + def get_name(self): |
3369 | + return "FlavorExtraSpecs" |
3370 | + |
3371 | + def get_alias(self): |
3372 | + return "os-flavor-extra-specs" |
3373 | + |
3374 | + def get_description(self): |
3375 | + return "Instance type (flavor) extra specs" |
3376 | + |
3377 | + def get_namespace(self): |
3378 | + return \ |
3379 | + "http://docs.openstack.org/ext/flavor_extra_specs/api/v1.1" |
3380 | + |
3381 | + def get_updated(self): |
3382 | + return "2011-06-23T00:00:00+00:00" |
3383 | + |
3384 | + def get_resources(self): |
3385 | + resources = [] |
3386 | + res = extensions.ResourceExtension( |
3387 | + 'os-extra_specs', |
3388 | + FlavorExtraSpecsController(), |
3389 | + parent=dict(member_name='flavor', collection_name='flavors')) |
3390 | + |
3391 | + resources.append(res) |
3392 | + return resources |
3393 | |
3394 | === added file 'nova/api/openstack/contrib/floating_ips.py' |
3395 | --- nova/api/openstack/contrib/floating_ips.py 1970-01-01 00:00:00 +0000 |
3396 | +++ nova/api/openstack/contrib/floating_ips.py 2011-07-14 20:24:25 +0000 |
3397 | @@ -0,0 +1,173 @@ |
3398 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
3399 | + |
3400 | +# Copyright 2011 OpenStack LLC. |
3401 | +# Copyright 2011 Grid Dynamics |
3402 | +# Copyright 2011 Eldar Nugaev, Kirill Shileev, Ilya Alekseyev |
3403 | +# |
3404 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
3405 | +# not use this file except in compliance with the License. You may obtain |
3406 | +# a copy of the License at |
3407 | +# |
3408 | +# http://www.apache.org/licenses/LICENSE-2.0 |
3409 | +# |
3410 | +# Unless required by applicable law or agreed to in writing, software |
3411 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
3412 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
3413 | +# License for the specific language governing permissions and limitations |
3414 | +# under the License |
3415 | +from webob import exc |
3416 | + |
3417 | +from nova import exception |
3418 | +from nova import network |
3419 | +from nova import rpc |
3420 | +from nova.api.openstack import faults |
3421 | +from nova.api.openstack import extensions |
3422 | + |
3423 | + |
3424 | +def _translate_floating_ip_view(floating_ip): |
3425 | + result = {'id': floating_ip['id'], |
3426 | + 'ip': floating_ip['address']} |
3427 | + if 'fixed_ip' in floating_ip: |
3428 | + result['fixed_ip'] = floating_ip['fixed_ip']['address'] |
3429 | + else: |
3430 | + result['fixed_ip'] = None |
3431 | + if 'instance' in floating_ip: |
3432 | + result['instance_id'] = floating_ip['instance']['id'] |
3433 | + else: |
3434 | + result['instance_id'] = None |
3435 | + return {'floating_ip': result} |
3436 | + |
3437 | + |
3438 | +def _translate_floating_ips_view(floating_ips): |
3439 | + return {'floating_ips': [_translate_floating_ip_view(floating_ip) |
3440 | + for floating_ip in floating_ips]} |
3441 | + |
3442 | + |
3443 | +class FloatingIPController(object): |
3444 | + """The Floating IPs API controller for the OpenStack API.""" |
3445 | + |
3446 | + _serialization_metadata = { |
3447 | + 'application/xml': { |
3448 | + "attributes": { |
3449 | + "floating_ip": [ |
3450 | + "id", |
3451 | + "ip", |
3452 | + "instance_id", |
3453 | + "fixed_ip", |
3454 | + ]}}} |
3455 | + |
3456 | + def __init__(self): |
3457 | + self.network_api = network.API() |
3458 | + super(FloatingIPController, self).__init__() |
3459 | + |
3460 | + def show(self, req, id): |
3461 | + """Return data about the given floating ip.""" |
3462 | + context = req.environ['nova.context'] |
3463 | + |
3464 | + try: |
3465 | + floating_ip = self.network_api.get_floating_ip(context, id) |
3466 | + except exception.NotFound: |
3467 | + return faults.Fault(exc.HTTPNotFound()) |
3468 | + |
3469 | + return _translate_floating_ip_view(floating_ip) |
3470 | + |
3471 | + def index(self, req): |
3472 | + context = req.environ['nova.context'] |
3473 | + |
3474 | + floating_ips = self.network_api.list_floating_ips(context) |
3475 | + |
3476 | + return _translate_floating_ips_view(floating_ips) |
3477 | + |
3478 | + def create(self, req): |
3479 | + context = req.environ['nova.context'] |
3480 | + |
3481 | + try: |
3482 | + address = self.network_api.allocate_floating_ip(context) |
3483 | + ip = self.network_api.get_floating_ip_by_ip(context, address) |
3484 | + except rpc.RemoteError as ex: |
3485 | + # NOTE(tr3buchet) - why does this block exist? |
3486 | + if ex.exc_type == 'NoMoreFloatingIps': |
3487 | + raise exception.NoMoreFloatingIps() |
3488 | + else: |
3489 | + raise |
3490 | + |
3491 | + return {'allocated': { |
3492 | + "id": ip['id'], |
3493 | + "floating_ip": ip['address']}} |
3494 | + |
3495 | + def delete(self, req, id): |
3496 | + context = req.environ['nova.context'] |
3497 | + |
3498 | + ip = self.network_api.get_floating_ip(context, id) |
3499 | + self.network_api.release_floating_ip(context, address=ip) |
3500 | + |
3501 | + return {'released': { |
3502 | + "id": ip['id'], |
3503 | + "floating_ip": ip['address']}} |
3504 | + |
3505 | + def associate(self, req, id, body): |
3506 | + """ /floating_ips/{id}/associate fixed ip in body """ |
3507 | + context = req.environ['nova.context'] |
3508 | + floating_ip = self._get_ip_by_id(context, id) |
3509 | + |
3510 | + fixed_ip = body['associate_address']['fixed_ip'] |
3511 | + |
3512 | + try: |
3513 | + self.network_api.associate_floating_ip(context, |
3514 | + floating_ip, fixed_ip) |
3515 | + except rpc.RemoteError: |
3516 | + raise |
3517 | + |
3518 | + return {'associated': |
3519 | + { |
3520 | + "floating_ip_id": id, |
3521 | + "floating_ip": floating_ip, |
3522 | + "fixed_ip": fixed_ip}} |
3523 | + |
3524 | + def disassociate(self, req, id): |
3525 | + """ POST /floating_ips/{id}/disassociate """ |
3526 | + context = req.environ['nova.context'] |
3527 | + floating_ip = self.network_api.get_floating_ip(context, id) |
3528 | + address = floating_ip['address'] |
3529 | + fixed_ip = floating_ip['fixed_ip']['address'] |
3530 | + |
3531 | + try: |
3532 | + self.network_api.disassociate_floating_ip(context, address) |
3533 | + except rpc.RemoteError: |
3534 | + raise |
3535 | + |
3536 | + return {'disassociated': {'floating_ip': address, |
3537 | + 'fixed_ip': fixed_ip}} |
3538 | + |
3539 | + def _get_ip_by_id(self, context, value): |
3540 | + """Checks that value is id and then returns its address.""" |
3541 | + return self.network_api.get_floating_ip(context, value)['address'] |
3542 | + |
3543 | + |
3544 | +class Floating_ips(extensions.ExtensionDescriptor): |
3545 | + def get_name(self): |
3546 | + return "Floating_ips" |
3547 | + |
3548 | + def get_alias(self): |
3549 | + return "os-floating-ips" |
3550 | + |
3551 | + def get_description(self): |
3552 | + return "Floating IPs support" |
3553 | + |
3554 | + def get_namespace(self): |
3555 | + return "http://docs.openstack.org/ext/floating_ips/api/v1.1" |
3556 | + |
3557 | + def get_updated(self): |
3558 | + return "2011-06-16T00:00:00+00:00" |
3559 | + |
3560 | + def get_resources(self): |
3561 | + resources = [] |
3562 | + |
3563 | + res = extensions.ResourceExtension('os-floating-ips', |
3564 | + FloatingIPController(), |
3565 | + member_actions={ |
3566 | + 'associate': 'POST', |
3567 | + 'disassociate': 'POST'}) |
3568 | + resources.append(res) |
3569 | + |
3570 | + return resources |
3571 | |
3572 | === added file 'nova/api/openstack/contrib/hosts.py' |
3573 | --- nova/api/openstack/contrib/hosts.py 1970-01-01 00:00:00 +0000 |
3574 | +++ nova/api/openstack/contrib/hosts.py 2011-07-14 20:24:25 +0000 |
3575 | @@ -0,0 +1,114 @@ |
3576 | +# Copyright (c) 2011 Openstack, LLC. |
3577 | +# All Rights Reserved. |
3578 | +# |
3579 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
3580 | +# not use this file except in compliance with the License. You may obtain |
3581 | +# a copy of the License at |
3582 | +# |
3583 | +# http://www.apache.org/licenses/LICENSE-2.0 |
3584 | +# |
3585 | +# Unless required by applicable law or agreed to in writing, software |
3586 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
3587 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
3588 | +# License for the specific language governing permissions and limitations |
3589 | +# under the License. |
3590 | + |
3591 | +"""The hosts admin extension.""" |
3592 | + |
3593 | +import webob.exc |
3594 | + |
3595 | +from nova import compute |
3596 | +from nova import exception |
3597 | +from nova import flags |
3598 | +from nova import log as logging |
3599 | +from nova.api.openstack import common |
3600 | +from nova.api.openstack import extensions |
3601 | +from nova.api.openstack import faults |
3602 | +from nova.scheduler import api as scheduler_api |
3603 | + |
3604 | + |
3605 | +LOG = logging.getLogger("nova.api.hosts") |
3606 | +FLAGS = flags.FLAGS |
3607 | + |
3608 | + |
3609 | +def _list_hosts(req, service=None): |
3610 | + """Returns a summary list of hosts, optionally filtering |
3611 | + by service type. |
3612 | + """ |
3613 | + context = req.environ['nova.context'] |
3614 | + hosts = scheduler_api.get_host_list(context) |
3615 | + if service: |
3616 | + hosts = [host for host in hosts |
3617 | + if host["service"] == service] |
3618 | + return hosts |
3619 | + |
3620 | + |
3621 | +def check_host(fn): |
3622 | + """Makes sure that the host exists.""" |
3623 | + def wrapped(self, req, id, service=None, *args, **kwargs): |
3624 | + listed_hosts = _list_hosts(req, service) |
3625 | + hosts = [h["host_name"] for h in listed_hosts] |
3626 | + if id in hosts: |
3627 | + return fn(self, req, id, *args, **kwargs) |
3628 | + else: |
3629 | + raise exception.HostNotFound(host=id) |
3630 | + return wrapped |
3631 | + |
3632 | + |
3633 | +class HostController(object): |
3634 | + """The Hosts API controller for the OpenStack API.""" |
3635 | + def __init__(self): |
3636 | + self.compute_api = compute.API() |
3637 | + super(HostController, self).__init__() |
3638 | + |
3639 | + def index(self, req): |
3640 | + return {'hosts': _list_hosts(req)} |
3641 | + |
3642 | + @check_host |
3643 | + def update(self, req, id, body): |
3644 | + for raw_key, raw_val in body.iteritems(): |
3645 | + key = raw_key.lower().strip() |
3646 | + val = raw_val.lower().strip() |
3647 | + # NOTE: (dabo) Right now only 'status' can be set, but other |
3648 | + # actions may follow. |
3649 | + if key == "status": |
3650 | + if val[:6] in ("enable", "disabl"): |
3651 | + return self._set_enabled_status(req, id, |
3652 | + enabled=(val.startswith("enable"))) |
3653 | + else: |
3654 | + explanation = _("Invalid status: '%s'") % raw_val |
3655 | + raise webob.exc.HTTPBadRequest(explanation=explanation) |
3656 | + else: |
3657 | + explanation = _("Invalid update setting: '%s'") % raw_key |
3658 | + raise webob.exc.HTTPBadRequest(explanation=explanation) |
3659 | + |
3660 | + def _set_enabled_status(self, req, host, enabled): |
3661 | + """Sets the specified host's ability to accept new instances.""" |
3662 | + context = req.environ['nova.context'] |
3663 | + state = "enabled" if enabled else "disabled" |
3664 | + LOG.audit(_("Setting host %(host)s to %(state)s.") % locals()) |
3665 | + result = self.compute_api.set_host_enabled(context, host=host, |
3666 | + enabled=enabled) |
3667 | + return {"host": host, "status": result} |
3668 | + |
3669 | + |
3670 | +class Hosts(extensions.ExtensionDescriptor): |
3671 | + def get_name(self): |
3672 | + return "Hosts" |
3673 | + |
3674 | + def get_alias(self): |
3675 | + return "os-hosts" |
3676 | + |
3677 | + def get_description(self): |
3678 | + return "Host administration" |
3679 | + |
3680 | + def get_namespace(self): |
3681 | + return "http://docs.openstack.org/ext/hosts/api/v1.1" |
3682 | + |
3683 | + def get_updated(self): |
3684 | + return "2011-06-29T00:00:00+00:00" |
3685 | + |
3686 | + def get_resources(self): |
3687 | + resources = [extensions.ResourceExtension('os-hosts', HostController(), |
3688 | + collection_actions={'update': 'PUT'}, member_actions={})] |
3689 | + return resources |
3690 | |
3691 | === added file 'nova/api/openstack/contrib/multinic.py' |
3692 | --- nova/api/openstack/contrib/multinic.py 1970-01-01 00:00:00 +0000 |
3693 | +++ nova/api/openstack/contrib/multinic.py 2011-07-14 20:24:25 +0000 |
3694 | @@ -0,0 +1,125 @@ |
3695 | +# Copyright 2011 OpenStack LLC. |
3696 | +# All Rights Reserved. |
3697 | +# |
3698 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
3699 | +# not use this file except in compliance with the License. You may obtain |
3700 | +# a copy of the License at |
3701 | +# |
3702 | +# http://www.apache.org/licenses/LICENSE-2.0 |
3703 | +# |
3704 | +# Unless required by applicable law or agreed to in writing, software |
3705 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
3706 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
3707 | +# License for the specific language governing permissions and limitations |
3708 | +# under the License. |
3709 | + |
3710 | +"""The multinic extension.""" |
3711 | + |
3712 | +from webob import exc |
3713 | + |
3714 | +from nova import compute |
3715 | +from nova import log as logging |
3716 | +from nova.api.openstack import extensions |
3717 | +from nova.api.openstack import faults |
3718 | + |
3719 | + |
3720 | +LOG = logging.getLogger("nova.api.multinic") |
3721 | + |
3722 | + |
3723 | +# Note: The class name is as it has to be for this to be loaded as an |
3724 | +# extension--only first character capitalized. |
3725 | +class Multinic(extensions.ExtensionDescriptor): |
3726 | + """The multinic extension. |
3727 | + |
3728 | + Exposes addFixedIp and removeFixedIp actions on servers. |
3729 | + |
3730 | + """ |
3731 | + |
3732 | + def __init__(self, *args, **kwargs): |
3733 | + """Initialize the extension. |
3734 | + |
3735 | + Gets a compute.API object so we can call the back-end |
3736 | + add_fixed_ip() and remove_fixed_ip() methods. |
3737 | + """ |
3738 | + |
3739 | + super(Multinic, self).__init__(*args, **kwargs) |
3740 | + self.compute_api = compute.API() |
3741 | + |
3742 | + def get_name(self): |
3743 | + """Return the extension name, as required by contract.""" |
3744 | + |
3745 | + return "Multinic" |
3746 | + |
3747 | + def get_alias(self): |
3748 | + """Return the extension alias, as required by contract.""" |
3749 | + |
3750 | + return "NMN" |
3751 | + |
3752 | + def get_description(self): |
3753 | + """Return the extension description, as required by contract.""" |
3754 | + |
3755 | + return "Multiple network support" |
3756 | + |
3757 | + def get_namespace(self): |
3758 | + """Return the namespace, as required by contract.""" |
3759 | + |
3760 | + return "http://docs.openstack.org/ext/multinic/api/v1.1" |
3761 | + |
3762 | + def get_updated(self): |
3763 | + """Return the last updated timestamp, as required by contract.""" |
3764 | + |
3765 | + return "2011-06-09T00:00:00+00:00" |
3766 | + |
3767 | + def get_actions(self): |
3768 | + """Return the actions the extension adds, as required by contract.""" |
3769 | + |
3770 | + actions = [] |
3771 | + |
3772 | + # Add the add_fixed_ip action |
3773 | + act = extensions.ActionExtension("servers", "addFixedIp", |
3774 | + self._add_fixed_ip) |
3775 | + actions.append(act) |
3776 | + |
3777 | + # Add the remove_fixed_ip action |
3778 | + act = extensions.ActionExtension("servers", "removeFixedIp", |
3779 | + self._remove_fixed_ip) |
3780 | + actions.append(act) |
3781 | + |
3782 | + return actions |
3783 | + |
3784 | + def _add_fixed_ip(self, input_dict, req, id): |
3785 | + """Adds an IP on a given network to an instance.""" |
3786 | + |
3787 | + try: |
3788 | + # Validate the input entity |
3789 | + if 'networkId' not in input_dict['addFixedIp']: |
3790 | + LOG.exception(_("Missing 'networkId' argument for addFixedIp")) |
3791 | + return faults.Fault(exc.HTTPUnprocessableEntity()) |
3792 | + |
3793 | + # Add the fixed IP |
3794 | + network_id = input_dict['addFixedIp']['networkId'] |
3795 | + self.compute_api.add_fixed_ip(req.environ['nova.context'], id, |
3796 | + network_id) |
3797 | + except Exception, e: |
3798 | + LOG.exception(_("Error in addFixedIp %s"), e) |
3799 | + return faults.Fault(exc.HTTPBadRequest()) |
3800 | + return exc.HTTPAccepted() |
3801 | + |
3802 | + def _remove_fixed_ip(self, input_dict, req, id): |
3803 | + """Removes an IP from an instance.""" |
3804 | + |
3805 | + try: |
3806 | + # Validate the input entity |
3807 | + if 'address' not in input_dict['removeFixedIp']: |
3808 | + LOG.exception(_("Missing 'address' argument for " |
3809 | + "removeFixedIp")) |
3810 | + return faults.Fault(exc.HTTPUnprocessableEntity()) |
3811 | + |
3812 | + # Remove the fixed IP |
3813 | + address = input_dict['removeFixedIp']['address'] |
3814 | + self.compute_api.remove_fixed_ip(req.environ['nova.context'], id, |
3815 | + address) |
3816 | + except Exception, e: |
3817 | + LOG.exception(_("Error in removeFixedIp %s"), e) |
3818 | + return faults.Fault(exc.HTTPBadRequest()) |
3819 | + return exc.HTTPAccepted() |
3820 | |
3821 | === modified file 'nova/api/openstack/contrib/volumes.py' |
3822 | --- nova/api/openstack/contrib/volumes.py 2011-04-19 09:54:47 +0000 |
3823 | +++ nova/api/openstack/contrib/volumes.py 2011-07-14 20:24:25 +0000 |
3824 | @@ -22,7 +22,6 @@ |
3825 | from nova import flags |
3826 | from nova import log as logging |
3827 | from nova import volume |
3828 | -from nova import wsgi |
3829 | from nova.api.openstack import common |
3830 | from nova.api.openstack import extensions |
3831 | from nova.api.openstack import faults |
3832 | @@ -64,7 +63,7 @@ |
3833 | return d |
3834 | |
3835 | |
3836 | -class VolumeController(wsgi.Controller): |
3837 | +class VolumeController(object): |
3838 | """The Volumes API controller for the OpenStack API.""" |
3839 | |
3840 | _serialization_metadata = { |
3841 | @@ -124,18 +123,17 @@ |
3842 | res = [entity_maker(context, vol) for vol in limited_list] |
3843 | return {'volumes': res} |
3844 | |
3845 | - def create(self, req): |
3846 | + def create(self, req, body): |
3847 | """Creates a new volume.""" |
3848 | context = req.environ['nova.context'] |
3849 | |
3850 | - env = self._deserialize(req.body, req.get_content_type()) |
3851 | - if not env: |
3852 | + if not body: |
3853 | return faults.Fault(exc.HTTPUnprocessableEntity()) |
3854 | |
3855 | - vol = env['volume'] |
3856 | + vol = body['volume'] |
3857 | size = vol['size'] |
3858 | LOG.audit(_("Create volume of %s GB"), size, context=context) |
3859 | - new_volume = self.volume_api.create(context, size, |
3860 | + new_volume = self.volume_api.create(context, size, None, |
3861 | vol.get('display_name'), |
3862 | vol.get('display_description')) |
3863 | |
3864 | @@ -175,7 +173,7 @@ |
3865 | return d |
3866 | |
3867 | |
3868 | -class VolumeAttachmentController(wsgi.Controller): |
3869 | +class VolumeAttachmentController(object): |
3870 | """The volume attachment API controller for the Openstack API. |
3871 | |
3872 | A child resource of the server. Note that we use the volume id |
3873 | @@ -219,17 +217,16 @@ |
3874 | return {'volumeAttachment': _translate_attachment_detail_view(context, |
3875 | vol)} |
3876 | |
3877 | - def create(self, req, server_id): |
3878 | + def create(self, req, server_id, body): |
3879 | """Attach a volume to an instance.""" |
3880 | context = req.environ['nova.context'] |
3881 | |
3882 | - env = self._deserialize(req.body, req.get_content_type()) |
3883 | - if not env: |
3884 | + if not body: |
3885 | return faults.Fault(exc.HTTPUnprocessableEntity()) |
3886 | |
3887 | instance_id = server_id |
3888 | - volume_id = env['volumeAttachment']['volumeId'] |
3889 | - device = env['volumeAttachment']['device'] |
3890 | + volume_id = body['volumeAttachment']['volumeId'] |
3891 | + device = body['volumeAttachment']['device'] |
3892 | |
3893 | msg = _("Attach volume %(volume_id)s to instance %(server_id)s" |
3894 | " at %(device)s") % locals() |
3895 | @@ -259,7 +256,7 @@ |
3896 | # TODO(justinsb): How do I return "accepted" here? |
3897 | return {'volumeAttachment': attachment} |
3898 | |
3899 | - def update(self, _req, _server_id, _id): |
3900 | + def update(self, req, server_id, id, body): |
3901 | """Update a volume attachment. We don't currently support this.""" |
3902 | return faults.Fault(exc.HTTPBadRequest()) |
3903 | |
3904 | @@ -304,7 +301,7 @@ |
3905 | return "Volumes" |
3906 | |
3907 | def get_alias(self): |
3908 | - return "VOLUMES" |
3909 | + return "os-volumes" |
3910 | |
3911 | def get_description(self): |
3912 | return "Volumes support" |
3913 | @@ -320,12 +317,12 @@ |
3914 | |
3915 | # NOTE(justinsb): No way to provide singular name ('volume') |
3916 | # Does this matter? |
3917 | - res = extensions.ResourceExtension('volumes', |
3918 | + res = extensions.ResourceExtension('os-volumes', |
3919 | VolumeController(), |
3920 | collection_actions={'detail': 'GET'}) |
3921 | resources.append(res) |
3922 | |
3923 | - res = extensions.ResourceExtension('volume_attachments', |
3924 | + res = extensions.ResourceExtension('os-volume_attachments', |
3925 | VolumeAttachmentController(), |
3926 | parent=dict( |
3927 | member_name='server', |
3928 | |
3929 | === added file 'nova/api/openstack/create_instance_helper.py' |
3930 | --- nova/api/openstack/create_instance_helper.py 1970-01-01 00:00:00 +0000 |
3931 | +++ nova/api/openstack/create_instance_helper.py 2011-07-14 20:24:25 +0000 |
3932 | @@ -0,0 +1,354 @@ |
3933 | +# Copyright 2011 OpenStack LLC. |
3934 | +# All Rights Reserved. |
3935 | +# |
3936 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
3937 | +# not use this file except in compliance with the License. You may obtain |
3938 | +# a copy of the License at |
3939 | +# |
3940 | +# http://www.apache.org/licenses/LICENSE-2.0 |
3941 | +# |
3942 | +# Unless required by applicable law or agreed to in writing, software |
3943 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
3944 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
3945 | +# License for the specific language governing permissions and limitations |
3946 | +# under the License. |
3947 | + |
3948 | +import base64 |
3949 | +import re |
3950 | +import webob |
3951 | + |
3952 | +from webob import exc |
3953 | +from xml.dom import minidom |
3954 | + |
3955 | +from nova import exception |
3956 | +from nova import flags |
3957 | +from nova import log as logging |
3958 | +import nova.image |
3959 | +from nova import quota |
3960 | +from nova import utils |
3961 | + |
3962 | +from nova.compute import instance_types |
3963 | +from nova.api.openstack import faults |
3964 | +from nova.api.openstack import wsgi |
3965 | +from nova.auth import manager as auth_manager |
3966 | + |
3967 | + |
3968 | +LOG = logging.getLogger('nova.api.openstack.create_instance_helper') |
3969 | +FLAGS = flags.FLAGS |
3970 | + |
3971 | + |
3972 | +class CreateFault(exception.NovaException): |
3973 | + message = _("Invalid parameters given to create_instance.") |
3974 | + |
3975 | + def __init__(self, fault): |
3976 | + self.fault = fault |
3977 | + super(CreateFault, self).__init__() |
3978 | + |
3979 | + |
3980 | +class CreateInstanceHelper(object): |
3981 | + """This is the base class for OS API Controllers that |
3982 | + are capable of creating instances (currently Servers and Zones). |
3983 | + |
3984 | + Once we stabilize the Zones portion of the API we may be able |
3985 | + to move this code back into servers.py |
3986 | + """ |
3987 | + |
3988 | + def __init__(self, controller): |
3989 | + """We need the image service to create an instance.""" |
3990 | + self.controller = controller |
3991 | + self._image_service = utils.import_object(FLAGS.image_service) |
3992 | + super(CreateInstanceHelper, self).__init__() |
3993 | + |
3994 | + def create_instance(self, req, body, create_method): |
3995 | + """Creates a new server for the given user. The approach |
3996 | + used depends on the create_method. For example, the standard |
3997 | + POST /server call uses compute.api.create(), while |
3998 | + POST /zones/server uses compute.api.create_all_at_once(). |
3999 | + |
4000 | + The problem is, both approaches return different values (i.e. |
4001 | + [instance dicts] vs. reservation_id). So the handling of the |
4002 | + return type from this method is left to the caller. |
4003 | + """ |
4004 | + if not body: |
4005 | + raise faults.Fault(exc.HTTPUnprocessableEntity()) |
4006 | + |
4007 | + context = req.environ['nova.context'] |
4008 | + |
4009 | + password = self.controller._get_server_admin_password(body['server']) |
4010 | + |
4011 | + key_name = None |
4012 | + key_data = None |
4013 | + key_pairs = auth_manager.AuthManager.get_key_pairs(context) |
4014 | + if key_pairs: |
4015 | + key_pair = key_pairs[0] |
4016 | + key_name = key_pair['name'] |
4017 | + key_data = key_pair['public_key'] |
4018 | + |
4019 | + image_href = self.controller._image_ref_from_req_data(body) |
4020 | + try: |
4021 | + image_service, image_id = nova.image.get_image_service(image_href) |
4022 | + kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image( |
4023 | + req, image_id) |
4024 | + images = set([str(x['id']) for x in image_service.index(context)]) |
4025 | + assert str(image_id) in images |
4026 | + except Exception, e: |
4027 | + msg = _("Cannot find requested image %(image_href)s: %(e)s" % |
4028 | + locals()) |
4029 | + raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) |
4030 | + |
4031 | + personality = body['server'].get('personality') |
4032 | + |
4033 | + injected_files = [] |
4034 | + if personality: |
4035 | + injected_files = self._get_injected_files(personality) |
4036 | + |
4037 | + flavor_id = self.controller._flavor_id_from_req_data(body) |
4038 | + |
4039 | + if not 'name' in body['server']: |
4040 | + msg = _("Server name is not defined") |
4041 | + raise exc.HTTPBadRequest(explanation=msg) |
4042 | + |
4043 | + zone_blob = body['server'].get('blob') |
4044 | + name = body['server']['name'] |
4045 | + self._validate_server_name(name) |
4046 | + name = name.strip() |
4047 | + |
4048 | + reservation_id = body['server'].get('reservation_id') |
4049 | + min_count = body['server'].get('min_count') |
4050 | + max_count = body['server'].get('max_count') |
4051 | + # min_count and max_count are optional. If they exist, they come |
4052 | + # in as strings. We want to default 'min_count' to 1, and default |
4053 | + # 'max_count' to be 'min_count'. |
4054 | + min_count = int(min_count) if min_count else 1 |
4055 | + max_count = int(max_count) if max_count else min_count |
4056 | + if min_count > max_count: |
4057 | + min_count = max_count |
4058 | + |
4059 | + try: |
4060 | + inst_type = \ |
4061 | + instance_types.get_instance_type_by_flavor_id(flavor_id) |
4062 | + extra_values = { |
4063 | + 'instance_type': inst_type, |
4064 | + 'image_ref': image_href, |
4065 | + 'password': password} |
4066 | + |
4067 | + return (extra_values, |
4068 | + create_method(context, |
4069 | + inst_type, |
4070 | + image_id, |
4071 | + kernel_id=kernel_id, |
4072 | + ramdisk_id=ramdisk_id, |
4073 | + display_name=name, |
4074 | + display_description=name, |
4075 | + key_name=key_name, |
4076 | + key_data=key_data, |
4077 | + metadata=body['server'].get('metadata', {}), |
4078 | + injected_files=injected_files, |
4079 | + admin_password=password, |
4080 | + zone_blob=zone_blob, |
4081 | + reservation_id=reservation_id, |
4082 | + min_count=min_count, |
4083 | + max_count=max_count)) |
4084 | + except quota.QuotaError as error: |
4085 | + self._handle_quota_error(error) |
4086 | + except exception.ImageNotFound as error: |
4087 | + msg = _("Can not find requested image") |
4088 | + raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) |
4089 | + |
4090 | + # Let the caller deal with unhandled exceptions. |
4091 | + |
4092 | + def _handle_quota_error(self, error): |
4093 | + """ |
4094 | + Reraise quota errors as api-specific http exceptions |
4095 | + """ |
4096 | + if error.code == "OnsetFileLimitExceeded": |
4097 | + expl = _("Personality file limit exceeded") |
4098 | + raise exc.HTTPBadRequest(explanation=expl) |
4099 | + if error.code == "OnsetFilePathLimitExceeded": |
4100 | + expl = _("Personality file path too long") |
4101 | + raise exc.HTTPBadRequest(explanation=expl) |
4102 | + if error.code == "OnsetFileContentLimitExceeded": |
4103 | + expl = _("Personality file content too long") |
4104 | + raise exc.HTTPBadRequest(explanation=expl) |
4105 | + # if the original error is okay, just reraise it |
4106 | + raise error |
4107 | + |
4108 | + def _deserialize_create(self, request): |
4109 | + """ |
4110 | + Deserialize a create request |
4111 | + |
4112 | + Overrides normal behavior in the case of xml content |
4113 | + """ |
4114 | + if request.content_type == "application/xml": |
4115 | + deserializer = ServerCreateRequestXMLDeserializer() |
4116 | + return deserializer.deserialize(request.body) |
4117 | + else: |
4118 | + return self._deserialize(request.body, request.get_content_type()) |
4119 | + |
4120 | + def _validate_server_name(self, value): |
4121 | + if not isinstance(value, basestring): |
4122 | + msg = _("Server name is not a string or unicode") |
4123 | + raise exc.HTTPBadRequest(explanation=msg) |
4124 | + |
4125 | + if value.strip() == '': |
4126 | + msg = _("Server name is an empty string") |
4127 | + raise exc.HTTPBadRequest(explanation=msg) |
4128 | + |
4129 | + def _get_kernel_ramdisk_from_image(self, req, image_id): |
4130 | + """Fetch an image from the ImageService, then if present, return the |
4131 | + associated kernel and ramdisk image IDs. |
4132 | + """ |
4133 | + context = req.environ['nova.context'] |
4134 | + image_meta = self._image_service.show(context, image_id) |
4135 | + # NOTE(sirp): extracted to a separate method to aid unit-testing, the |
4136 | + # new method doesn't need a request obj or an ImageService stub |
4137 | + kernel_id, ramdisk_id = self._do_get_kernel_ramdisk_from_image( |
4138 | + image_meta) |
4139 | + return kernel_id, ramdisk_id |
4140 | + |
4141 | + @staticmethod |
4142 | + def _do_get_kernel_ramdisk_from_image(image_meta): |
4143 | + """Given an ImageService image_meta, return kernel and ramdisk image |
4144 | + ids if present. |
4145 | + |
4146 | + This is only valid for `ami` style images. |
4147 | + """ |
4148 | + image_id = image_meta['id'] |
4149 | + if image_meta['status'] != 'active': |
4150 | + raise exception.ImageUnacceptable(image_id=image_id, |
4151 | + reason=_("status is not active")) |
4152 | + |
4153 | + if image_meta.get('container_format') != 'ami': |
4154 | + return None, None |
4155 | + |
4156 | + try: |
4157 | + kernel_id = image_meta['properties']['kernel_id'] |
4158 | + except KeyError: |
4159 | + raise exception.KernelNotFoundForImage(image_id=image_id) |
4160 | + |
4161 | + try: |
4162 | + ramdisk_id = image_meta['properties']['ramdisk_id'] |
4163 | + except KeyError: |
4164 | + raise exception.RamdiskNotFoundForImage(image_id=image_id) |
4165 | + |
4166 | + return kernel_id, ramdisk_id |
4167 | + |
4168 | + def _get_injected_files(self, personality): |
4169 | + """ |
4170 | + Create a list of injected files from the personality attribute |
4171 | + |
4172 | + At this time, injected_files must be formatted as a list of |
4173 | + (file_path, file_content) pairs for compatibility with the |
4174 | + underlying compute service. |
4175 | + """ |
4176 | + injected_files = [] |
4177 | + |
4178 | + for item in personality: |
4179 | + try: |
4180 | + path = item['path'] |
4181 | + contents = item['contents'] |
4182 | + except KeyError as key: |
4183 | + expl = _('Bad personality format: missing %s') % key |
4184 | + raise exc.HTTPBadRequest(explanation=expl) |
4185 | + except TypeError: |
4186 | + expl = _('Bad personality format') |
4187 | + raise exc.HTTPBadRequest(explanation=expl) |
4188 | + try: |
4189 | + contents = base64.b64decode(contents) |
4190 | + except TypeError: |
4191 | + expl = _('Personality content for %s cannot be decoded') % path |
4192 | + raise exc.HTTPBadRequest(explanation=expl) |
4193 | + injected_files.append((path, contents)) |
4194 | + return injected_files |
4195 | + |
4196 | + def _get_server_admin_password_old_style(self, server): |
4197 | + """ Determine the admin password for a server on creation """ |
4198 | + return utils.generate_password(16) |
4199 | + |
4200 | + def _get_server_admin_password_new_style(self, server): |
4201 | + """ Determine the admin password for a server on creation """ |
4202 | + password = server.get('adminPass') |
4203 | + |
4204 | + if password is None: |
4205 | + return utils.generate_password(16) |
4206 | + if not isinstance(password, basestring) or password == '': |
4207 | + msg = _("Invalid adminPass") |
4208 | + raise exc.HTTPBadRequest(explanation=msg) |
4209 | + return password |
4210 | + |
4211 | + |
4212 | +class ServerXMLDeserializer(wsgi.XMLDeserializer): |
4213 | + """ |
4214 | + Deserializer to handle xml-formatted server create requests. |
4215 | + |
4216 | + Handles standard server attributes as well as optional metadata |
4217 | + and personality attributes |
4218 | + """ |
4219 | + |
4220 | + def create(self, string): |
4221 | + """Deserialize an xml-formatted server create request""" |
4222 | + dom = minidom.parseString(string) |
4223 | + server = self._extract_server(dom) |
4224 | + return {'body': {'server': server}} |
4225 | + |
4226 | + def _extract_server(self, node): |
4227 | + """Marshal the server attribute of a parsed request""" |
4228 | + server = {} |
4229 | + server_node = self._find_first_child_named(node, 'server') |
4230 | + for attr in ["name", "imageId", "flavorId", "imageRef", "flavorRef"]: |
4231 | + if server_node.getAttribute(attr): |
4232 | + server[attr] = server_node.getAttribute(attr) |
4233 | + metadata = self._extract_metadata(server_node) |
4234 | + if metadata is not None: |
4235 | + server["metadata"] = metadata |
4236 | + personality = self._extract_personality(server_node) |
4237 | + if personality is not None: |
4238 | + server["personality"] = personality |
4239 | + return server |
4240 | + |
4241 | + def _extract_metadata(self, server_node): |
4242 | + """Marshal the metadata attribute of a parsed request""" |
4243 | + metadata_node = self._find_first_child_named(server_node, "metadata") |
4244 | + if metadata_node is None: |
4245 | + return None |
4246 | + metadata = {} |
4247 | + for meta_node in self._find_children_named(metadata_node, "meta"): |
4248 | + key = meta_node.getAttribute("key") |
4249 | + metadata[key] = self._extract_text(meta_node) |
4250 | + return metadata |
4251 | + |
4252 | + def _extract_personality(self, server_node): |
4253 | + """Marshal the personality attribute of a parsed request""" |
4254 | + personality_node = \ |
4255 | + self._find_first_child_named(server_node, "personality") |
4256 | + if personality_node is None: |
4257 | + return None |
4258 | + personality = [] |
4259 | + for file_node in self._find_children_named(personality_node, "file"): |
4260 | + item = {} |
4261 | + if file_node.hasAttribute("path"): |
4262 | + item["path"] = file_node.getAttribute("path") |
4263 | + item["contents"] = self._extract_text(file_node) |
4264 | + personality.append(item) |
4265 | + return personality |
4266 | + |
4267 | + def _find_first_child_named(self, parent, name): |
4268 | + """Search a nodes children for the first child with a given name""" |
4269 | + for node in parent.childNodes: |
4270 | + if node.nodeName == name: |
4271 | + return node |
4272 | + return None |
4273 | + |
4274 | + def _find_children_named(self, parent, name): |
4275 | + """Return all of a nodes children who have the given name""" |
4276 | + for node in parent.childNodes: |
4277 | + if node.nodeName == name: |
4278 | + yield node |
4279 | + |
4280 | + def _extract_text(self, node): |
4281 | + """Get the text field contained by the given node""" |
4282 | + if len(node.childNodes) == 1: |
4283 | + child = node.childNodes[0] |
4284 | + if child.nodeType == child.TEXT_NODE: |
4285 | + return child.nodeValue |
4286 | + return "" |
4287 | |
4288 | === modified file 'nova/api/openstack/extensions.py' |
4289 | --- nova/api/openstack/extensions.py 2011-05-17 03:14:51 +0000 |
4290 | +++ nova/api/openstack/extensions.py 2011-07-14 20:24:25 +0000 |
4291 | @@ -27,9 +27,10 @@ |
4292 | from nova import exception |
4293 | from nova import flags |
4294 | from nova import log as logging |
4295 | -from nova import wsgi |
4296 | +from nova import wsgi as base_wsgi |
4297 | from nova.api.openstack import common |
4298 | from nova.api.openstack import faults |
4299 | +from nova.api.openstack import wsgi |
4300 | |
4301 | |
4302 | LOG = logging.getLogger('extensions') |
4303 | @@ -115,28 +116,34 @@ |
4304 | return request_exts |
4305 | |
4306 | |
4307 | -class ActionExtensionController(common.OpenstackController): |
4308 | - |
4309 | +class ActionExtensionController(object): |
4310 | def __init__(self, application): |
4311 | - |
4312 | self.application = application |
4313 | self.action_handlers = {} |
4314 | |
4315 | def add_action(self, action_name, handler): |
4316 | self.action_handlers[action_name] = handler |
4317 | |
4318 | - def action(self, req, id): |
4319 | - |
4320 | - input_dict = self._deserialize(req.body, req.get_content_type()) |
4321 | + def action(self, req, id, body): |
4322 | for action_name, handler in self.action_handlers.iteritems(): |
4323 | - if action_name in input_dict: |
4324 | - return handler(input_dict, req, id) |
4325 | + if action_name in body: |
4326 | + return handler(body, req, id) |
4327 | # no action handler found (bump to downstream application) |
4328 | res = self.application |
4329 | return res |
4330 | |
4331 | |
4332 | -class RequestExtensionController(common.OpenstackController): |
4333 | +class ActionExtensionResource(wsgi.Resource): |
4334 | + |
4335 | + def __init__(self, application): |
4336 | + controller = ActionExtensionController(application) |
4337 | + wsgi.Resource.__init__(self, controller) |
4338 | + |
4339 | + def add_action(self, action_name, handler): |
4340 | + self.controller.add_action(action_name, handler) |
4341 | + |
4342 | + |
4343 | +class RequestExtensionController(object): |
4344 | |
4345 | def __init__(self, application): |
4346 | self.application = application |
4347 | @@ -153,7 +160,17 @@ |
4348 | return res |
4349 | |
4350 | |
4351 | -class ExtensionController(common.OpenstackController): |
4352 | +class RequestExtensionResource(wsgi.Resource): |
4353 | + |
4354 | + def __init__(self, application): |
4355 | + controller = RequestExtensionController(application) |
4356 | + wsgi.Resource.__init__(self, controller) |
4357 | + |
4358 | + def add_handler(self, handler): |
4359 | + self.controller.add_handler(handler) |
4360 | + |
4361 | + |
4362 | +class ExtensionsResource(wsgi.Resource): |
4363 | |
4364 | def __init__(self, extension_manager): |
4365 | self.extension_manager = extension_manager |
4366 | @@ -186,7 +203,7 @@ |
4367 | raise faults.Fault(webob.exc.HTTPNotFound()) |
4368 | |
4369 | |
4370 | -class ExtensionMiddleware(wsgi.Middleware): |
4371 | +class ExtensionMiddleware(base_wsgi.Middleware): |
4372 | """Extensions middleware for WSGI.""" |
4373 | @classmethod |
4374 | def factory(cls, global_config, **local_config): |
4375 | @@ -195,43 +212,43 @@ |
4376 | return cls(app, **local_config) |
4377 | return _factory |
4378 | |
4379 | - def _action_ext_controllers(self, application, ext_mgr, mapper): |
4380 | - """Return a dict of ActionExtensionController-s by collection.""" |
4381 | - action_controllers = {} |
4382 | + def _action_ext_resources(self, application, ext_mgr, mapper): |
4383 | + """Return a dict of ActionExtensionResource-s by collection.""" |
4384 | + action_resources = {} |
4385 | for action in ext_mgr.get_actions(): |
4386 | - if not action.collection in action_controllers.keys(): |
4387 | - controller = ActionExtensionController(application) |
4388 | + if not action.collection in action_resources.keys(): |
4389 | + resource = ActionExtensionResource(application) |
4390 | mapper.connect("/%s/:(id)/action.:(format)" % |
4391 | action.collection, |
4392 | action='action', |
4393 | - controller=controller, |
4394 | + controller=resource, |
4395 | conditions=dict(method=['POST'])) |
4396 | mapper.connect("/%s/:(id)/action" % action.collection, |
4397 | action='action', |
4398 | - controller=controller, |
4399 | + controller=resource, |
4400 | conditions=dict(method=['POST'])) |
4401 | - action_controllers[action.collection] = controller |
4402 | - |
4403 | - return action_controllers |
4404 | - |
4405 | - def _request_ext_controllers(self, application, ext_mgr, mapper): |
4406 | - """Returns a dict of RequestExtensionController-s by collection.""" |
4407 | - request_ext_controllers = {} |
4408 | + action_resources[action.collection] = resource |
4409 | + |
4410 | + return action_resources |
4411 | + |
4412 | + def _request_ext_resources(self, application, ext_mgr, mapper): |
4413 | + """Returns a dict of RequestExtensionResource-s by collection.""" |
4414 | + request_ext_resources = {} |
4415 | for req_ext in ext_mgr.get_request_extensions(): |
4416 | - if not req_ext.key in request_ext_controllers.keys(): |
4417 | - controller = RequestExtensionController(application) |
4418 | + if not req_ext.key in request_ext_resources.keys(): |
4419 | + resource = RequestExtensionResource(application) |
4420 | mapper.connect(req_ext.url_route + '.:(format)', |
4421 | action='process', |
4422 | - controller=controller, |
4423 | + controller=resource, |
4424 | conditions=req_ext.conditions) |
4425 | |
4426 | mapper.connect(req_ext.url_route, |
4427 | action='process', |
4428 | - controller=controller, |
4429 | + controller=resource, |
4430 | conditions=req_ext.conditions) |
4431 | - request_ext_controllers[req_ext.key] = controller |
4432 | + request_ext_resources[req_ext.key] = resource |
4433 | |
4434 | - return request_ext_controllers |
4435 | + return request_ext_resources |
4436 | |
4437 | def __init__(self, application, ext_mgr=None): |
4438 | |
4439 | @@ -246,22 +263,22 @@ |
4440 | LOG.debug(_('Extended resource: %s'), |
4441 | resource.collection) |
4442 | mapper.resource(resource.collection, resource.collection, |
4443 | - controller=resource.controller, |
4444 | + controller=wsgi.Resource(resource.controller), |
4445 | collection=resource.collection_actions, |
4446 | member=resource.member_actions, |
4447 | parent_resource=resource.parent) |
4448 | |
4449 | # extended actions |
4450 | - action_controllers = self._action_ext_controllers(application, ext_mgr, |
4451 | + action_resources = self._action_ext_resources(application, ext_mgr, |
4452 | mapper) |
4453 | for action in ext_mgr.get_actions(): |
4454 | LOG.debug(_('Extended action: %s'), action.action_name) |
4455 | - controller = action_controllers[action.collection] |
4456 | - controller.add_action(action.action_name, action.handler) |
4457 | + resource = action_resources[action.collection] |
4458 | + resource.add_action(action.action_name, action.handler) |
4459 | |
4460 | # extended requests |
4461 | - req_controllers = self._request_ext_controllers(application, ext_mgr, |
4462 | - mapper) |
4463 | + req_controllers = self._request_ext_resources(application, ext_mgr, |
4464 | + mapper) |
4465 | for request_ext in ext_mgr.get_request_extensions(): |
4466 | LOG.debug(_('Extended request: %s'), request_ext.key) |
4467 | controller = req_controllers[request_ext.key] |
4468 | @@ -313,7 +330,7 @@ |
4469 | """Returns a list of ResourceExtension objects.""" |
4470 | resources = [] |
4471 | resources.append(ResourceExtension('extensions', |
4472 | - ExtensionController(self))) |
4473 | + ExtensionsResource(self))) |
4474 | for alias, ext in self.extensions.iteritems(): |
4475 | try: |
4476 | resources.extend(ext.get_resources()) |
4477 | @@ -357,6 +374,8 @@ |
4478 | LOG.debug(_('Ext updated: %s'), extension.get_updated()) |
4479 | except AttributeError as ex: |
4480 | LOG.exception(_("Exception loading extension: %s"), unicode(ex)) |
4481 | + return False |
4482 | + return True |
4483 | |
4484 | def _load_all_extensions(self): |
4485 | """Load extensions from the configured path. |
4486 | @@ -395,22 +414,23 @@ |
4487 | 'file': ext_path}) |
4488 | continue |
4489 | new_ext = new_ext_class() |
4490 | - self._check_extension(new_ext) |
4491 | - self._add_extension(new_ext) |
4492 | - |
4493 | - def _add_extension(self, ext): |
4494 | + self.add_extension(new_ext) |
4495 | + |
4496 | + def add_extension(self, ext): |
4497 | + # Do nothing if the extension doesn't check out |
4498 | + if not self._check_extension(ext): |
4499 | + return |
4500 | + |
4501 | alias = ext.get_alias() |
4502 | LOG.audit(_('Loaded extension: %s'), alias) |
4503 | |
4504 | - self._check_extension(ext) |
4505 | - |
4506 | if alias in self.extensions: |
4507 | raise exception.Error("Found duplicate extension: %s" % alias) |
4508 | self.extensions[alias] = ext |
4509 | |
4510 | |
4511 | class RequestExtension(object): |
4512 | - """Extend requests and responses of core nova OpenStack API controllers. |
4513 | + """Extend requests and responses of core nova OpenStack API resources. |
4514 | |
4515 | Provide a way to add data to responses and handle custom request data |
4516 | that is sent to core nova OpenStack API controllers. |
4517 | @@ -424,7 +444,7 @@ |
4518 | |
4519 | |
4520 | class ActionExtension(object): |
4521 | - """Add custom actions to core nova OpenStack API controllers.""" |
4522 | + """Add custom actions to core nova OpenStack API resources.""" |
4523 | |
4524 | def __init__(self, collection, action_name, handler): |
4525 | self.collection = collection |
4526 | |
4527 | === modified file 'nova/api/openstack/faults.py' |
4528 | --- nova/api/openstack/faults.py 2011-04-07 18:55:42 +0000 |
4529 | +++ nova/api/openstack/faults.py 2011-07-14 20:24:25 +0000 |
4530 | @@ -19,8 +19,7 @@ |
4531 | import webob.dec |
4532 | import webob.exc |
4533 | |
4534 | -from nova import wsgi |
4535 | -from nova.api.openstack import common |
4536 | +from nova.api.openstack import wsgi |
4537 | |
4538 | |
4539 | class Fault(webob.exc.HTTPException): |
4540 | @@ -55,13 +54,21 @@ |
4541 | if code == 413: |
4542 | retry = self.wrapped_exc.headers['Retry-After'] |
4543 | fault_data[fault_name]['retryAfter'] = retry |
4544 | + |
4545 | # 'code' is an attribute on the fault tag itself |
4546 | - metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} |
4547 | - default_xmlns = common.XML_NS_V10 |
4548 | - serializer = wsgi.Serializer(metadata, default_xmlns) |
4549 | + metadata = {'attributes': {fault_name: 'code'}} |
4550 | + |
4551 | content_type = req.best_match_content_type() |
4552 | - self.wrapped_exc.body = serializer.serialize(fault_data, content_type) |
4553 | + |
4554 | + serializer = { |
4555 | + 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, |
4556 | + xmlns=wsgi.XMLNS_V10), |
4557 | + 'application/json': wsgi.JSONDictSerializer(), |
4558 | + }[content_type] |
4559 | + |
4560 | + self.wrapped_exc.body = serializer.serialize(fault_data) |
4561 | self.wrapped_exc.content_type = content_type |
4562 | + |
4563 | return self.wrapped_exc |
4564 | |
4565 | |
4566 | @@ -70,14 +77,6 @@ |
4567 | Rate-limited request response. |
4568 | """ |
4569 | |
4570 | - _serialization_metadata = { |
4571 | - "application/xml": { |
4572 | - "attributes": { |
4573 | - "overLimitFault": "code", |
4574 | - }, |
4575 | - }, |
4576 | - } |
4577 | - |
4578 | def __init__(self, message, details, retry_time): |
4579 | """ |
4580 | Initialize new `OverLimitFault` with relevant information. |
4581 | @@ -97,8 +96,16 @@ |
4582 | Return the wrapped exception with a serialized body conforming to our |
4583 | error format. |
4584 | """ |
4585 | - serializer = wsgi.Serializer(self._serialization_metadata) |
4586 | content_type = request.best_match_content_type() |
4587 | - content = serializer.serialize(self.content, content_type) |
4588 | + metadata = {"attributes": {"overLimitFault": "code"}} |
4589 | + |
4590 | + serializer = { |
4591 | + 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, |
4592 | + xmlns=wsgi.XMLNS_V10), |
4593 | + 'application/json': wsgi.JSONDictSerializer(), |
4594 | + }[content_type] |
4595 | + |
4596 | + content = serializer.serialize(self.content) |
4597 | self.wrapped_exc.body = content |
4598 | + |
4599 | return self.wrapped_exc |
4600 | |
4601 | === modified file 'nova/api/openstack/flavors.py' |
4602 | --- nova/api/openstack/flavors.py 2011-05-06 20:13:35 +0000 |
4603 | +++ nova/api/openstack/flavors.py 2011-07-14 20:24:25 +0000 |
4604 | @@ -19,22 +19,13 @@ |
4605 | |
4606 | from nova import db |
4607 | from nova import exception |
4608 | -from nova.api.openstack import common |
4609 | from nova.api.openstack import views |
4610 | - |
4611 | - |
4612 | -class Controller(common.OpenstackController): |
4613 | +from nova.api.openstack import wsgi |
4614 | + |
4615 | + |
4616 | +class Controller(object): |
4617 | """Flavor controller for the OpenStack API.""" |
4618 | |
4619 | - _serialization_metadata = { |
4620 | - 'application/xml': { |
4621 | - "attributes": { |
4622 | - "flavor": ["id", "name", "ram", "disk"], |
4623 | - "link": ["rel", "type", "href"], |
4624 | - } |
4625 | - } |
4626 | - } |
4627 | - |
4628 | def index(self, req): |
4629 | """Return all flavors in brief.""" |
4630 | items = self._get_flavors(req, is_detail=False) |
4631 | @@ -71,14 +62,33 @@ |
4632 | |
4633 | |
4634 | class ControllerV10(Controller): |
4635 | + |
4636 | def _get_view_builder(self, req): |
4637 | return views.flavors.ViewBuilder() |
4638 | |
4639 | |
4640 | class ControllerV11(Controller): |
4641 | + |
4642 | def _get_view_builder(self, req): |
4643 | base_url = req.application_url |
4644 | return views.flavors.ViewBuilderV11(base_url) |
4645 | |
4646 | - def get_default_xmlns(self, req): |
4647 | - return common.XML_NS_V11 |
4648 | + |
4649 | +def create_resource(version='1.0'): |
4650 | + controller = { |
4651 | + '1.0': ControllerV10, |
4652 | + '1.1': ControllerV11, |
4653 | + }[version]() |
4654 | + |
4655 | + xmlns = { |
4656 | + '1.0': wsgi.XMLNS_V10, |
4657 | + '1.1': wsgi.XMLNS_V11, |
4658 | + }[version] |
4659 | + |
4660 | + body_serializers = { |
4661 | + 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns), |
4662 | + } |
4663 | + |
4664 | + serializer = wsgi.ResponseSerializer(body_serializers) |
4665 | + |
4666 | + return wsgi.Resource(controller, serializer=serializer) |
4667 | |
4668 | === modified file 'nova/api/openstack/image_metadata.py' |
4669 | --- nova/api/openstack/image_metadata.py 2011-04-08 07:58:48 +0000 |
4670 | +++ nova/api/openstack/image_metadata.py 2011-07-14 20:24:25 +0000 |
4671 | @@ -16,24 +16,24 @@ |
4672 | # under the License. |
4673 | |
4674 | from webob import exc |
4675 | +from xml.dom import minidom |
4676 | |
4677 | from nova import flags |
4678 | +from nova import image |
4679 | from nova import quota |
4680 | from nova import utils |
4681 | -from nova import wsgi |
4682 | -from nova.api.openstack import common |
4683 | from nova.api.openstack import faults |
4684 | +from nova.api.openstack import wsgi |
4685 | |
4686 | |
4687 | FLAGS = flags.FLAGS |
4688 | |
4689 | |
4690 | -class Controller(common.OpenstackController): |
4691 | +class Controller(object): |
4692 | """The image metadata API controller for the Openstack API""" |
4693 | |
4694 | def __init__(self): |
4695 | - self.image_service = utils.import_object(FLAGS.image_service) |
4696 | - super(Controller, self).__init__() |
4697 | + self.image_service = image.get_default_image_service() |
4698 | |
4699 | def _get_metadata(self, context, image_id, image=None): |
4700 | if not image: |
4701 | @@ -60,13 +60,12 @@ |
4702 | context = req.environ['nova.context'] |
4703 | metadata = self._get_metadata(context, image_id) |
4704 | if id in metadata: |
4705 | - return {id: metadata[id]} |
4706 | + return {'meta': {id: metadata[id]}} |
4707 | else: |
4708 | return faults.Fault(exc.HTTPNotFound()) |
4709 | |
4710 | - def create(self, req, image_id): |
4711 | + def create(self, req, image_id, body): |
4712 | context = req.environ['nova.context'] |
4713 | - body = self._deserialize(req.body, req.get_content_type()) |
4714 | img = self.image_service.show(context, image_id) |
4715 | metadata = self._get_metadata(context, image_id, img) |
4716 | if 'metadata' in body: |
4717 | @@ -77,18 +76,24 @@ |
4718 | self.image_service.update(context, image_id, img, None) |
4719 | return dict(metadata=metadata) |
4720 | |
4721 | - def update(self, req, image_id, id): |
4722 | + def update(self, req, image_id, id, body): |
4723 | context = req.environ['nova.context'] |
4724 | - body = self._deserialize(req.body, req.get_content_type()) |
4725 | - if not id in body: |
4726 | + |
4727 | + try: |
4728 | + meta = body['meta'] |
4729 | + except KeyError: |
4730 | + expl = _('Incorrect request body format') |
4731 | + raise exc.HTTPBadRequest(explanation=expl) |
4732 | + |
4733 | + if not id in meta: |
4734 | expl = _('Request body and URI mismatch') |
4735 | raise exc.HTTPBadRequest(explanation=expl) |
4736 | - if len(body) > 1: |
4737 | + if len(meta) > 1: |
4738 | expl = _('Request body contains too many items') |
4739 | raise exc.HTTPBadRequest(explanation=expl) |
4740 | img = self.image_service.show(context, image_id) |
4741 | metadata = self._get_metadata(context, image_id, img) |
4742 | - metadata[id] = body[id] |
4743 | + metadata[id] = meta[id] |
4744 | self._check_quota_limit(context, metadata) |
4745 | img['properties'] = metadata |
4746 | self.image_service.update(context, image_id, img, None) |
4747 | @@ -104,3 +109,60 @@ |
4748 | metadata.pop(id) |
4749 | img['properties'] = metadata |
4750 | self.image_service.update(context, image_id, img, None) |
4751 | + |
4752 | + |
4753 | +class ImageMetadataXMLSerializer(wsgi.XMLDictSerializer): |
4754 | + def __init__(self, xmlns=wsgi.XMLNS_V11): |
4755 | + super(ImageMetadataXMLSerializer, self).__init__(xmlns=xmlns) |
4756 | + |
4757 | + def _meta_item_to_xml(self, doc, key, value): |
4758 | + node = doc.createElement('meta') |
4759 | + doc.appendChild(node) |
4760 | + node.setAttribute('key', '%s' % key) |
4761 | + text = doc.createTextNode('%s' % value) |
4762 | + node.appendChild(text) |
4763 | + return node |
4764 | + |
4765 | + def meta_list_to_xml(self, xml_doc, meta_items): |
4766 | + container_node = xml_doc.createElement('metadata') |
4767 | + for (key, value) in meta_items: |
4768 | + item_node = self._meta_item_to_xml(xml_doc, key, value) |
4769 | + container_node.appendChild(item_node) |
4770 | + return container_node |
4771 | + |
4772 | + def _meta_list_to_xml_string(self, metadata_dict): |
4773 | + xml_doc = minidom.Document() |
4774 | + items = metadata_dict['metadata'].items() |
4775 | + container_node = self.meta_list_to_xml(xml_doc, items) |
4776 | + xml_doc.appendChild(container_node) |
4777 | + self._add_xmlns(container_node) |
4778 | + return xml_doc.toprettyxml(indent=' ', encoding='UTF-8') |
4779 | + |
4780 | + def index(self, metadata_dict): |
4781 | + return self._meta_list_to_xml_string(metadata_dict) |
4782 | + |
4783 | + def create(self, metadata_dict): |
4784 | + return self._meta_list_to_xml_string(metadata_dict) |
4785 | + |
4786 | + def _meta_item_to_xml_string(self, meta_item_dict): |
4787 | + xml_doc = minidom.Document() |
4788 | + item_key, item_value = meta_item_dict.items()[0] |
4789 | + item_node = self._meta_item_to_xml(xml_doc, item_key, item_value) |
4790 | + xml_doc.appendChild(item_node) |
4791 | + self._add_xmlns(item_node) |
4792 | + return xml_doc.toprettyxml(indent=' ', encoding='UTF-8') |
4793 | + |
4794 | + def show(self, meta_item_dict): |
4795 | + return self._meta_item_to_xml_string(meta_item_dict['meta']) |
4796 | + |
4797 | + def update(self, meta_item_dict): |
4798 | + return self._meta_item_to_xml_string(meta_item_dict['meta']) |
4799 | + |
4800 | + |
4801 | +def create_resource(): |
4802 | + body_serializers = { |
4803 | + 'application/xml': ImageMetadataXMLSerializer(), |
4804 | + } |
4805 | + serializer = wsgi.ResponseSerializer(body_serializers) |
4806 | + |
4807 | + return wsgi.Resource(Controller(), serializer=serializer) |
4808 | |
4809 | === modified file 'nova/api/openstack/images.py' |
4810 | --- nova/api/openstack/images.py 2011-04-21 17:29:11 +0000 |
4811 | +++ nova/api/openstack/images.py 2011-07-14 20:24:25 +0000 |
4812 | @@ -13,88 +13,73 @@ |
4813 | # License for the specific language governing permissions and limitations |
4814 | # under the License. |
4815 | |
4816 | +import urlparse |
4817 | +import os.path |
4818 | + |
4819 | import webob.exc |
4820 | +from xml.dom import minidom |
4821 | |
4822 | from nova import compute |
4823 | from nova import exception |
4824 | from nova import flags |
4825 | +import nova.image |
4826 | from nova import log |
4827 | -from nova import utils |
4828 | from nova.api.openstack import common |
4829 | from nova.api.openstack import faults |
4830 | +from nova.api.openstack import image_metadata |
4831 | +from nova.api.openstack import servers |
4832 | from nova.api.openstack.views import images as images_view |
4833 | +from nova.api.openstack import wsgi |
4834 | |
4835 | |
4836 | LOG = log.getLogger('nova.api.openstack.images') |
4837 | FLAGS = flags.FLAGS |
4838 | |
4839 | - |
4840 | -class Controller(common.OpenstackController): |
4841 | - """Base `wsgi.Controller` for retrieving/displaying images.""" |
4842 | - |
4843 | - _serialization_metadata = { |
4844 | - 'application/xml': { |
4845 | - "attributes": { |
4846 | - "image": ["id", "name", "updated", "created", "status", |
4847 | - "serverId", "progress"], |
4848 | - "link": ["rel", "type", "href"], |
4849 | - }, |
4850 | - }, |
4851 | - } |
4852 | +SUPPORTED_FILTERS = ['name', 'status'] |
4853 | + |
4854 | + |
4855 | +class Controller(object): |
4856 | + """Base controller for retrieving/displaying images.""" |
4857 | |
4858 | def __init__(self, image_service=None, compute_service=None): |
4859 | """Initialize new `ImageController`. |
4860 | |
4861 | :param compute_service: `nova.compute.api:API` |
4862 | :param image_service: `nova.image.service:BaseImageService` |
4863 | + |
4864 | """ |
4865 | - _default_service = utils.import_object(flags.FLAGS.image_service) |
4866 | - |
4867 | self._compute_service = compute_service or compute.API() |
4868 | - self._image_service = image_service or _default_service |
4869 | - |
4870 | - def index(self, req): |
4871 | - """Return an index listing of images available to the request. |
4872 | - |
4873 | - :param req: `wsgi.Request` object |
4874 | - """ |
4875 | - context = req.environ['nova.context'] |
4876 | - images = self._image_service.index(context) |
4877 | - images = common.limited(images, req) |
4878 | - builder = self.get_builder(req).build |
4879 | - return dict(images=[builder(image, detail=False) for image in images]) |
4880 | - |
4881 | - def detail(self, req): |
4882 | - """Return a detailed index listing of images available to the request. |
4883 | - |
4884 | - :param req: `wsgi.Request` object. |
4885 | - """ |
4886 | - context = req.environ['nova.context'] |
4887 | - images = self._image_service.detail(context) |
4888 | - images = common.limited(images, req) |
4889 | - builder = self.get_builder(req).build |
4890 | - return dict(images=[builder(image, detail=True) for image in images]) |
4891 | + self._image_service = image_service or \ |
4892 | + nova.image.get_default_image_service() |
4893 | + |
4894 | + def _get_filters(self, req): |
4895 | + """ |
4896 | + Return a dictionary of query param filters from the request |
4897 | + |
4898 | + :param req: the Request object coming from the wsgi layer |
4899 | + :retval a dict of key/value filters |
4900 | + """ |
4901 | + filters = {} |
4902 | + for param in req.str_params: |
4903 | + if param in SUPPORTED_FILTERS or param.startswith('property-'): |
4904 | + filters[param] = req.str_params.get(param) |
4905 | + |
4906 | + return filters |
4907 | |
4908 | def show(self, req, id): |
4909 | """Return detailed information about a specific image. |
4910 | |
4911 | :param req: `wsgi.Request` object |
4912 | - :param id: Image identifier (integer) |
4913 | + :param id: Image identifier |
4914 | """ |
4915 | context = req.environ['nova.context'] |
4916 | |
4917 | try: |
4918 | - image_id = int(id) |
4919 | - except ValueError: |
4920 | + image = self._image_service.show(context, id) |
4921 | + except (exception.NotFound, exception.InvalidImageRef): |
4922 | explanation = _("Image not found.") |
4923 | raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation)) |
4924 | |
4925 | - try: |
4926 | - image = self._image_service.show(context, image_id) |
4927 | - except exception.NotFound: |
4928 | - explanation = _("Image '%d' not found.") % (image_id) |
4929 | - raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation)) |
4930 | - |
4931 | return dict(image=self.get_builder(req).build(image, detail=True)) |
4932 | |
4933 | def delete(self, req, id): |
4934 | @@ -103,35 +88,78 @@ |
4935 | :param req: `wsgi.Request` object |
4936 | :param id: Image identifier (integer) |
4937 | """ |
4938 | - image_id = id |
4939 | context = req.environ['nova.context'] |
4940 | - self._image_service.delete(context, image_id) |
4941 | + self._image_service.delete(context, id) |
4942 | return webob.exc.HTTPNoContent() |
4943 | |
4944 | - def create(self, req): |
4945 | - """Snapshot a server instance and save the image. |
4946 | + def create(self, req, body): |
4947 | + """Snapshot or backup a server instance and save the image. |
4948 | + |
4949 | + Images now have an `image_type` associated with them, which can be |
4950 | + 'snapshot' or the backup type, like 'daily' or 'weekly'. |
4951 | + |
4952 | + If the image_type is backup-like, then the rotation factor can be |
4953 | + included and that will cause the oldest backups that exceed the |
4954 | + rotation factor to be deleted. |
4955 | |
4956 | :param req: `wsgi.Request` object |
4957 | """ |
4958 | + def get_param(param): |
4959 | + try: |
4960 | + return body["image"][param] |
4961 | + except KeyError: |
4962 | + raise webob.exc.HTTPBadRequest(explanation="Missing required " |
4963 | + "param: %s" % param) |
4964 | + |
4965 | context = req.environ['nova.context'] |
4966 | content_type = req.get_content_type() |
4967 | - image = self._deserialize(req.body, content_type) |
4968 | |
4969 | - if not image: |
4970 | + if not body: |
4971 | raise webob.exc.HTTPBadRequest() |
4972 | |
4973 | + image_type = body["image"].get("image_type", "snapshot") |
4974 | + |
4975 | try: |
4976 | - server_id = image["image"]["serverId"] |
4977 | - image_name = image["image"]["name"] |
4978 | + server_id = self._server_id_from_req(req, body) |
4979 | except KeyError: |
4980 | raise webob.exc.HTTPBadRequest() |
4981 | |
4982 | - image = self._compute_service.snapshot(context, server_id, image_name) |
4983 | + image_name = get_param("name") |
4984 | + props = self._get_extra_properties(req, body) |
4985 | + |
4986 | + if image_type == "snapshot": |
4987 | + image = self._compute_service.snapshot( |
4988 | + context, server_id, image_name, |
4989 | + extra_properties=props) |
4990 | + elif image_type == "backup": |
4991 | + # NOTE(sirp): Unlike snapshot, backup is not a customer facing |
4992 | + # API call; rather, it's used by the internal backup scheduler |
4993 | + if not FLAGS.allow_admin_api: |
4994 | + raise webob.exc.HTTPBadRequest( |
4995 | + explanation="Admin API Required") |
4996 | + |
4997 | + backup_type = get_param("backup_type") |
4998 | + rotation = int(get_param("rotation")) |
4999 | + |
5000 | + image = self._compute_service.backup( |
The diff has been truncated for viewing.