summaryrefslogtreecommitdiffstats
path: root/nova/api
diff options
context:
space:
mode:
Diffstat (limited to 'nova/api')
-rw-r--r--nova/api/ec2/cloud.py7
-rw-r--r--nova/api/metadata/base.py6
-rw-r--r--nova/api/metadata/password.py9
-rw-r--r--nova/api/openstack/common.py19
-rw-r--r--nova/api/openstack/compute/contrib/attach_interfaces.py2
-rw-r--r--nova/api/openstack/compute/contrib/availability_zone.py2
-rw-r--r--nova/api/openstack/compute/contrib/cell_capacities.py2
-rw-r--r--nova/api/openstack/compute/contrib/certificates.py15
-rw-r--r--nova/api/openstack/compute/contrib/console_output.py2
-rw-r--r--nova/api/openstack/compute/contrib/consoles.py2
-rw-r--r--nova/api/openstack/compute/contrib/coverage_ext.py5
-rw-r--r--nova/api/openstack/compute/contrib/createserverext.py2
-rw-r--r--nova/api/openstack/compute/contrib/disk_config.py2
-rw-r--r--nova/api/openstack/compute/contrib/extended_availability_zone.py11
-rw-r--r--nova/api/openstack/compute/contrib/extended_floating_ips.py2
-rw-r--r--nova/api/openstack/compute/contrib/extended_ips.py2
-rw-r--r--nova/api/openstack/compute/contrib/fixed_ips.py3
-rw-r--r--nova/api/openstack/compute/contrib/flavorextraspecs.py2
-rw-r--r--nova/api/openstack/compute/contrib/flavormanage.py4
-rw-r--r--nova/api/openstack/compute/contrib/floating_ip_dns.py2
-rw-r--r--nova/api/openstack/compute/contrib/floating_ip_pools.py2
-rw-r--r--nova/api/openstack/compute/contrib/floating_ips.py2
-rw-r--r--nova/api/openstack/compute/contrib/multiple_create.py2
-rw-r--r--nova/api/openstack/compute/contrib/scheduler_hints.py2
-rw-r--r--nova/api/openstack/compute/contrib/security_groups.py144
-rw-r--r--nova/api/openstack/compute/contrib/server_password.py2
-rw-r--r--nova/api/openstack/compute/contrib/server_start_stop.py2
-rw-r--r--nova/api/openstack/compute/contrib/services.py3
-rw-r--r--nova/api/openstack/compute/contrib/used_limits.py2
-rw-r--r--nova/api/openstack/compute/contrib/used_limits_for_admin.py2
-rw-r--r--nova/api/openstack/compute/contrib/user_data.py2
-rw-r--r--nova/api/openstack/compute/extensions.py5
-rw-r--r--nova/api/openstack/compute/flavors.py3
-rw-r--r--nova/api/openstack/compute/limits.py34
-rw-r--r--nova/api/openstack/compute/plugins/v3/agents.py168
-rw-r--r--nova/api/openstack/compute/plugins/v3/cells.py347
-rw-r--r--nova/api/openstack/compute/plugins/v3/certificates.py97
-rw-r--r--nova/api/openstack/compute/plugins/v3/evacuate.py101
-rw-r--r--nova/api/openstack/compute/plugins/v3/extension_info.py23
-rw-r--r--nova/api/openstack/compute/plugins/v3/fixed_ips.py2
-rw-r--r--nova/api/openstack/compute/plugins/v3/flavor_access.py220
-rw-r--r--nova/api/openstack/compute/plugins/v3/flavor_disabled.py91
-rw-r--r--nova/api/openstack/compute/plugins/v3/flavors.py169
-rw-r--r--nova/api/openstack/compute/plugins/v3/images.py242
-rw-r--r--nova/api/openstack/compute/plugins/v3/keypairs.py3
-rw-r--r--nova/api/openstack/compute/plugins/v3/quota_classes.py103
-rw-r--r--nova/api/openstack/compute/plugins/v3/quota_sets.py214
-rw-r--r--nova/api/openstack/compute/plugins/v3/rescue.py100
-rw-r--r--nova/api/openstack/compute/plugins/v3/server_diagnostics.py71
-rw-r--r--nova/api/openstack/compute/schemas/v3/flavor.rng12
-rw-r--r--nova/api/openstack/compute/schemas/v3/flavors.rng6
-rw-r--r--nova/api/openstack/compute/servers.py11
-rw-r--r--nova/api/openstack/compute/views/servers.py4
-rw-r--r--nova/api/openstack/extensions.py44
-rw-r--r--nova/api/openstack/xmlutil.py8
55 files changed, 2174 insertions, 170 deletions
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 47575b201..51a86e02f 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -211,9 +211,8 @@ def db_to_inst_obj(context, db_instance):
# NOTE(danms): This is a temporary helper method for converting
# Instance DB objects to NovaObjects without needing to re-query.
inst_obj = instance_obj.Instance._from_db_object(
- instance_obj.Instance(), db_instance,
+ context, instance_obj.Instance(), db_instance,
expected_attrs=['system_metadata', 'metadata'])
- inst_obj._context = context
return inst_obj
@@ -802,7 +801,6 @@ class CloudController(object):
'detaching': 'in-use'}
instance_ec2_id = None
- instance_data = None
if volume.get('instance_uuid', None):
instance_uuid = volume['instance_uuid']
@@ -810,8 +808,7 @@ class CloudController(object):
instance_uuid)
instance_ec2_id = ec2utils.id_to_ec2_inst_id(instance_uuid)
- instance_data = '%s[%s]' % (instance_ec2_id,
- instance['host'])
+
v = {}
v['volumeId'] = ec2utils.id_to_ec2_vol_id(volume['id'])
v['status'] = valid_ec2_api_volume_status_map.get(volume['status'],
diff --git a/nova/api/metadata/base.py b/nova/api/metadata/base.py
index 425e5bf53..da0a50662 100644
--- a/nova/api/metadata/base.py
+++ b/nova/api/metadata/base.py
@@ -32,6 +32,8 @@ from nova.compute import flavors
from nova import conductor
from nova import context
from nova import network
+from nova import utils
+
from nova.openstack.common import timeutils
from nova.virt import netutils
@@ -127,9 +129,7 @@ class InstanceMetadata():
self.address = address
# expose instance metadata.
- self.launch_metadata = {}
- for item in instance.get('metadata', []):
- self.launch_metadata[item['key']] = item['value']
+ self.launch_metadata = utils.instance_meta(instance)
self.password = password.extract_password(instance)
diff --git a/nova/api/metadata/password.py b/nova/api/metadata/password.py
index f3453e945..793dcc0a7 100644
--- a/nova/api/metadata/password.py
+++ b/nova/api/metadata/password.py
@@ -27,10 +27,10 @@ MAX_SIZE = CHUNKS * CHUNK_LENGTH
def extract_password(instance):
result = ''
- for datum in sorted(instance.get('system_metadata', []),
- key=lambda x: x['key']):
- if datum['key'].startswith('password_'):
- result += datum['value']
+ sys_meta = utils.instance_sys_meta(instance)
+ for key in sorted(sys_meta.keys()):
+ if key.startswith('password_'):
+ result += sys_meta[key]
return result or None
@@ -49,7 +49,6 @@ def convert_password(context, password):
def handle_password(req, meta_data):
ctxt = context.get_admin_context()
- password = meta_data.password
if req.method == 'GET':
return meta_data.password
elif req.method == 'POST':
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index bec919f4b..509256d87 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -358,9 +358,12 @@ def raise_http_conflict_for_instance_invalid_state(exc, action):
"""
attr = exc.kwargs.get('attr')
state = exc.kwargs.get('state')
+ not_launched = exc.kwargs.get('not_launched')
if attr and state:
msg = _("Cannot '%(action)s' while instance is in %(attr)s "
"%(state)s") % {'action': action, 'attr': attr, 'state': state}
+ elif not_launched:
+ msg = _("Cannot '%s' an instance which has never been active") % action
else:
# At least give some meaningful message
msg = _("Instance is in an invalid state for '%s'") % action
@@ -454,6 +457,16 @@ def check_snapshots_enabled(f):
class ViewBuilder(object):
"""Model API responses as dictionaries."""
+ def _get_project_id(self, request):
+ """
+ Get project id from request url if present or empty string
+ otherwise
+ """
+ project_id = request.environ["nova.context"].project_id
+ if project_id in request.url:
+ return project_id
+ return ''
+
def _get_links(self, request, identifier, collection_name):
return [{
"rel": "self",
@@ -472,7 +485,7 @@ class ViewBuilder(object):
params["marker"] = identifier
prefix = self._update_compute_link_prefix(request.application_url)
url = os.path.join(prefix,
- request.environ["nova.context"].project_id,
+ self._get_project_id(request),
collection_name)
return "%s?%s" % (url, dict_to_query_str(params))
@@ -480,7 +493,7 @@ class ViewBuilder(object):
"""Return an href string pointing to this object."""
prefix = self._update_compute_link_prefix(request.application_url)
return os.path.join(prefix,
- request.environ["nova.context"].project_id,
+ self._get_project_id(request),
collection_name,
str(identifier))
@@ -489,7 +502,7 @@ class ViewBuilder(object):
base_url = remove_version_from_href(request.application_url)
base_url = self._update_compute_link_prefix(base_url)
return os.path.join(base_url,
- request.environ["nova.context"].project_id,
+ self._get_project_id(request),
collection_name,
str(identifier))
diff --git a/nova/api/openstack/compute/contrib/attach_interfaces.py b/nova/api/openstack/compute/contrib/attach_interfaces.py
index ec565a0d1..a823eed2b 100644
--- a/nova/api/openstack/compute/contrib/attach_interfaces.py
+++ b/nova/api/openstack/compute/contrib/attach_interfaces.py
@@ -60,7 +60,7 @@ class InterfaceAttachmentController(object):
port_id = id
try:
- instance = self.compute_api.get(context, server_id)
+ self.compute_api.get(context, server_id)
except exception.NotFound:
raise exc.HTTPNotFound()
diff --git a/nova/api/openstack/compute/contrib/availability_zone.py b/nova/api/openstack/compute/contrib/availability_zone.py
index 22001b65f..1f22bf252 100644
--- a/nova/api/openstack/compute/contrib/availability_zone.py
+++ b/nova/api/openstack/compute/contrib/availability_zone.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
from oslo.config import cfg
diff --git a/nova/api/openstack/compute/contrib/cell_capacities.py b/nova/api/openstack/compute/contrib/cell_capacities.py
index ae8b42336..ab79b4327 100644
--- a/nova/api/openstack/compute/contrib/cell_capacities.py
+++ b/nova/api/openstack/compute/contrib/cell_capacities.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
from nova.api.openstack import extensions
diff --git a/nova/api/openstack/compute/contrib/certificates.py b/nova/api/openstack/compute/contrib/certificates.py
index 64a6e26fe..979008a87 100644
--- a/nova/api/openstack/compute/contrib/certificates.py
+++ b/nova/api/openstack/compute/contrib/certificates.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
import webob.exc
@@ -38,15 +38,6 @@ class CertificateTemplate(xmlutil.TemplateBuilder):
return xmlutil.MasterTemplate(root, 1)
-class CertificatesTemplate(xmlutil.TemplateBuilder):
- def construct(self):
- root = xmlutil.TemplateElement('certificates')
- elem = xmlutil.SubTemplateElement(root, 'certificate',
- selector='certificates')
- make_certificate(elem)
- return xmlutil.MasterTemplate(root, 1)
-
-
def _translate_certificate_view(certificate, private_key=None):
return {
'data': certificate,
@@ -64,7 +55,7 @@ class CertificatesController(object):
@wsgi.serializers(xml=CertificateTemplate)
def show(self, req, id):
- """Return a list of certificates."""
+ """Return certificate information."""
context = req.environ['nova.context']
authorize(context)
if id != 'root':
@@ -76,7 +67,7 @@ class CertificatesController(object):
@wsgi.serializers(xml=CertificateTemplate)
def create(self, req, body=None):
- """Return a list of certificates."""
+ """Create a certificate."""
context = req.environ['nova.context']
authorize(context)
pk, cert = self.cert_rpcapi.generate_x509_cert(context,
diff --git a/nova/api/openstack/compute/contrib/console_output.py b/nova/api/openstack/compute/contrib/console_output.py
index eb20b3275..07b3f2556 100644
--- a/nova/api/openstack/compute/contrib/console_output.py
+++ b/nova/api/openstack/compute/contrib/console_output.py
@@ -14,7 +14,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
import re
import webob
diff --git a/nova/api/openstack/compute/contrib/consoles.py b/nova/api/openstack/compute/contrib/consoles.py
index bf1f41690..8247620f2 100644
--- a/nova/api/openstack/compute/contrib/consoles.py
+++ b/nova/api/openstack/compute/contrib/consoles.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
import webob
diff --git a/nova/api/openstack/compute/contrib/coverage_ext.py b/nova/api/openstack/compute/contrib/coverage_ext.py
index 154699470..fd1ad53e9 100644
--- a/nova/api/openstack/compute/contrib/coverage_ext.py
+++ b/nova/api/openstack/compute/contrib/coverage_ext.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
# See: http://wiki.openstack.org/Nova/CoverageExtension for more information
# and usage explanation for this API extension
@@ -134,11 +134,12 @@ class CoverageController(object):
# doesn't resolve to 127.0.0.1. Currently backdoors only open on
# loopback so this is for covering the common single host use case
except socket.error as e:
+ exc_info = sys.exc_info()
if 'ECONNREFUSED' in e and service['host'] == self.host:
service['telnet'] = telnetlib.Telnet('127.0.0.1',
service['port'])
else:
- raise e
+ raise exc_info[0], exc_info[1], exc_info[2]
self.services.append(service)
self._start_coverage_telnet(service['telnet'], service['service'])
diff --git a/nova/api/openstack/compute/contrib/createserverext.py b/nova/api/openstack/compute/contrib/createserverext.py
index 337fedae6..9559ceef3 100644
--- a/nova/api/openstack/compute/contrib/createserverext.py
+++ b/nova/api/openstack/compute/contrib/createserverext.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
from nova.api.openstack import extensions
diff --git a/nova/api/openstack/compute/contrib/disk_config.py b/nova/api/openstack/compute/contrib/disk_config.py
index f2b906144..692ed6455 100644
--- a/nova/api/openstack/compute/contrib/disk_config.py
+++ b/nova/api/openstack/compute/contrib/disk_config.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
"""Disk Config extension."""
diff --git a/nova/api/openstack/compute/contrib/extended_availability_zone.py b/nova/api/openstack/compute/contrib/extended_availability_zone.py
index 00765e209..6e77cd6bd 100644
--- a/nova/api/openstack/compute/contrib/extended_availability_zone.py
+++ b/nova/api/openstack/compute/contrib/extended_availability_zone.py
@@ -20,7 +20,7 @@
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova.api.openstack import xmlutil
-from nova import availability_zones
+from nova import availability_zones as avail_zone
authorize = extensions.soft_extension_authorizer('compute',
'extended_availability_zone')
@@ -29,8 +29,13 @@ authorize = extensions.soft_extension_authorizer('compute',
class ExtendedAZController(wsgi.Controller):
def _extend_server(self, context, server, instance):
key = "%s:availability_zone" % Extended_availability_zone.alias
- server[key] = availability_zones.get_instance_availability_zone(
- context, instance)
+ az = avail_zone.get_instance_availability_zone(context, instance)
+ if not az and instance.get('availability_zone'):
+ # Likely hasn't reached a viable compute node yet so give back the
+ # desired availability_zone that *may* exist in the instance
+ # record itself.
+ az = instance['availability_zone']
+ server[key] = az
@wsgi.extends
def show(self, req, resp_obj, id):
diff --git a/nova/api/openstack/compute/contrib/extended_floating_ips.py b/nova/api/openstack/compute/contrib/extended_floating_ips.py
index 06f1fa903..44b84bad9 100644
--- a/nova/api/openstack/compute/contrib/extended_floating_ips.py
+++ b/nova/api/openstack/compute/contrib/extended_floating_ips.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
from nova.api.openstack import extensions
diff --git a/nova/api/openstack/compute/contrib/extended_ips.py b/nova/api/openstack/compute/contrib/extended_ips.py
index ac75293a6..20356c08d 100644
--- a/nova/api/openstack/compute/contrib/extended_ips.py
+++ b/nova/api/openstack/compute/contrib/extended_ips.py
@@ -94,7 +94,7 @@ def make_server(elem):
class ExtendedIpsServerTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('server', selector='server')
- elem = xmlutil.SubTemplateElement(root, 'server', selector='servers')
+ xmlutil.SubTemplateElement(root, 'server', selector='servers')
make_server(root)
return xmlutil.SlaveTemplate(root, 1, nsmap={
Extended_ips.alias: Extended_ips.namespace})
diff --git a/nova/api/openstack/compute/contrib/fixed_ips.py b/nova/api/openstack/compute/contrib/fixed_ips.py
index b474b685d..532282a2b 100644
--- a/nova/api/openstack/compute/contrib/fixed_ips.py
+++ b/nova/api/openstack/compute/contrib/fixed_ips.py
@@ -69,8 +69,7 @@ class FixedIPController(object):
fixed_ip = db.fixed_ip_get_by_address(context, address)
db.fixed_ip_update(context, fixed_ip['address'],
{'reserved': reserved})
- except (exception.FixedIpNotFoundForAddress,
- exception.FixedIpInvalid) as ex:
+ except (exception.FixedIpNotFoundForAddress, exception.FixedIpInvalid):
msg = _("Fixed IP %s not found") % address
raise webob.exc.HTTPNotFound(explanation=msg)
diff --git a/nova/api/openstack/compute/contrib/flavorextraspecs.py b/nova/api/openstack/compute/contrib/flavorextraspecs.py
index 6e33d3603..00e7d7233 100644
--- a/nova/api/openstack/compute/contrib/flavorextraspecs.py
+++ b/nova/api/openstack/compute/contrib/flavorextraspecs.py
@@ -104,7 +104,7 @@ class FlavorExtraSpecsController(object):
extra_spec = db.instance_type_extra_specs_get_item(context,
flavor_id, id)
return extra_spec
- except exception.InstanceTypeExtraSpecsNotFound as e:
+ except exception.InstanceTypeExtraSpecsNotFound:
raise exc.HTTPNotFound()
def delete(self, req, flavor_id, id):
diff --git a/nova/api/openstack/compute/contrib/flavormanage.py b/nova/api/openstack/compute/contrib/flavormanage.py
index 43d5d2110..602e82c36 100644
--- a/nova/api/openstack/compute/contrib/flavormanage.py
+++ b/nova/api/openstack/compute/contrib/flavormanage.py
@@ -10,7 +10,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
import webob
@@ -76,6 +76,8 @@ class FlavorManageController(wsgi.Controller):
except (exception.InstanceTypeExists,
exception.InstanceTypeIdExists) as err:
raise webob.exc.HTTPConflict(explanation=err.format_message())
+ except exception.InvalidInput as exc:
+ raise webob.exc.HTTPBadRequest(explanation=exc.format_message())
return self._view_builder.show(req, flavor)
diff --git a/nova/api/openstack/compute/contrib/floating_ip_dns.py b/nova/api/openstack/compute/contrib/floating_ip_dns.py
index ecaa8e7b9..1d6a8b812 100644
--- a/nova/api/openstack/compute/contrib/floating_ip_dns.py
+++ b/nova/api/openstack/compute/contrib/floating_ip_dns.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
import urllib
diff --git a/nova/api/openstack/compute/contrib/floating_ip_pools.py b/nova/api/openstack/compute/contrib/floating_ip_pools.py
index e792ce433..aefe65d26 100644
--- a/nova/api/openstack/compute/contrib/floating_ip_pools.py
+++ b/nova/api/openstack/compute/contrib/floating_ip_pools.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
diff --git a/nova/api/openstack/compute/contrib/floating_ips.py b/nova/api/openstack/compute/contrib/floating_ips.py
index 284a211cd..807201e7e 100644
--- a/nova/api/openstack/compute/contrib/floating_ips.py
+++ b/nova/api/openstack/compute/contrib/floating_ips.py
@@ -15,7 +15,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
import webob
diff --git a/nova/api/openstack/compute/contrib/multiple_create.py b/nova/api/openstack/compute/contrib/multiple_create.py
index fd450b6d8..ee3dcf4b4 100644
--- a/nova/api/openstack/compute/contrib/multiple_create.py
+++ b/nova/api/openstack/compute/contrib/multiple_create.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
from nova.api.openstack import extensions
diff --git a/nova/api/openstack/compute/contrib/scheduler_hints.py b/nova/api/openstack/compute/contrib/scheduler_hints.py
index 0775307f4..0a5136205 100644
--- a/nova/api/openstack/compute/contrib/scheduler_hints.py
+++ b/nova/api/openstack/compute/contrib/scheduler_hints.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
import webob.exc
diff --git a/nova/api/openstack/compute/contrib/security_groups.py b/nova/api/openstack/compute/contrib/security_groups.py
index b34a77cab..e2862be4e 100644
--- a/nova/api/openstack/compute/contrib/security_groups.py
+++ b/nova/api/openstack/compute/contrib/security_groups.py
@@ -16,6 +16,7 @@
"""The security groups extension."""
+import contextlib
import json
import webob
from webob import exc
@@ -177,6 +178,25 @@ class SecurityGroupRulesXMLDeserializer(wsgi.MetadataXMLDeserializer):
return sg_rule
+@contextlib.contextmanager
+def translate_exceptions():
+ """Translate nova exceptions to http exceptions."""
+ try:
+ yield
+ except exception.Invalid as exp:
+ msg = exp.format_message()
+ raise exc.HTTPBadRequest(explanation=msg)
+ except exception.SecurityGroupNotFound as exp:
+ msg = exp.format_message()
+ raise exc.HTTPNotFound(explanation=msg)
+ except exception.InstanceNotFound as exp:
+ msg = exp.format_message()
+ raise exc.HTTPNotFound(explanation=msg)
+ except exception.SecurityGroupLimitExceeded as exp:
+ msg = exp.format_message()
+ raise exc.HTTPRequestEntityTooLarge(explanation=msg)
+
+
class SecurityGroupControllerBase(object):
"""Base class for Security Group controllers."""
@@ -196,8 +216,9 @@ class SecurityGroupControllerBase(object):
sg_rule['group'] = {}
sg_rule['ip_range'] = {}
if rule['group_id']:
- source_group = self.security_group_api.get(context,
- id=rule['group_id'])
+ with translate_exceptions():
+ source_group = self.security_group_api.get(context,
+ id=rule['group_id'])
sg_rule['group'] = {'name': source_group.get('name'),
'tenant_id': source_group.get('project_id')}
else:
@@ -233,10 +254,10 @@ class SecurityGroupController(SecurityGroupControllerBase):
"""Return data about the given security group."""
context = _authorize_context(req)
- id = self.security_group_api.validate_id(id)
-
- security_group = self.security_group_api.get(context, None, id,
- map_exception=True)
+ with translate_exceptions():
+ id = self.security_group_api.validate_id(id)
+ security_group = self.security_group_api.get(context, None, id,
+ map_exception=True)
return {'security_group': self._format_security_group(context,
security_group)}
@@ -245,12 +266,11 @@ class SecurityGroupController(SecurityGroupControllerBase):
"""Delete a security group."""
context = _authorize_context(req)
- id = self.security_group_api.validate_id(id)
-
- security_group = self.security_group_api.get(context, None, id,
- map_exception=True)
-
- self.security_group_api.destroy(context, security_group)
+ with translate_exceptions():
+ id = self.security_group_api.validate_id(id)
+ security_group = self.security_group_api.get(context, None, id,
+ map_exception=True)
+ self.security_group_api.destroy(context, security_group)
return webob.Response(status_int=202)
@@ -262,9 +282,11 @@ class SecurityGroupController(SecurityGroupControllerBase):
search_opts = {}
search_opts.update(req.GET)
- raw_groups = self.security_group_api.list(context,
- project=context.project_id,
- search_opts=search_opts)
+ with translate_exceptions():
+ project_id = context.project_id
+ raw_groups = self.security_group_api.list(context,
+ project=project_id,
+ search_opts=search_opts)
limited_list = common.limited(raw_groups, req)
result = [self._format_security_group(context, group)
@@ -285,16 +307,12 @@ class SecurityGroupController(SecurityGroupControllerBase):
group_name = security_group.get('name', None)
group_description = security_group.get('description', None)
- self.security_group_api.validate_property(group_name, 'name', None)
- self.security_group_api.validate_property(group_description,
- 'description', None)
-
- try:
+ with translate_exceptions():
+ self.security_group_api.validate_property(group_name, 'name', None)
+ self.security_group_api.validate_property(group_description,
+ 'description', None)
group_ref = self.security_group_api.create_security_group(
context, group_name, group_description)
- except exception.SecurityGroupLimitExceeded as err:
- raise exc.HTTPRequestEntityTooLarge(
- explanation=err.format_message())
return {'security_group': self._format_security_group(context,
group_ref)}
@@ -304,21 +322,21 @@ class SecurityGroupController(SecurityGroupControllerBase):
"""Update a security group."""
context = _authorize_context(req)
- id = self.security_group_api.validate_id(id)
+ with translate_exceptions():
+ id = self.security_group_api.validate_id(id)
+ security_group = self.security_group_api.get(context, None, id,
+ map_exception=True)
- security_group = self.security_group_api.get(context, None, id,
- map_exception=True)
security_group_data = self._from_body(body, 'security_group')
-
group_name = security_group_data.get('name', None)
group_description = security_group_data.get('description', None)
- self.security_group_api.validate_property(group_name, 'name', None)
- self.security_group_api.validate_property(group_description,
- 'description', None)
-
- group_ref = self.security_group_api.update_security_group(
- context, security_group, group_name, group_description)
+ with translate_exceptions():
+ self.security_group_api.validate_property(group_name, 'name', None)
+ self.security_group_api.validate_property(group_description,
+ 'description', None)
+ group_ref = self.security_group_api.update_security_group(
+ context, security_group, group_name, group_description)
return {'security_group': self._format_security_group(context,
group_ref)}
@@ -333,11 +351,12 @@ class SecurityGroupRulesController(SecurityGroupControllerBase):
sg_rule = self._from_body(body, 'security_group_rule')
- parent_group_id = self.security_group_api.validate_id(
- sg_rule.get('parent_group_id', None))
-
- security_group = self.security_group_api.get(context, None,
- parent_group_id, map_exception=True)
+ with translate_exceptions():
+ parent_group_id = self.security_group_api.validate_id(
+ sg_rule.get('parent_group_id', None))
+ security_group = self.security_group_api.get(context, None,
+ parent_group_id,
+ map_exception=True)
try:
new_rule = self._rule_args_to_dict(context,
to_port=sg_rule.get('to_port'),
@@ -360,13 +379,10 @@ class SecurityGroupRulesController(SecurityGroupControllerBase):
msg = _("Bad prefix for network in cidr %s") % new_rule['cidr']
raise exc.HTTPBadRequest(explanation=msg)
- try:
+ with translate_exceptions():
security_group_rule = (
self.security_group_api.create_security_group_rule(
context, security_group, new_rule))
- except exception.SecurityGroupLimitExceeded as err:
- raise exc.HTTPRequestEntityTooLarge(
- explanation=err.format_message())
return {"security_group_rule": self._format_security_group_rule(
context,
@@ -390,17 +406,15 @@ class SecurityGroupRulesController(SecurityGroupControllerBase):
def delete(self, req, id):
context = _authorize_context(req)
- id = self.security_group_api.validate_id(id)
-
- rule = self.security_group_api.get_rule(context, id)
-
- group_id = rule['parent_group_id']
-
- security_group = self.security_group_api.get(context, None, group_id,
- map_exception=True)
-
- self.security_group_api.remove_rules(context, security_group,
- [rule['id']])
+ with translate_exceptions():
+ id = self.security_group_api.validate_id(id)
+ rule = self.security_group_api.get_rule(context, id)
+ group_id = rule['parent_group_id']
+ security_group = self.security_group_api.get(context, None,
+ group_id,
+ map_exception=True)
+ self.security_group_api.remove_rules(context, security_group,
+ [rule['id']])
return webob.Response(status_int=202)
@@ -414,13 +428,11 @@ class ServerSecurityGroupController(SecurityGroupControllerBase):
self.security_group_api.ensure_default(context)
- try:
+ with translate_exceptions():
instance = self.compute_api.get(context, server_id)
- except exception.InstanceNotFound as exp:
- raise exc.HTTPNotFound(explanation=exp.format_message())
+ groups = self.security_group_api.get_instance_security_groups(
+ context, instance['uuid'], True)
- groups = self.security_group_api.get_instance_security_groups(
- context, instance['uuid'], True)
result = [self._format_security_group(context, group)
for group in groups]
@@ -455,15 +467,9 @@ class SecurityGroupActionController(wsgi.Controller):
return group_name
def _invoke(self, method, context, id, group_name):
- try:
+ with translate_exceptions():
instance = self.compute_api.get(context, id)
method(context, instance, group_name)
- except exception.SecurityGroupNotFound as exp:
- raise exc.HTTPNotFound(explanation=exp.format_message())
- except exception.InstanceNotFound as exp:
- raise exc.HTTPNotFound(explanation=exp.format_message())
- except exception.Invalid as exp:
- raise exc.HTTPBadRequest(explanation=exp.format_message())
return webob.Response(status_int=202)
@@ -642,15 +648,15 @@ class Security_groups(extensions.ExtensionDescriptor):
class NativeSecurityGroupExceptions(object):
@staticmethod
def raise_invalid_property(msg):
- raise exc.HTTPBadRequest(explanation=msg)
+ raise exception.Invalid(msg)
@staticmethod
def raise_group_already_exists(msg):
- raise exc.HTTPBadRequest(explanation=msg)
+ raise exception.Invalid(msg)
@staticmethod
def raise_invalid_group(msg):
- raise exc.HTTPBadRequest(explanation=msg)
+ raise exception.Invalid(msg)
@staticmethod
def raise_invalid_cidr(cidr, decoding_exception=None):
@@ -662,7 +668,7 @@ class NativeSecurityGroupExceptions(object):
@staticmethod
def raise_not_found(msg):
- raise exc.HTTPNotFound(explanation=msg)
+ raise exception.SecurityGroupNotFound(msg)
class NativeNovaSecurityGroupAPI(NativeSecurityGroupExceptions,
diff --git a/nova/api/openstack/compute/contrib/server_password.py b/nova/api/openstack/compute/contrib/server_password.py
index 14ea91ef2..2884e4040 100644
--- a/nova/api/openstack/compute/contrib/server_password.py
+++ b/nova/api/openstack/compute/contrib/server_password.py
@@ -39,7 +39,7 @@ class ServerPasswordTemplate(xmlutil.TemplateBuilder):
class ServerPasswordController(object):
- """The flavor access API controller for the OpenStack API."""
+ """The Server Password API controller for the OpenStack API."""
def __init__(self):
self.compute_api = compute.API()
diff --git a/nova/api/openstack/compute/contrib/server_start_stop.py b/nova/api/openstack/compute/contrib/server_start_stop.py
index 2803cd04b..1734b2cfa 100644
--- a/nova/api/openstack/compute/contrib/server_start_stop.py
+++ b/nova/api/openstack/compute/contrib/server_start_stop.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
import webob
diff --git a/nova/api/openstack/compute/contrib/services.py b/nova/api/openstack/compute/contrib/services.py
index 3a637010a..76eec7ce4 100644
--- a/nova/api/openstack/compute/contrib/services.py
+++ b/nova/api/openstack/compute/contrib/services.py
@@ -183,8 +183,7 @@ class ServiceController(object):
raise webob.exc.HTTPUnprocessableEntity(detail=msg)
try:
- svc = self.host_api.service_update(context, host, binary,
- status_detail)
+ self.host_api.service_update(context, host, binary, status_detail)
except exception.ServiceNotFound:
raise webob.exc.HTTPNotFound(_("Unknown service"))
diff --git a/nova/api/openstack/compute/contrib/used_limits.py b/nova/api/openstack/compute/contrib/used_limits.py
index 5a90a9def..a0fab1594 100644
--- a/nova/api/openstack/compute/contrib/used_limits.py
+++ b/nova/api/openstack/compute/contrib/used_limits.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
diff --git a/nova/api/openstack/compute/contrib/used_limits_for_admin.py b/nova/api/openstack/compute/contrib/used_limits_for_admin.py
index a6ec9c002..d2a8af0d7 100644
--- a/nova/api/openstack/compute/contrib/used_limits_for_admin.py
+++ b/nova/api/openstack/compute/contrib/used_limits_for_admin.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
from nova.api.openstack import extensions
diff --git a/nova/api/openstack/compute/contrib/user_data.py b/nova/api/openstack/compute/contrib/user_data.py
index 3e69a65cc..ef5aa6d5a 100644
--- a/nova/api/openstack/compute/contrib/user_data.py
+++ b/nova/api/openstack/compute/contrib/user_data.py
@@ -12,7 +12,7 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
-# under the License
+# under the License.
from nova.api.openstack import extensions
diff --git a/nova/api/openstack/compute/extensions.py b/nova/api/openstack/compute/extensions.py
index 5e6633f1d..ac8c04aa0 100644
--- a/nova/api/openstack/compute/extensions.py
+++ b/nova/api/openstack/compute/extensions.py
@@ -19,7 +19,6 @@ from oslo.config import cfg
from nova.api.openstack import extensions as base_extensions
from nova.openstack.common import log as logging
-from nova.openstack.common.plugin import pluginmanager
ext_opts = [
cfg.MultiStrOpt('osapi_compute_extension',
@@ -38,10 +37,6 @@ class ExtensionManager(base_extensions.ExtensionManager):
def __init__(self):
LOG.audit(_('Initializing extension manager.'))
self.cls_list = CONF.osapi_compute_extension
- self.PluginManager = pluginmanager.PluginManager('nova',
- 'compute-extensions')
- self.PluginManager.load_plugins()
- self.cls_list.append(self.PluginManager.plugin_extension_factory)
self.extensions = {}
self.sorted_ext_list = []
self._load_extensions()
diff --git a/nova/api/openstack/compute/flavors.py b/nova/api/openstack/compute/flavors.py
index 4da130e9b..c6303646a 100644
--- a/nova/api/openstack/compute/flavors.py
+++ b/nova/api/openstack/compute/flavors.py
@@ -24,6 +24,7 @@ from nova.api.openstack import xmlutil
from nova.compute import flavors
from nova import exception
from nova.openstack.common import strutils
+from nova import utils
def make_flavor(elem, detailed=False):
@@ -98,7 +99,7 @@ class Controller(wsgi.Controller):
if is_public is None:
# preserve default value of showing only public flavors
return True
- elif is_public == 'none':
+ elif utils.is_none_string(is_public):
return None
else:
try:
diff --git a/nova/api/openstack/compute/limits.py b/nova/api/openstack/compute/limits.py
index 07e791306..107f40436 100644
--- a/nova/api/openstack/compute/limits.py
+++ b/nova/api/openstack/compute/limits.py
@@ -33,19 +33,13 @@ from nova.api.openstack import xmlutil
from nova.openstack.common import importutils
from nova.openstack.common import jsonutils
from nova import quota
+from nova import utils
from nova import wsgi as base_wsgi
QUOTAS = quota.QUOTAS
-# Convenience constants for the limits dictionary passed to Limiter().
-PER_SECOND = 1
-PER_MINUTE = 60
-PER_HOUR = 60 * 60
-PER_DAY = 60 * 60 * 24
-
-
limits_nsmap = {None: xmlutil.XMLNS_COMMON_V10, 'atom': xmlutil.XMLNS_ATOM}
@@ -122,14 +116,7 @@ class Limit(object):
Stores information about a limit for HTTP requests.
"""
- UNITS = {
- 1: "SECOND",
- 60: "MINUTE",
- 60 * 60: "HOUR",
- 60 * 60 * 24: "DAY",
- }
-
- UNIT_MAP = dict([(v, k) for k, v in UNITS.items()])
+ UNITS = dict([(v, k) for k, v in utils.TIME_UNITS.items()])
def __init__(self, verb, uri, regex, value, unit):
"""
@@ -223,12 +210,13 @@ class Limit(object):
# a regular-expression to match, value and unit of measure (PER_DAY, etc.)
DEFAULT_LIMITS = [
- Limit("POST", "*", ".*", 10, PER_MINUTE),
- Limit("POST", "*/servers", "^/servers", 50, PER_DAY),
- Limit("PUT", "*", ".*", 10, PER_MINUTE),
- Limit("GET", "*changes-since*", ".*changes-since.*", 3, PER_MINUTE),
- Limit("DELETE", "*", ".*", 100, PER_MINUTE),
- Limit("GET", "*/os-fping", "^/os-fping", 12, PER_HOUR),
+ Limit("POST", "*", ".*", 10, utils.TIME_UNITS['MINUTE']),
+ Limit("POST", "*/servers", "^/servers", 50, utils.TIME_UNITS['DAY']),
+ Limit("PUT", "*", ".*", 10, utils.TIME_UNITS['MINUTE']),
+ Limit("GET", "*changes-since*", ".*changes-since.*", 3,
+ utils.TIME_UNITS['MINUTE']),
+ Limit("DELETE", "*", ".*", 100, utils.TIME_UNITS['MINUTE']),
+ Limit("GET", "*/os-fping", "^/os-fping", 12, utils.TIME_UNITS['HOUR']),
]
@@ -390,9 +378,9 @@ class Limiter(object):
# Convert unit
unit = unit.upper()
- if unit not in Limit.UNIT_MAP:
+ if unit not in utils.TIME_UNITS:
raise ValueError("Invalid units specified")
- unit = Limit.UNIT_MAP[unit]
+ unit = utils.TIME_UNITS[unit]
# Build a limit
result.append(Limit(verb, uri, regex, value, unit))
diff --git a/nova/api/openstack/compute/plugins/v3/agents.py b/nova/api/openstack/compute/plugins/v3/agents.py
new file mode 100644
index 000000000..02e752dac
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/agents.py
@@ -0,0 +1,168 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+import webob.exc
+
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova import db
+from nova import exception
+
+
+authorize = extensions.extension_authorizer('compute', 'agents')
+
+
+class AgentsIndexTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('agents')
+ elem = xmlutil.SubTemplateElement(root, 'agent', selector='agents')
+ elem.set('hypervisor')
+ elem.set('os')
+ elem.set('architecture')
+ elem.set('version')
+ elem.set('md5hash')
+ elem.set('agent_id')
+ elem.set('url')
+
+ return xmlutil.MasterTemplate(root, 1)
+
+
+class AgentController(object):
+ """
+ The agent is talking about guest agent.The host can use this for
+ things like accessing files on the disk, configuring networking,
+ or running other applications/scripts in the guest while it is
+ running. Typically this uses some hypervisor-specific transport
+ to avoid being dependent on a working network configuration.
+ Xen, VMware, and VirtualBox have guest agents,although the Xen
+ driver is the only one with an implementation for managing them
+ in openstack. KVM doesn't really have a concept of a guest agent
+ (although one could be written).
+
+ You can find the design of agent update in this link:
+ http://wiki.openstack.org/AgentUpdate
+ and find the code in nova.virt.xenapi.vmops.VMOps._boot_new_instance.
+ In this design We need update agent in guest from host, so we need
+ some interfaces to update the agent info in host.
+
+ You can find more information about the design of the GuestAgent in
+ the following link:
+ http://wiki.openstack.org/GuestAgent
+ http://wiki.openstack.org/GuestAgentXenStoreCommunication
+ """
+ @wsgi.serializers(xml=AgentsIndexTemplate)
+ def index(self, req):
+ """
+ Return a list of all agent builds. Filter by hypervisor.
+ """
+ context = req.environ['nova.context']
+ authorize(context)
+ hypervisor = None
+ agents = []
+ if 'hypervisor' in req.GET:
+ hypervisor = req.GET['hypervisor']
+
+ for agent_build in db.agent_build_get_all(context, hypervisor):
+ agents.append({'hypervisor': agent_build.hypervisor,
+ 'os': agent_build.os,
+ 'architecture': agent_build.architecture,
+ 'version': agent_build.version,
+ 'md5hash': agent_build.md5hash,
+ 'agent_id': agent_build.id,
+ 'url': agent_build.url})
+
+ return {'agents': agents}
+
+ def update(self, req, id, body):
+ """Update an existing agent build."""
+ context = req.environ['nova.context']
+ authorize(context)
+
+ try:
+ para = body['para']
+ url = para['url']
+ md5hash = para['md5hash']
+ version = para['version']
+ except (TypeError, KeyError):
+ raise webob.exc.HTTPUnprocessableEntity()
+
+ try:
+ db.agent_build_update(context, id,
+ {'version': version,
+ 'url': url,
+ 'md5hash': md5hash})
+ except exception.AgentBuildNotFound as ex:
+ raise webob.exc.HTTPNotFound(explanation=ex.format_message())
+
+ return {"agent": {'agent_id': id, 'version': version,
+ 'url': url, 'md5hash': md5hash}}
+
+ def delete(self, req, id):
+ """Deletes an existing agent build."""
+ context = req.environ['nova.context']
+ authorize(context)
+
+ try:
+ db.agent_build_destroy(context, id)
+ except exception.AgentBuildNotFound as ex:
+ raise webob.exc.HTTPNotFound(explanation=ex.format_message())
+
+ def create(self, req, body):
+ """Creates a new agent build."""
+ context = req.environ['nova.context']
+ authorize(context)
+
+ try:
+ agent = body['agent']
+ hypervisor = agent['hypervisor']
+ os = agent['os']
+ architecture = agent['architecture']
+ version = agent['version']
+ url = agent['url']
+ md5hash = agent['md5hash']
+ except (TypeError, KeyError):
+ raise webob.exc.HTTPUnprocessableEntity()
+
+ try:
+ agent_build_ref = db.agent_build_create(context,
+ {'hypervisor': hypervisor,
+ 'os': os,
+ 'architecture': architecture,
+ 'version': version,
+ 'url': url,
+ 'md5hash': md5hash})
+ agent['agent_id'] = agent_build_ref.id
+ except Exception as ex:
+ raise webob.exc.HTTPServerError(str(ex))
+ return {'agent': agent}
+
+
+class Agents(extensions.ExtensionDescriptor):
+ """Agents support."""
+
+ name = "Agents"
+ alias = "os-agents"
+ namespace = "http://docs.openstack.org/compute/ext/agents/api/v2"
+ updated = "2012-10-28T00:00:00-00:00"
+
+ def get_resources(self):
+ resources = []
+ resource = extensions.ResourceExtension('os-agents',
+ AgentController())
+ resources.append(resource)
+ return resources
diff --git a/nova/api/openstack/compute/plugins/v3/cells.py b/nova/api/openstack/compute/plugins/v3/cells.py
new file mode 100644
index 000000000..e07792018
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/cells.py
@@ -0,0 +1,347 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011-2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""The cells extension."""
+
+from oslo.config import cfg
+from webob import exc
+
+from nova.api.openstack import common
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova.cells import rpcapi as cells_rpcapi
+from nova.compute import api as compute
+from nova import db
+from nova import exception
+from nova.openstack.common import log as logging
+from nova.openstack.common import timeutils
+
+
+LOG = logging.getLogger(__name__)
+CONF = cfg.CONF
+CONF.import_opt('name', 'nova.cells.opts', group='cells')
+CONF.import_opt('capabilities', 'nova.cells.opts', group='cells')
+
+ALIAS = "os-cells"
+authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
+
+
+def make_cell(elem):
+ elem.set('name')
+ elem.set('username')
+ elem.set('type')
+ elem.set('rpc_host')
+ elem.set('rpc_port')
+
+ caps = xmlutil.SubTemplateElement(elem, 'capabilities',
+ selector='capabilities')
+ cap = xmlutil.SubTemplateElement(caps, xmlutil.Selector(0),
+ selector=xmlutil.get_items)
+ cap.text = 1
+ make_capacity(elem)
+
+
+def make_capacity(cell):
+
+ def get_units_by_mb(capacity_info):
+ return capacity_info['units_by_mb'].items()
+
+ capacity = xmlutil.SubTemplateElement(cell, 'capacities',
+ selector='capacities')
+
+ ram_free = xmlutil.SubTemplateElement(capacity, 'ram_free',
+ selector='ram_free')
+ ram_free.set('total_mb', 'total_mb')
+ unit_by_mb = xmlutil.SubTemplateElement(ram_free, 'unit_by_mb',
+ selector=get_units_by_mb)
+ unit_by_mb.set('mb', 0)
+ unit_by_mb.set('unit', 1)
+
+ disk_free = xmlutil.SubTemplateElement(capacity, 'disk_free',
+ selector='disk_free')
+ disk_free.set('total_mb', 'total_mb')
+ unit_by_mb = xmlutil.SubTemplateElement(disk_free, 'unit_by_mb',
+ selector=get_units_by_mb)
+ unit_by_mb.set('mb', 0)
+ unit_by_mb.set('unit', 1)
+
+cell_nsmap = {None: wsgi.XMLNS_V10}
+
+
+class CellTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('cell', selector='cell')
+ make_cell(root)
+ return xmlutil.MasterTemplate(root, 1, nsmap=cell_nsmap)
+
+
+class CellsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('cells')
+ elem = xmlutil.SubTemplateElement(root, 'cell', selector='cells')
+ make_cell(elem)
+ return xmlutil.MasterTemplate(root, 1, nsmap=cell_nsmap)
+
+
+class CellDeserializer(wsgi.XMLDeserializer):
+ """Deserializer to handle xml-formatted cell create requests."""
+
+ def _extract_capabilities(self, cap_node):
+ caps = {}
+ for cap in cap_node.childNodes:
+ cap_name = cap.tagName
+ caps[cap_name] = self.extract_text(cap)
+ return caps
+
+ def _extract_cell(self, node):
+ cell = {}
+ cell_node = self.find_first_child_named(node, 'cell')
+
+ extract_fns = {'capabilities': self._extract_capabilities}
+
+ for child in cell_node.childNodes:
+ name = child.tagName
+ extract_fn = extract_fns.get(name, self.extract_text)
+ cell[name] = extract_fn(child)
+ return cell
+
+ def default(self, string):
+ """Deserialize an xml-formatted cell create request."""
+ node = xmlutil.safe_minidom_parse_string(string)
+
+ return {'body': {'cell': self._extract_cell(node)}}
+
+
+def _filter_keys(item, keys):
+ """
+ Filters all model attributes except for keys
+ item is a dict
+
+ """
+ return dict((k, v) for k, v in item.iteritems() if k in keys)
+
+
+def _scrub_cell(cell, detail=False):
+ keys = ['name', 'username', 'rpc_host', 'rpc_port']
+ if detail:
+ keys.append('capabilities')
+
+ cell_info = _filter_keys(cell, keys)
+ cell_info['type'] = 'parent' if cell['is_parent'] else 'child'
+ return cell_info
+
+
+class CellsController(object):
+ """Controller for Cell resources."""
+
+ def __init__(self):
+ self.compute_api = compute.API()
+ self.cells_rpcapi = cells_rpcapi.CellsAPI()
+
+ def _get_cells(self, ctxt, req, detail=False):
+ """Return all cells."""
+ # Ask the CellsManager for the most recent data
+ items = self.cells_rpcapi.get_cell_info_for_neighbors(ctxt)
+ items = common.limited(items, req)
+ items = [_scrub_cell(item, detail=detail) for item in items]
+ return dict(cells=items)
+
+ @wsgi.serializers(xml=CellsTemplate)
+ def index(self, req):
+ """Return all cells in brief."""
+ ctxt = req.environ['nova.context']
+ authorize(ctxt)
+ return self._get_cells(ctxt, req)
+
+ @wsgi.serializers(xml=CellsTemplate)
+ def detail(self, req):
+ """Return all cells in detail."""
+ ctxt = req.environ['nova.context']
+ authorize(ctxt)
+ return self._get_cells(ctxt, req, detail=True)
+
+ @wsgi.serializers(xml=CellTemplate)
+ def info(self, req):
+ """Return name and capabilities for this cell."""
+ context = req.environ['nova.context']
+ authorize(context)
+ cell_capabs = {}
+ my_caps = CONF.cells.capabilities
+ for cap in my_caps:
+ key, value = cap.split('=')
+ cell_capabs[key] = value
+ cell = {'name': CONF.cells.name,
+ 'type': 'self',
+ 'rpc_host': None,
+ 'rpc_port': 0,
+ 'username': None,
+ 'capabilities': cell_capabs}
+ return dict(cell=cell)
+
+ @wsgi.serializers(xml=CellTemplate)
+ def capacities(self, req, id=None):
+ """Return capacities for a given cell or all cells."""
+ # TODO(kaushikc): return capacities as a part of cell info and
+ # cells detail calls in v3, along with capabilities
+ context = req.environ['nova.context']
+ authorize(context)
+ try:
+ capacities = self.cells_rpcapi.get_capacities(context,
+ cell_name=id)
+ except exception.CellNotFound:
+ msg = (_("Cell %(id)s not found.") % {'id': id})
+ raise exc.HTTPNotFound(explanation=msg)
+
+ return dict(cell={"capacities": capacities})
+
+ @wsgi.serializers(xml=CellTemplate)
+ def show(self, req, id):
+ """Return data about the given cell name. 'id' is a cell name."""
+ context = req.environ['nova.context']
+ authorize(context)
+ try:
+ cell = db.cell_get(context, id)
+ except exception.CellNotFound:
+ raise exc.HTTPNotFound()
+ return dict(cell=_scrub_cell(cell))
+
+ def delete(self, req, id):
+ """Delete a child or parent cell entry. 'id' is a cell name."""
+ context = req.environ['nova.context']
+ authorize(context)
+ num_deleted = db.cell_delete(context, id)
+ if num_deleted == 0:
+ raise exc.HTTPNotFound()
+ return {}
+
+ def _validate_cell_name(self, cell_name):
+ """Validate cell name is not empty and doesn't contain '!' or '.'."""
+ if not cell_name:
+ msg = _("Cell name cannot be empty")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+ if '!' in cell_name or '.' in cell_name:
+ msg = _("Cell name cannot contain '!' or '.'")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ def _validate_cell_type(self, cell_type):
+ """Validate cell_type is 'parent' or 'child'."""
+ if cell_type not in ['parent', 'child']:
+ msg = _("Cell type must be 'parent' or 'child'")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ def _convert_cell_type(self, cell):
+ """Convert cell['type'] to is_parent boolean."""
+ if 'type' in cell:
+ self._validate_cell_type(cell['type'])
+ cell['is_parent'] = cell['type'] == 'parent'
+ del cell['type']
+ else:
+ cell['is_parent'] = False
+
+ @wsgi.serializers(xml=CellTemplate)
+ @wsgi.deserializers(xml=CellDeserializer)
+ def create(self, req, body):
+ """Create a child cell entry."""
+ context = req.environ['nova.context']
+ authorize(context)
+ if 'cell' not in body:
+ msg = _("No cell information in request")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+ cell = body['cell']
+ if 'name' not in cell:
+ msg = _("No cell name in request")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+ self._validate_cell_name(cell['name'])
+ self._convert_cell_type(cell)
+ cell = db.cell_create(context, cell)
+ return dict(cell=_scrub_cell(cell))
+
+ @wsgi.serializers(xml=CellTemplate)
+ @wsgi.deserializers(xml=CellDeserializer)
+ def update(self, req, id, body):
+ """Update a child cell entry. 'id' is the cell name to update."""
+ context = req.environ['nova.context']
+ authorize(context)
+ if 'cell' not in body:
+ msg = _("No cell information in request")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+ cell = body['cell']
+ cell.pop('id', None)
+ if 'name' in cell:
+ self._validate_cell_name(cell['name'])
+ self._convert_cell_type(cell)
+ try:
+ cell = db.cell_update(context, id, cell)
+ except exception.CellNotFound:
+ raise exc.HTTPNotFound()
+ return dict(cell=_scrub_cell(cell))
+
+ def sync_instances(self, req, body):
+ """Tell all cells to sync instance info."""
+ context = req.environ['nova.context']
+ authorize(context)
+ project_id = body.pop('project_id', None)
+ deleted = body.pop('deleted', False)
+ updated_since = body.pop('updated_since', None)
+ if body:
+ msg = _("Only 'updated_since' and 'project_id' are understood.")
+ raise exc.HTTPBadRequest(explanation=msg)
+ if updated_since:
+ try:
+ timeutils.parse_isotime(updated_since)
+ except ValueError:
+ msg = _('Invalid changes-since value')
+ raise exc.HTTPBadRequest(explanation=msg)
+ self.cells_rpcapi.sync_instances(context, project_id=project_id,
+ updated_since=updated_since, deleted=deleted)
+
+
+class Cells(extensions.V3APIExtensionBase):
+ """Enables cells-related functionality such as adding neighbor cells,
+ listing neighbor cells, and getting the capabilities of the local cell.
+ """
+
+ name = "Cells"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/cells/api/v3"
+ version = 1
+
+ def get_resources(self):
+ coll_actions = {
+ 'detail': 'GET',
+ 'info': 'GET',
+ 'sync_instances': 'POST',
+ 'capacities': 'GET',
+ }
+ memb_actions = {
+ 'capacities': 'GET',
+ }
+
+ res = extensions.ResourceExtension(ALIAS, CellsController(),
+ collection_actions=coll_actions,
+ member_actions=memb_actions)
+ return [res]
+
+ def get_controller_extensions(self):
+ return []
diff --git a/nova/api/openstack/compute/plugins/v3/certificates.py b/nova/api/openstack/compute/plugins/v3/certificates.py
new file mode 100644
index 000000000..175780f9c
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/certificates.py
@@ -0,0 +1,97 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2012 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License
+
+import webob.exc
+
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+import nova.cert.rpcapi
+from nova import network
+
+ALIAS = "os-certificates"
+authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
+
+
+def make_certificate(elem):
+ elem.set('data')
+ elem.set('private_key')
+
+
+class CertificateTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('certificate',
+ selector='certificate')
+ make_certificate(root)
+ return xmlutil.MasterTemplate(root, 1)
+
+
+def _translate_certificate_view(certificate, private_key=None):
+ return {
+ 'data': certificate,
+ 'private_key': private_key,
+ }
+
+
+class CertificatesController(object):
+ """The x509 Certificates API controller for the OpenStack API."""
+
+ def __init__(self):
+ self.network_api = network.API()
+ self.cert_rpcapi = nova.cert.rpcapi.CertAPI()
+ super(CertificatesController, self).__init__()
+
+ @wsgi.serializers(xml=CertificateTemplate)
+ def show(self, req, id):
+ """Return certificate information."""
+ context = req.environ['nova.context']
+ authorize(context)
+ if id != 'root':
+ msg = _("Only root certificate can be retrieved.")
+ raise webob.exc.HTTPNotImplemented(explanation=msg)
+ cert = self.cert_rpcapi.fetch_ca(context,
+ project_id=context.project_id)
+ return {'certificate': _translate_certificate_view(cert)}
+
+ @wsgi.serializers(xml=CertificateTemplate)
+ def create(self, req, body=None):
+ """Create a certificate."""
+ context = req.environ['nova.context']
+ authorize(context)
+ pk, cert = self.cert_rpcapi.generate_x509_cert(context,
+ user_id=context.user_id, project_id=context.project_id)
+ context = req.environ['nova.context']
+ return {'certificate': _translate_certificate_view(cert, pk)}
+
+
+class Certificates(extensions.V3APIExtensionBase):
+ """Certificates support."""
+
+ name = "Certificates"
+ alias = ALIAS
+ namespace = ("http://docs.openstack.org/compute/ext/"
+ "certificates/api/v3")
+ version = 1
+
+ def get_resources(self):
+ resources = [
+ extensions.ResourceExtension('os-certificates',
+ CertificatesController(),
+ member_actions={})]
+ return resources
+
+ def get_controller_extensions(self):
+ return []
diff --git a/nova/api/openstack/compute/plugins/v3/evacuate.py b/nova/api/openstack/compute/plugins/v3/evacuate.py
new file mode 100644
index 000000000..86e90e03e
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/evacuate.py
@@ -0,0 +1,101 @@
+# Copyright 2013 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+from webob import exc
+
+from nova.api.openstack import common
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova import compute
+from nova import exception
+from nova.openstack.common import log as logging
+from nova.openstack.common import strutils
+from nova import utils
+
+LOG = logging.getLogger(__name__)
+ALIAS = "os-evacuate"
+authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
+
+
+class EvacuateController(wsgi.Controller):
+ def __init__(self, *args, **kwargs):
+ super(EvacuateController, self).__init__(*args, **kwargs)
+ self.compute_api = compute.API()
+
+ @wsgi.action('evacuate')
+ def _evacuate(self, req, id, body):
+ """
+ Permit admins to evacuate a server from a failed host
+ to a new one.
+ """
+ context = req.environ["nova.context"]
+ authorize(context)
+
+ try:
+ if len(body) != 1:
+ raise exc.HTTPBadRequest(_("Malformed request body"))
+
+ evacuate_body = body["evacuate"]
+ host = evacuate_body["host"]
+ on_shared_storage = strutils.bool_from_string(
+ evacuate_body["onSharedStorage"])
+
+ password = None
+ if 'adminPass' in evacuate_body:
+ # check that if requested to evacuate server on shared storage
+ # password not specified
+ if on_shared_storage:
+ msg = _("admin password can't be changed on existing disk")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ password = evacuate_body['adminPass']
+ elif not on_shared_storage:
+ password = utils.generate_password()
+
+ except (TypeError, KeyError):
+ msg = _("host and onSharedStorage must be specified.")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ try:
+ instance = self.compute_api.get(context, id)
+ self.compute_api.evacuate(context, instance, host,
+ on_shared_storage, password)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'evacuate')
+ except Exception as e:
+ msg = _("Error in evacuate, %s") % e
+ LOG.exception(msg, instance=instance)
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ if password:
+ return {'adminPass': password}
+
+
+class Evacuate(extensions.V3APIExtensionBase):
+ """Enables server evacuation."""
+
+ name = "Evacuate"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/evacuate/api/v3"
+ version = 1
+
+ def get_resources(self):
+ return []
+
+ def get_controller_extensions(self):
+ controller = EvacuateController()
+ extension = extensions.ControllerExtension(self, 'servers', controller)
+ return [extension]
diff --git a/nova/api/openstack/compute/plugins/v3/extension_info.py b/nova/api/openstack/compute/plugins/v3/extension_info.py
index 43b0551c7..c626f6104 100644
--- a/nova/api/openstack/compute/plugins/v3/extension_info.py
+++ b/nova/api/openstack/compute/plugins/v3/extension_info.py
@@ -19,6 +19,10 @@ import webob.exc
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova.api.openstack import xmlutil
+from nova.openstack.common import log as logging
+
+
+LOG = logging.getLogger(__name__)
def make_ext(elem):
@@ -64,11 +68,25 @@ class ExtensionInfoController(object):
ext_data['version'] = ext.version
return ext_data
+ def _get_extensions(self, context):
+ """Filter extensions list based on policy"""
+
+ discoverable_extensions = dict()
+ for alias, ext in self.extension_info.get_extensions().iteritems():
+ authorize = extensions.soft_extension_authorizer(
+ 'compute', 'v3:' + alias)
+ if authorize(context, action='discoverable'):
+ discoverable_extensions[alias] = ext
+ else:
+ LOG.debug(_("Filter out extension %s from discover list"), alias)
+ return discoverable_extensions
+
@wsgi.serializers(xml=ExtensionsTemplate)
def index(self, req):
+ context = req.environ['nova.context']
sorted_ext_list = sorted(
- self.extension_info.get_extensions().iteritems())
+ self._get_extensions(context).iteritems())
extensions = []
for _alias, ext in sorted_ext_list:
@@ -77,9 +95,10 @@ class ExtensionInfoController(object):
@wsgi.serializers(xml=ExtensionTemplate)
def show(self, req, id):
+ context = req.environ['nova.context']
try:
# NOTE(dprince): the extensions alias is used as the 'id' for show
- ext = self.extension_info.get_extensions()[id]
+ ext = self._get_extensions(context)[id]
except KeyError:
raise webob.exc.HTTPNotFound()
diff --git a/nova/api/openstack/compute/plugins/v3/fixed_ips.py b/nova/api/openstack/compute/plugins/v3/fixed_ips.py
index e98b830bd..5fa4ae3c2 100644
--- a/nova/api/openstack/compute/plugins/v3/fixed_ips.py
+++ b/nova/api/openstack/compute/plugins/v3/fixed_ips.py
@@ -28,6 +28,7 @@ authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
class FixedIPController(object):
+ @extensions.expected_errors(404)
def show(self, req, id):
"""Return data about the given fixed ip."""
context = req.environ['nova.context']
@@ -55,6 +56,7 @@ class FixedIPController(object):
return fixed_ip_info
+ @extensions.expected_errors((400, 404))
def action(self, req, id, body):
context = req.environ['nova.context']
authorize(context)
diff --git a/nova/api/openstack/compute/plugins/v3/flavor_access.py b/nova/api/openstack/compute/plugins/v3/flavor_access.py
new file mode 100644
index 000000000..459a4041b
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/flavor_access.py
@@ -0,0 +1,220 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""The flavor access extension."""
+
+import webob
+
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova.compute import flavors
+from nova import exception
+
+ALIAS = 'os-flavor-access'
+authorize = extensions.soft_extension_authorizer('compute', 'v3:' + ALIAS)
+
+
+def make_flavor(elem):
+ elem.set('{%s}is_public' % FlavorAccess.namespace,
+ '%s:is_public' % FlavorAccess.alias)
+
+
+def make_flavor_access(elem):
+ elem.set('flavor_id')
+ elem.set('tenant_id')
+
+
+class FlavorTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavor', selector='flavor')
+ make_flavor(root)
+ alias = FlavorAccess.alias
+ namespace = FlavorAccess.namespace
+ return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace})
+
+
+class FlavorsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavors')
+ elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors')
+ make_flavor(elem)
+ alias = FlavorAccess.alias
+ namespace = FlavorAccess.namespace
+ return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace})
+
+
+class FlavorAccessTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavor_access')
+ elem = xmlutil.SubTemplateElement(root, 'access',
+ selector='flavor_access')
+ make_flavor_access(elem)
+ return xmlutil.MasterTemplate(root, 1)
+
+
+def _marshall_flavor_access(flavor_id):
+ rval = []
+ try:
+ access_list = flavors.\
+ get_flavor_access_by_flavor_id(flavor_id)
+ except exception.FlavorNotFound:
+ explanation = _("Flavor not found.")
+ raise webob.exc.HTTPNotFound(explanation=explanation)
+
+ for access in access_list:
+ rval.append({'flavor_id': flavor_id,
+ 'tenant_id': access['project_id']})
+
+ return {'flavor_access': rval}
+
+
+class FlavorAccessController(object):
+ """The flavor access API controller for the OpenStack API."""
+
+ def __init__(self):
+ super(FlavorAccessController, self).__init__()
+
+ @wsgi.serializers(xml=FlavorAccessTemplate)
+ def index(self, req, flavor_id):
+ context = req.environ['nova.context']
+ authorize(context)
+
+ try:
+ flavor = flavors.get_flavor_by_flavor_id(flavor_id)
+ except exception.FlavorNotFound:
+ explanation = _("Flavor not found.")
+ raise webob.exc.HTTPNotFound(explanation=explanation)
+
+ # public flavor to all projects
+ if flavor['is_public']:
+ explanation = _("Access list not available for public flavors.")
+ raise webob.exc.HTTPNotFound(explanation=explanation)
+
+ # private flavor to listed projects only
+ return _marshall_flavor_access(flavor_id)
+
+
+class FlavorActionController(wsgi.Controller):
+ """The flavor access API controller for the OpenStack API."""
+
+ def _check_body(self, body):
+ if body is None or body == "":
+ raise webob.exc.HTTPBadRequest(explanation=_("No request body"))
+
+ def _get_flavor_refs(self, context):
+ """Return a dictionary mapping flavorid to flavor_ref."""
+
+ flavor_refs = flavors.get_all_flavors(context)
+ rval = {}
+ for name, obj in flavor_refs.iteritems():
+ rval[obj['flavorid']] = obj
+ return rval
+
+ def _extend_flavor(self, flavor_rval, flavor_ref):
+ key = "%s:is_public" % (FlavorAccess.alias)
+ flavor_rval[key] = flavor_ref['is_public']
+
+ @wsgi.extends
+ def show(self, req, resp_obj, id):
+ context = req.environ['nova.context']
+ if authorize(context):
+ # Attach our slave template to the response object
+ resp_obj.attach(xml=FlavorTemplate())
+ db_flavor = req.get_db_flavor(id)
+
+ self._extend_flavor(resp_obj.obj['flavor'], db_flavor)
+
+ @wsgi.extends
+ def detail(self, req, resp_obj):
+ context = req.environ['nova.context']
+ if authorize(context):
+ # Attach our slave template to the response object
+ resp_obj.attach(xml=FlavorsTemplate())
+
+ flavors = list(resp_obj.obj['flavors'])
+ for flavor_rval in flavors:
+ db_flavor = req.get_db_flavor(flavor_rval['id'])
+ self._extend_flavor(flavor_rval, db_flavor)
+
+ @wsgi.extends(action='create')
+ def create(self, req, body, resp_obj):
+ context = req.environ['nova.context']
+ if authorize(context):
+ # Attach our slave template to the response object
+ resp_obj.attach(xml=FlavorTemplate())
+
+ db_flavor = req.get_db_flavor(resp_obj.obj['flavor']['id'])
+
+ self._extend_flavor(resp_obj.obj['flavor'], db_flavor)
+
+ @wsgi.serializers(xml=FlavorAccessTemplate)
+ @wsgi.action("addTenantAccess")
+ def _addTenantAccess(self, req, id, body):
+ context = req.environ['nova.context']
+ authorize(context)
+ self._check_body(body)
+
+ vals = body['addTenantAccess']
+ tenant = vals['tenant']
+
+ try:
+ flavors.add_flavor_access(id, tenant, context)
+ except exception.FlavorAccessExists as err:
+ raise webob.exc.HTTPConflict(explanation=err.format_message())
+
+ return _marshall_flavor_access(id)
+
+ @wsgi.serializers(xml=FlavorAccessTemplate)
+ @wsgi.action("removeTenantAccess")
+ def _removeTenantAccess(self, req, id, body):
+ context = req.environ['nova.context']
+ authorize(context)
+ self._check_body(body)
+
+ vals = body['removeTenantAccess']
+ tenant = vals['tenant']
+
+ try:
+ flavors.remove_flavor_access(id, tenant, context)
+ except exception.FlavorAccessNotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=e.format_message())
+
+ return _marshall_flavor_access(id)
+
+
+class FlavorAccess(extensions.V3APIExtensionBase):
+ """Flavor access support."""
+
+ name = "FlavorAccess"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/%s/api/v3" % ALIAS
+ version = 1
+
+ def get_resources(self):
+ res = extensions.ResourceExtension(
+ ALIAS,
+ controller=FlavorAccessController(),
+ parent=dict(member_name='flavor', collection_name='flavors'))
+
+ return [res]
+
+ def get_controller_extensions(self):
+ extension = extensions.ControllerExtension(
+ self, 'flavors', FlavorActionController())
+
+ return [extension]
diff --git a/nova/api/openstack/compute/plugins/v3/flavor_disabled.py b/nova/api/openstack/compute/plugins/v3/flavor_disabled.py
new file mode 100644
index 000000000..79f6df9ce
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/flavor_disabled.py
@@ -0,0 +1,91 @@
+# Copyright 2012 Nebula, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""The Flavor Disabled API extension."""
+
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+
+ALIAS = 'os-flavor-disabled'
+authorize = extensions.soft_extension_authorizer('compute', 'v3:' + ALIAS)
+
+
+class FlavorDisabledController(wsgi.Controller):
+ def _extend_flavors(self, req, flavors):
+ for flavor in flavors:
+ db_flavor = req.get_db_flavor(flavor['id'])
+ key = "%s:disabled" % FlavorDisabled.alias
+ flavor[key] = db_flavor['disabled']
+
+ def _show(self, req, resp_obj):
+ if not authorize(req.environ['nova.context']):
+ return
+ if 'flavor' in resp_obj.obj:
+ resp_obj.attach(xml=FlavorDisabledTemplate())
+ self._extend_flavors(req, [resp_obj.obj['flavor']])
+
+ @wsgi.extends
+ def show(self, req, resp_obj, id):
+ return self._show(req, resp_obj)
+
+ @wsgi.extends(action='create')
+ def create(self, req, resp_obj, body):
+ return self._show(req, resp_obj)
+
+ @wsgi.extends
+ def detail(self, req, resp_obj):
+ if not authorize(req.environ['nova.context']):
+ return
+ resp_obj.attach(xml=FlavorsDisabledTemplate())
+ self._extend_flavors(req, list(resp_obj.obj['flavors']))
+
+
+class FlavorDisabled(extensions.V3APIExtensionBase):
+ """Support to show the disabled status of a flavor."""
+
+ name = "FlavorDisabled"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/%s/api/v3" % ALIAS
+ version = 1
+
+ def get_controller_extensions(self):
+ controller = FlavorDisabledController()
+ extension = extensions.ControllerExtension(self, 'flavors', controller)
+ return [extension]
+
+ def get_resources(self):
+ return []
+
+
+def make_flavor(elem):
+ elem.set('{%s}disabled' % FlavorDisabled.namespace,
+ '%s:disabled' % FlavorDisabled.alias)
+
+
+class FlavorDisabledTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavor', selector='flavor')
+ make_flavor(root)
+ return xmlutil.SlaveTemplate(root, 1, nsmap={
+ FlavorDisabled.alias: FlavorDisabled.namespace})
+
+
+class FlavorsDisabledTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavors')
+ elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors')
+ make_flavor(elem)
+ return xmlutil.SlaveTemplate(root, 1, nsmap={
+ FlavorDisabled.alias: FlavorDisabled.namespace})
diff --git a/nova/api/openstack/compute/plugins/v3/flavors.py b/nova/api/openstack/compute/plugins/v3/flavors.py
new file mode 100644
index 000000000..31c0fa8b7
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/flavors.py
@@ -0,0 +1,169 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import webob
+
+from nova.api.openstack import common
+from nova.api.openstack.compute.views import flavors as flavors_view
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova.compute import flavors
+from nova import exception
+from nova.openstack.common import strutils
+
+
+def make_flavor(elem, detailed=False):
+ elem.set('name')
+ elem.set('id')
+ if detailed:
+ elem.set('ram')
+ elem.set('disk')
+ elem.set('vcpus', xmlutil.EmptyStringSelector('vcpus'))
+ # NOTE(vish): this was originally added without a namespace
+ elem.set('swap', xmlutil.EmptyStringSelector('swap'))
+
+ xmlutil.make_links(elem, 'links')
+
+
+flavor_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM}
+
+
+class FlavorTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavor', selector='flavor')
+ make_flavor(root, detailed=True)
+ return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap)
+
+
+class MinimalFlavorsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavors')
+ elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors')
+ make_flavor(elem)
+ return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap)
+
+
+class FlavorsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('flavors')
+ elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors')
+ make_flavor(elem, detailed=True)
+ return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap)
+
+
+class FlavorsController(wsgi.Controller):
+ """Flavor controller for the OpenStack API."""
+
+ _view_builder_class = flavors_view.ViewBuilder
+
+ @wsgi.serializers(xml=MinimalFlavorsTemplate)
+ def index(self, req):
+ """Return all flavors in brief."""
+ limited_flavors = self._get_flavors(req)
+ return self._view_builder.index(req, limited_flavors)
+
+ @wsgi.serializers(xml=FlavorsTemplate)
+ def detail(self, req):
+ """Return all flavors in detail."""
+ limited_flavors = self._get_flavors(req)
+ req.cache_db_flavors(limited_flavors)
+ return self._view_builder.detail(req, limited_flavors)
+
+ @wsgi.serializers(xml=FlavorTemplate)
+ def show(self, req, id):
+ """Return data about the given flavor id."""
+ try:
+ flavor = flavors.get_flavor_by_flavor_id(id)
+ req.cache_db_flavor(flavor)
+ except exception.NotFound:
+ raise webob.exc.HTTPNotFound()
+
+ return self._view_builder.show(req, flavor)
+
+ def _parse_is_public(self, is_public):
+ """Parse is_public into something usable."""
+
+ if is_public is None:
+ # preserve default value of showing only public flavors
+ return True
+ elif is_public == 'none':
+ return None
+ else:
+ try:
+ return strutils.bool_from_string(is_public, strict=True)
+ except ValueError:
+ msg = _('Invalid is_public filter [%s]') % is_public
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ def _get_flavors(self, req):
+ """Helper function that returns a list of flavor dicts."""
+ filters = {}
+
+ context = req.environ['nova.context']
+ if context.is_admin:
+ # Only admin has query access to all flavor types
+ filters['is_public'] = self._parse_is_public(
+ req.params.get('is_public', None))
+ else:
+ filters['is_public'] = True
+ filters['disabled'] = False
+
+ if 'minRam' in req.params:
+ try:
+ filters['min_memory_mb'] = int(req.params['minRam'])
+ except ValueError:
+ msg = _('Invalid minRam filter [%s]') % req.params['minRam']
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ if 'minDisk' in req.params:
+ try:
+ filters['min_root_gb'] = int(req.params['minDisk'])
+ except ValueError:
+ msg = _('Invalid minDisk filter [%s]') % req.params['minDisk']
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ limited_flavors = flavors.get_all_flavors(context, filters=filters)
+ flavors_list = limited_flavors.values()
+ sorted_flavors = sorted(flavors_list,
+ key=lambda item: item['flavorid'])
+ limited_flavors = common.limited_by_marker(sorted_flavors, req)
+ return limited_flavors
+
+
+class Flavors(extensions.V3APIExtensionBase):
+ """ Flavors Extension. """
+ name = "flavors"
+ alias = "flavors"
+ namespace = "http://docs.openstack.org/compute/core/flavors/v3"
+ version = 1
+
+ def get_resources(self):
+ collection_actions = {'detail': 'GET'}
+ member_actions = {'action': 'POST'}
+
+ resources = [
+ extensions.ResourceExtension('flavors',
+ FlavorsController(),
+ member_name='flavor',
+ collection_actions=collection_actions,
+ member_actions=member_actions)
+ ]
+ return resources
+
+ def get_controller_extensions(self):
+ return []
diff --git a/nova/api/openstack/compute/plugins/v3/images.py b/nova/api/openstack/compute/plugins/v3/images.py
new file mode 100644
index 000000000..dde22488d
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/images.py
@@ -0,0 +1,242 @@
+# Copyright 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import webob.exc
+
+from nova.api.openstack import common
+from nova.api.openstack.compute.views import images as views_images
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova import exception
+import nova.image.glance
+import nova.utils
+
+
+ALIAS = "os-images"
+authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
+
+SUPPORTED_FILTERS = {
+ 'name': 'name',
+ 'status': 'status',
+ 'changes-since': 'changes-since',
+ 'server': 'property-instance_uuid',
+ 'type': 'property-image_type',
+ 'minRam': 'min_ram',
+ 'minDisk': 'min_disk',
+}
+
+
+def make_image(elem, detailed=False):
+ elem.set('name')
+ elem.set('id')
+
+ if detailed:
+ elem.set('updated')
+ elem.set('created')
+ elem.set('status')
+ elem.set('progress')
+ elem.set('minRam')
+ elem.set('minDisk')
+
+ server = xmlutil.SubTemplateElement(elem, 'server', selector='server')
+ server.set('id')
+ xmlutil.make_links(server, 'links')
+
+ elem.append(common.MetadataTemplate())
+
+ xmlutil.make_links(elem, 'links')
+
+
+image_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM}
+
+
+class ImageTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('image', selector='image')
+ make_image(root, detailed=True)
+ return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap)
+
+
+class MinimalImagesTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('images')
+ elem = xmlutil.SubTemplateElement(root, 'image', selector='images')
+ make_image(elem)
+ xmlutil.make_links(root, 'images_links')
+ return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap)
+
+
+class ImagesTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('images')
+ elem = xmlutil.SubTemplateElement(root, 'image', selector='images')
+ make_image(elem, detailed=True)
+ return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap)
+
+
+class ImagesController(wsgi.Controller):
+ """Base controller for retrieving/displaying images."""
+
+ _view_builder_class = views_images.ViewBuilder
+
+ def __init__(self, image_service=None, **kwargs):
+ """Initialize new `ImageController`.
+
+ :param image_service: `nova.image.glance:GlanceImageService`
+
+ """
+ super(ImagesController, self).__init__(**kwargs)
+ self._image_service = (image_service or
+ nova.image.glance.get_default_image_service())
+
+ def _get_filters(self, req):
+ """
+ Return a dictionary of query param filters from the request
+
+ :param req: the Request object coming from the wsgi layer
+ :retval a dict of key/value filters
+ """
+ filters = {}
+ for param in req.params:
+ if param in SUPPORTED_FILTERS or param.startswith('property-'):
+ # map filter name or carry through if property-*
+ filter_name = SUPPORTED_FILTERS.get(param, param)
+ filters[filter_name] = req.params.get(param)
+
+ # ensure server filter is the instance uuid
+ filter_name = 'property-instance_uuid'
+ try:
+ filters[filter_name] = filters[filter_name].rsplit('/', 1)[1]
+ except (AttributeError, IndexError, KeyError):
+ pass
+
+ filter_name = 'status'
+ if filter_name in filters:
+ # The Image API expects us to use lowercase strings for status
+ filters[filter_name] = filters[filter_name].lower()
+
+ return filters
+
+ @wsgi.serializers(xml=ImageTemplate)
+ def show(self, req, id):
+ """Return detailed information about a specific image.
+
+ :param req: `wsgi.Request` object
+ :param id: Image identifier
+ """
+ context = req.environ['nova.context']
+ authorize(context)
+
+ try:
+ image = self._image_service.show(context, id)
+ except (exception.NotFound, exception.InvalidImageRef):
+ explanation = _("Image not found.")
+ raise webob.exc.HTTPNotFound(explanation=explanation)
+
+ req.cache_db_items('images', [image], 'id')
+ return self._view_builder.show(req, image)
+
+ def delete(self, req, id):
+ """Delete an image, if allowed.
+
+ :param req: `wsgi.Request` object
+ :param id: Image identifier (integer)
+ """
+ context = req.environ['nova.context']
+ authorize(context)
+
+ try:
+ self._image_service.delete(context, id)
+ except exception.ImageNotFound:
+ explanation = _("Image not found.")
+ raise webob.exc.HTTPNotFound(explanation=explanation)
+ except exception.ImageNotAuthorized:
+ # The image service raises this exception on delete if glanceclient
+ # raises HTTPForbidden.
+ explanation = _("You are not allowed to delete the image.")
+ raise webob.exc.HTTPForbidden(explanation=explanation)
+ return webob.exc.HTTPNoContent()
+
+ @wsgi.serializers(xml=MinimalImagesTemplate)
+ def index(self, req):
+ """Return an index listing of images available to the request.
+
+ :param req: `wsgi.Request` object
+
+ """
+ context = req.environ['nova.context']
+ authorize(context)
+
+ filters = self._get_filters(req)
+ params = req.GET.copy()
+ page_params = common.get_pagination_params(req)
+ for key, val in page_params.iteritems():
+ params[key] = val
+
+ try:
+ images = self._image_service.detail(context, filters=filters,
+ **page_params)
+ except exception.Invalid as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.format_message())
+ return self._view_builder.index(req, images)
+
+ @wsgi.serializers(xml=ImagesTemplate)
+ def detail(self, req):
+ """Return a detailed index listing of images available to the request.
+
+ :param req: `wsgi.Request` object.
+
+ """
+ context = req.environ['nova.context']
+ authorize(context)
+
+ filters = self._get_filters(req)
+ params = req.GET.copy()
+ page_params = common.get_pagination_params(req)
+ for key, val in page_params.iteritems():
+ params[key] = val
+ try:
+ images = self._image_service.detail(context, filters=filters,
+ **page_params)
+ except exception.Invalid as e:
+ raise webob.exc.HTTPBadRequest(explanation=e.format_message())
+
+ req.cache_db_items('images', images, 'id')
+ return self._view_builder.detail(req, images)
+
+ def create(self, *args, **kwargs):
+ raise webob.exc.HTTPMethodNotAllowed()
+
+
+class Images(extensions.V3APIExtensionBase):
+ """Server addresses."""
+
+ name = "Images"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/images/v3"
+ version = 1
+
+ def get_resources(self):
+ collection_actions = {'detail': 'GET'}
+ resources = [
+ extensions.ResourceExtension(
+ ALIAS, ImagesController(),
+ collection_actions=collection_actions)]
+
+ return resources
+
+ def get_controller_extensions(self):
+ return []
diff --git a/nova/api/openstack/compute/plugins/v3/keypairs.py b/nova/api/openstack/compute/plugins/v3/keypairs.py
index bf740641e..ab40b051c 100644
--- a/nova/api/openstack/compute/plugins/v3/keypairs.py
+++ b/nova/api/openstack/compute/plugins/v3/keypairs.py
@@ -55,6 +55,7 @@ class KeypairController(object):
self.api = compute_api.KeypairAPI()
@wsgi.serializers(xml=KeypairTemplate)
+ @extensions.expected_errors((400, 409, 413))
def create(self, req, body):
"""
Create or import keypair.
@@ -100,6 +101,7 @@ class KeypairController(object):
except exception.KeyPairExists as exc:
raise webob.exc.HTTPConflict(explanation=exc.format_message())
+ @extensions.expected_errors(404)
def delete(self, req, id):
"""
Delete a keypair with a given name
@@ -113,6 +115,7 @@ class KeypairController(object):
return webob.Response(status_int=202)
@wsgi.serializers(xml=KeypairTemplate)
+ @extensions.expected_errors(404)
def show(self, req, id):
"""Return data for the given key name."""
context = req.environ['nova.context']
diff --git a/nova/api/openstack/compute/plugins/v3/quota_classes.py b/nova/api/openstack/compute/plugins/v3/quota_classes.py
new file mode 100644
index 000000000..361748df8
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/quota_classes.py
@@ -0,0 +1,103 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import webob
+
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+import nova.context
+from nova import db
+from nova import exception
+from nova import quota
+
+
+QUOTAS = quota.QUOTAS
+
+
+authorize = extensions.extension_authorizer('compute', 'quota_classes')
+
+
+class QuotaClassTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('quota_class_set',
+ selector='quota_class_set')
+ root.set('id')
+
+ for resource in QUOTAS.resources:
+ elem = xmlutil.SubTemplateElement(root, resource)
+ elem.text = resource
+
+ return xmlutil.MasterTemplate(root, 1)
+
+
+class QuotaClassSetsController(object):
+
+ def _format_quota_set(self, quota_class, quota_set):
+ """Convert the quota object to a result dict."""
+
+ result = dict(id=str(quota_class))
+
+ for resource in QUOTAS.resources:
+ result[resource] = quota_set[resource]
+
+ return dict(quota_class_set=result)
+
+ @wsgi.serializers(xml=QuotaClassTemplate)
+ def show(self, req, id):
+ context = req.environ['nova.context']
+ authorize(context)
+ try:
+ nova.context.authorize_quota_class_context(context, id)
+ return self._format_quota_set(id,
+ QUOTAS.get_class_quotas(context, id))
+ except exception.NotAuthorized:
+ raise webob.exc.HTTPForbidden()
+
+ @wsgi.serializers(xml=QuotaClassTemplate)
+ def update(self, req, id, body):
+ context = req.environ['nova.context']
+ authorize(context)
+ quota_class = id
+ for key in body['quota_class_set'].keys():
+ if key in QUOTAS:
+ value = int(body['quota_class_set'][key])
+ try:
+ db.quota_class_update(context, quota_class, key, value)
+ except exception.QuotaClassNotFound:
+ db.quota_class_create(context, quota_class, key, value)
+ except exception.AdminRequired:
+ raise webob.exc.HTTPForbidden()
+ return {'quota_class_set': QUOTAS.get_class_quotas(context,
+ quota_class)}
+
+
+class Quota_classes(extensions.ExtensionDescriptor):
+ """Quota classes management support."""
+
+ name = "QuotaClasses"
+ alias = "os-quota-class-sets"
+ namespace = ("http://docs.openstack.org/compute/ext/"
+ "quota-classes-sets/api/v1.1")
+ updated = "2012-03-12T00:00:00+00:00"
+
+ def get_resources(self):
+ resources = []
+
+ res = extensions.ResourceExtension('os-quota-class-sets',
+ QuotaClassSetsController())
+ resources.append(res)
+
+ return resources
diff --git a/nova/api/openstack/compute/plugins/v3/quota_sets.py b/nova/api/openstack/compute/plugins/v3/quota_sets.py
new file mode 100644
index 000000000..67af5d127
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/quota_sets.py
@@ -0,0 +1,214 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import webob
+
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+import nova.context
+from nova import db
+from nova import exception
+from nova.openstack.common import log as logging
+from nova.openstack.common import strutils
+from nova import quota
+
+
+ALIAS = "os-quota-sets"
+QUOTAS = quota.QUOTAS
+LOG = logging.getLogger(__name__)
+NON_QUOTA_KEYS = ['tenant_id', 'id', 'force']
+
+
+authorize_update = extensions.extension_authorizer('compute',
+ 'v3:%s:update' % ALIAS)
+authorize_show = extensions.extension_authorizer('compute',
+ 'v3:%s:show' % ALIAS)
+authorize_delete = extensions.extension_authorizer('compute',
+ 'v3:%s:delete' % ALIAS)
+
+
+class QuotaTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('quota_set', selector='quota_set')
+ root.set('id')
+
+ for resource in QUOTAS.resources:
+ elem = xmlutil.SubTemplateElement(root, resource)
+ elem.text = resource
+
+ return xmlutil.MasterTemplate(root, 1)
+
+
+class QuotaSetsController(object):
+
+ def __init__(self, ext_mgr):
+ self.ext_mgr = ext_mgr
+
+ def _format_quota_set(self, project_id, quota_set):
+ """Convert the quota object to a result dict."""
+
+ result = dict(id=str(project_id))
+
+ for resource in QUOTAS.resources:
+ result[resource] = quota_set[resource]
+
+ return dict(quota_set=result)
+
+ def _validate_quota_limit(self, limit):
+ # NOTE: -1 is a flag value for unlimited
+ if limit < -1:
+ msg = _("Quota limit must be -1 or greater.")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ def _get_quotas(self, context, id, usages=False):
+ values = QUOTAS.get_project_quotas(context, id, usages=usages)
+
+ if usages:
+ return values
+ else:
+ return dict((k, v['limit']) for k, v in values.items())
+
+ @wsgi.serializers(xml=QuotaTemplate)
+ def show(self, req, id):
+ context = req.environ['nova.context']
+ authorize_show(context)
+ try:
+ nova.context.authorize_project_context(context, id)
+ return self._format_quota_set(id, self._get_quotas(context, id))
+ except exception.NotAuthorized:
+ raise webob.exc.HTTPForbidden()
+
+ @wsgi.serializers(xml=QuotaTemplate)
+ def update(self, req, id, body):
+ context = req.environ['nova.context']
+ authorize_update(context)
+ project_id = id
+
+ bad_keys = []
+
+ # By default, we can force update the quota if the extended
+ # is not loaded
+ force_update = True
+ extended_loaded = False
+ if self.ext_mgr.is_loaded('os-extended-quotas'):
+ # force optional has been enabled, the default value of
+ # force_update need to be changed to False
+ extended_loaded = True
+ force_update = False
+
+ for key, value in body['quota_set'].items():
+ if (key not in QUOTAS and
+ key not in NON_QUOTA_KEYS):
+ bad_keys.append(key)
+ continue
+ if key == 'force' and extended_loaded:
+ # only check the force optional when the extended has
+ # been loaded
+ force_update = strutils.bool_from_string(value)
+ elif key not in NON_QUOTA_KEYS and value:
+ try:
+ value = int(value)
+ except (ValueError, TypeError):
+ msg = _("Quota '%(value)s' for %(key)s should be "
+ "integer.") % locals()
+ LOG.warn(msg)
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+ self._validate_quota_limit(value)
+
+ LOG.debug(_("force update quotas: %s") % force_update)
+
+ if len(bad_keys) > 0:
+ msg = _("Bad key(s) %s in quota_set") % ",".join(bad_keys)
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ try:
+ project_quota = self._get_quotas(context, id, True)
+ except exception.NotAuthorized:
+ raise webob.exc.HTTPForbidden()
+
+ for key, value in body['quota_set'].items():
+ if key in NON_QUOTA_KEYS or not value:
+ continue
+ # validate whether already used and reserved exceeds the new
+ # quota, this check will be ignored if admin want to force
+ # update
+ value = int(value)
+ if force_update is not True and value >= 0:
+ quota_value = project_quota.get(key)
+ if quota_value and quota_value['limit'] >= 0:
+ quota_used = (quota_value['in_use'] +
+ quota_value['reserved'])
+ LOG.debug(_("Quota %(key)s used: %(quota_used)s, "
+ "value: %(value)s."),
+ {'key': key, 'quota_used': quota_used,
+ 'value': value})
+ if quota_used > value:
+ msg = (_("Quota value %(value)s for %(key)s are "
+ "greater than already used and reserved "
+ "%(quota_used)s") %
+ {'value': value, 'key': key,
+ 'quota_used': quota_used})
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ try:
+ db.quota_update(context, project_id, key, value)
+ except exception.ProjectQuotaNotFound:
+ db.quota_create(context, project_id, key, value)
+ except exception.AdminRequired:
+ raise webob.exc.HTTPForbidden()
+ return {'quota_set': self._get_quotas(context, id)}
+
+ @wsgi.serializers(xml=QuotaTemplate)
+ def defaults(self, req, id):
+ context = req.environ['nova.context']
+ authorize_show(context)
+ return self._format_quota_set(id, QUOTAS.get_defaults(context))
+
+ def delete(self, req, id):
+ if self.ext_mgr.is_loaded('os-extended-quotas'):
+ context = req.environ['nova.context']
+ authorize_delete(context)
+ try:
+ nova.context.authorize_project_context(context, id)
+ QUOTAS.destroy_all_by_project(context, id)
+ return webob.Response(status_int=202)
+ except exception.NotAuthorized:
+ raise webob.exc.HTTPForbidden()
+ raise webob.exc.HTTPNotFound()
+
+
+class QuotaSets(extensions.V3APIExtensionBase):
+ """Quotas management support."""
+
+ name = "Quotas"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/os-quotas-sets/api/v3"
+ version = 1
+
+ def get_resources(self):
+ resources = []
+
+ res = extensions.ResourceExtension(ALIAS,
+ QuotaSetsController(self.ext_mgr),
+ member_actions={'defaults': 'GET'})
+ resources.append(res)
+
+ return resources
+
+ def get_controller_extensions(self):
+ return []
diff --git a/nova/api/openstack/compute/plugins/v3/rescue.py b/nova/api/openstack/compute/plugins/v3/rescue.py
new file mode 100644
index 000000000..ded18bb1a
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/rescue.py
@@ -0,0 +1,100 @@
+# Copyright 2011 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""The rescue mode extension."""
+
+from oslo.config import cfg
+import webob
+from webob import exc
+
+from nova.api.openstack import common
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova import compute
+from nova import exception
+from nova import utils
+
+
+ALIAS = "os-rescue"
+CONF = cfg.CONF
+authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
+
+
+class RescueController(wsgi.Controller):
+ def __init__(self, *args, **kwargs):
+ super(RescueController, self).__init__(*args, **kwargs)
+ self.compute_api = compute.API()
+
+ def _get_instance(self, context, instance_id):
+ try:
+ return self.compute_api.get(context, instance_id)
+ except exception.InstanceNotFound:
+ msg = _("Server not found")
+ raise exc.HTTPNotFound(msg)
+
+ @wsgi.action('rescue')
+ def _rescue(self, req, id, body):
+ """Rescue an instance."""
+ context = req.environ["nova.context"]
+ authorize(context)
+
+ if body['rescue'] and 'adminPass' in body['rescue']:
+ password = body['rescue']['adminPass']
+ else:
+ password = utils.generate_password()
+
+ instance = self._get_instance(context, id)
+ try:
+ self.compute_api.rescue(context, instance,
+ rescue_password=password)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'rescue')
+ except exception.InvalidVolume as volume_error:
+ raise exc.HTTPConflict(explanation=volume_error.format_message())
+ except exception.InstanceNotRescuable as non_rescuable:
+ raise exc.HTTPBadRequest(
+ explanation=non_rescuable.format_message())
+
+ return {'adminPass': password}
+
+ @wsgi.action('unrescue')
+ def _unrescue(self, req, id, body):
+ """Unrescue an instance."""
+ context = req.environ["nova.context"]
+ authorize(context)
+ instance = self._get_instance(context, id)
+ try:
+ self.compute_api.unrescue(context, instance)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'unrescue')
+ return webob.Response(status_int=202)
+
+
+class Rescue(extensions.V3APIExtensionBase):
+ """Instance rescue mode."""
+
+ name = "Rescue"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/rescue/api/v3"
+ version = 1
+
+ def get_resources(self):
+ return []
+
+ def get_controller_extensions(self):
+ controller = RescueController()
+ extension = extensions.ControllerExtension(self, 'servers', controller)
+ return [extension]
diff --git a/nova/api/openstack/compute/plugins/v3/server_diagnostics.py b/nova/api/openstack/compute/plugins/v3/server_diagnostics.py
new file mode 100644
index 000000000..6a19732dc
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/server_diagnostics.py
@@ -0,0 +1,71 @@
+# Copyright 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import webob.exc
+
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova import compute
+from nova import exception
+
+
+ALIAS = "os-server-diagnostics"
+authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
+sd_nsmap = {None: wsgi.XMLNS_V11}
+
+
+class ServerDiagnosticsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('diagnostics')
+ elem = xmlutil.SubTemplateElement(root, xmlutil.Selector(0),
+ selector=xmlutil.get_items)
+ elem.text = 1
+ return xmlutil.MasterTemplate(root, 1, nsmap=sd_nsmap)
+
+
+class ServerDiagnosticsController(object):
+ @wsgi.serializers(xml=ServerDiagnosticsTemplate)
+ def index(self, req, server_id):
+ context = req.environ["nova.context"]
+ authorize(context)
+ compute_api = compute.API()
+ try:
+ instance = compute_api.get(context, server_id)
+ except exception.NotFound():
+ raise webob.exc.HTTPNotFound(_("Instance not found"))
+
+ return compute_api.get_diagnostics(context, instance)
+
+
+class ServerDiagnostics(extensions.V3APIExtensionBase):
+ """Allow Admins to view server diagnostics through server action."""
+
+ name = "ServerDiagnostics"
+ alias = ALIAS
+ namespace = ("http://docs.openstack.org/compute/ext/"
+ "server-diagnostics/api/v3")
+ version = 1
+
+ def get_resources(self):
+ parent_def = {'member_name': 'server', 'collection_name': 'servers'}
+ resources = [
+ extensions.ResourceExtension(ALIAS,
+ ServerDiagnosticsController(),
+ parent=parent_def)]
+ return resources
+
+ def get_controller_extensions(self):
+ return []
diff --git a/nova/api/openstack/compute/schemas/v3/flavor.rng b/nova/api/openstack/compute/schemas/v3/flavor.rng
new file mode 100644
index 000000000..4b6b74001
--- /dev/null
+++ b/nova/api/openstack/compute/schemas/v3/flavor.rng
@@ -0,0 +1,12 @@
+<element name="flavor" ns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns="http://relaxng.org/ns/structure/1.0">
+ <attribute name="name"> <text/> </attribute>
+ <attribute name="id"> <text/> </attribute>
+ <attribute name="ram"> <text/> </attribute>
+ <attribute name="disk"> <text/> </attribute>
+ <attribute name="vcpus"> <text/> </attribute>
+ <attribute name="swap"> <text/> </attribute>
+ <zeroOrMore>
+ <externalRef href="../atom-link.rng"/>
+ </zeroOrMore>
+</element>
diff --git a/nova/api/openstack/compute/schemas/v3/flavors.rng b/nova/api/openstack/compute/schemas/v3/flavors.rng
new file mode 100644
index 000000000..b7a3acc01
--- /dev/null
+++ b/nova/api/openstack/compute/schemas/v3/flavors.rng
@@ -0,0 +1,6 @@
+<element name="flavors" xmlns="http://relaxng.org/ns/structure/1.0"
+ ns="http://docs.openstack.org/compute/api/v1.1">
+ <zeroOrMore>
+ <externalRef href="flavor.rng"/>
+ </zeroOrMore>
+</element>
diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py
index de7a51812..8c329d12b 100644
--- a/nova/api/openstack/compute/servers.py
+++ b/nova/api/openstack/compute/servers.py
@@ -552,10 +552,10 @@ class Controller(wsgi.Controller):
search_opts=search_opts,
limit=limit,
marker=marker)
- except exception.MarkerNotFound as e:
+ except exception.MarkerNotFound:
msg = _('marker [%s] not found') % marker
raise exc.HTTPBadRequest(explanation=msg)
- except exception.FlavorNotFound as e:
+ except exception.FlavorNotFound:
log_msg = _("Flavor '%s' could not be found ")
LOG.debug(log_msg, search_opts['flavor'])
instance_list = []
@@ -919,6 +919,9 @@ class Controller(wsgi.Controller):
except exception.KeypairNotFound as error:
msg = _("Invalid key_name provided.")
raise exc.HTTPBadRequest(explanation=msg)
+ except exception.ConfigDriveInvalidValue:
+ msg = _("Invalid config_drive provided.")
+ raise exc.HTTPBadRequest(explanation=msg)
except rpc_common.RemoteError as err:
msg = "%(err_type)s: %(err_msg)s" % {'err_type': err.exc_type,
'err_msg': err.value}
@@ -1093,11 +1096,11 @@ class Controller(wsgi.Controller):
except exception.InstanceInvalidState as state_error:
common.raise_http_conflict_for_instance_invalid_state(state_error,
'resize')
- except exception.ImageNotAuthorized as image_error:
+ except exception.ImageNotAuthorized:
msg = _("You are not authorized to access the image "
"the instance was started with.")
raise exc.HTTPUnauthorized(explanation=msg)
- except exception.ImageNotFound as image_error:
+ except exception.ImageNotFound:
msg = _("Image that the instance was started "
"with could not be found.")
raise exc.HTTPBadRequest(explanation=msg)
diff --git a/nova/api/openstack/compute/views/servers.py b/nova/api/openstack/compute/views/servers.py
index 734bb647d..03b0a97a5 100644
--- a/nova/api/openstack/compute/views/servers.py
+++ b/nova/api/openstack/compute/views/servers.py
@@ -25,6 +25,7 @@ from nova.api.openstack.compute.views import images as views_images
from nova.compute import flavors
from nova.openstack.common import log as logging
from nova.openstack.common import timeutils
+from nova import utils
LOG = logging.getLogger(__name__)
@@ -132,8 +133,7 @@ class ViewBuilder(common.ViewBuilder):
@staticmethod
def _get_metadata(instance):
- metadata = instance.get("metadata", [])
- return dict((item['key'], item['value']) for item in metadata)
+ return utils.instance_meta(instance)
@staticmethod
def _get_vm_state(instance):
diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py
index 6cbc5bb78..69cc87546 100644
--- a/nova/api/openstack/extensions.py
+++ b/nova/api/openstack/extensions.py
@@ -17,6 +17,7 @@
# under the License.
import abc
+import functools
import os
import webob.dec
@@ -392,9 +393,9 @@ def extension_authorizer(api_name, extension_name):
def soft_extension_authorizer(api_name, extension_name):
hard_authorize = extension_authorizer(api_name, extension_name)
- def authorize(context):
+ def authorize(context, action=None):
try:
- hard_authorize(context)
+ hard_authorize(context, action=action)
return True
except exception.NotAuthorized:
return False
@@ -451,3 +452,42 @@ class V3APIExtensionBase(object):
def version(self):
"""Version of the extension."""
pass
+
+
+def expected_errors(errors):
+ """Decorator for v3 API methods which specifies expected exceptions.
+
+ Specify which exceptions may occur when an API method is called. If an
+ unexpected exception occurs then return a 500 instead and ask the user
+ of the API to file a bug report.
+ """
+ def decorator(f):
+ @functools.wraps(f)
+ def wrapped(*args, **kwargs):
+ try:
+ return f(*args, **kwargs)
+ except Exception as exc:
+ if isinstance(exc, webob.exc.WSGIHTTPException):
+ if isinstance(errors, int):
+ t_errors = (errors,)
+ else:
+ t_errors = errors
+ if exc.code in t_errors:
+ raise
+ elif isinstance(exc, exception.PolicyNotAuthorized):
+ # Note(cyeoh): Special case to handle
+ # PolicyNotAuthorized exceptions so every
+ # extension method does not need to wrap authorize
+ # calls. ResourceExceptionHandler silently
+ # converts NotAuthorized to HTTPForbidden
+ raise
+
+ LOG.exception(_("Unexpected exception in API method"))
+ msg = _('Unexpected API Error. Please report this at '
+ 'http://bugs.launchpad.net/nova/ and attach the Nova '
+ 'API log if possible.\n%s') % type(exc)
+ raise webob.exc.HTTPInternalServerError(explanation=msg)
+
+ return wrapped
+
+ return decorator
diff --git a/nova/api/openstack/xmlutil.py b/nova/api/openstack/xmlutil.py
index 04f5e28e3..37766b3e3 100644
--- a/nova/api/openstack/xmlutil.py
+++ b/nova/api/openstack/xmlutil.py
@@ -33,12 +33,12 @@ XMLNS_COMMON_V10 = 'http://docs.openstack.org/common/api/v1.0'
XMLNS_ATOM = 'http://www.w3.org/2005/Atom'
-def validate_schema(xml, schema_name):
+def validate_schema(xml, schema_name, version='v1.1'):
if isinstance(xml, str):
xml = etree.fromstring(xml)
- base_path = 'nova/api/openstack/compute/schemas/v1.1/'
- if schema_name in ('atom', 'atom-link'):
- base_path = 'nova/api/openstack/compute/schemas/'
+ base_path = 'nova/api/openstack/compute/schemas/'
+ if schema_name not in ('atom', 'atom-link'):
+ base_path += '%s/' % version
schema_path = os.path.join(utils.novadir(),
'%s%s.rng' % (base_path, schema_name))
schema_doc = etree.parse(schema_path)