summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--etc/nova/policy.json1
-rw-r--r--nova/api/openstack/__init__.py29
-rw-r--r--nova/api/openstack/compute/__init__.py14
-rw-r--r--nova/api/openstack/compute/plugins/__init__.py59
-rw-r--r--nova/api/openstack/compute/plugins/v3/extension_info.py105
-rw-r--r--nova/api/openstack/compute/plugins/v3/keypairs.py215
-rw-r--r--nova/api/openstack/compute/plugins/v3/servers.py1533
-rw-r--r--nova/api/openstack/extensions.py6
-rw-r--r--nova/tests/api/openstack/compute/plugins/v3/test_keypairs.py378
-rw-r--r--nova/tests/api/openstack/fakes.py24
-rw-r--r--nova/tests/fake_policy.py1
-rw-r--r--setup.cfg6
12 files changed, 2360 insertions, 11 deletions
diff --git a/etc/nova/policy.json b/etc/nova/policy.json
index 26a227ae2..cd6892596 100644
--- a/etc/nova/policy.json
+++ b/etc/nova/policy.json
@@ -74,6 +74,7 @@
"compute_extension:instance_actions:events": "rule:admin_api",
"compute_extension:instance_usage_audit_log": "rule:admin_api",
"compute_extension:keypairs": "",
+ "compute_extension:v3:os-keypairs": "",
"compute_extension:multinic": "",
"compute_extension:networks": "rule:admin_api",
"compute_extension:networks:view": "",
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 18c38a2fd..113354d04 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -222,17 +222,24 @@ class APIRouterV3(base_wsgi.Router):
"""Simple paste factory, :class:`nova.wsgi.Router` doesn't have one."""
return cls()
- def __init__(self):
+ def __init__(self, init_only=None):
# TODO(cyeoh): bp v3-api-extension-framework. Currently load
# all extensions but eventually should be able to exclude
# based on a config file
def _check_load_extension(ext):
- return isinstance(ext.obj, extensions.V3APIExtensionBase)
+ if (self.init_only is None or ext.obj.alias in
+ self.init_only) and isinstance(ext.obj,
+ extensions.V3APIExtensionBase):
+ return self._register_extension(ext)
+ else:
+ return False
+ self.init_only = init_only
self.api_extension_manager = stevedore.enabled.EnabledExtensionManager(
namespace=self.API_EXTENSION_NAMESPACE,
check_func=_check_load_extension,
- invoke_on_load=True)
+ invoke_on_load=True,
+ invoke_kwds={"extension_info": self.loaded_extension_info})
mapper = PlainMapper()
self.resources = {}
@@ -242,14 +249,17 @@ class APIRouterV3(base_wsgi.Router):
if list(self.api_extension_manager):
# NOTE(cyeoh): Stevedore raises an exception if there are
# no plugins detected. I wonder if this is a bug.
- self.api_extension_manager.map(self._register_extensions)
self.api_extension_manager.map(self._register_resources,
mapper=mapper)
self.api_extension_manager.map(self._register_controllers)
super(APIRouterV3, self).__init__(mapper)
- def _register_extensions(self, ext):
+ @property
+ def loaded_extension_info(self):
+ raise NotImplementedError
+
+ def _register_extension(self, ext):
raise NotImplementedError()
def _register_resources(self, ext, mapper):
@@ -281,7 +291,14 @@ class APIRouterV3(base_wsgi.Router):
if resource.parent:
kargs['parent_resource'] = resource.parent
- mapper.resource(resource.collection, resource.collection,
+ # non core-API plugins use the collection name as the
+ # member name, but the core-API plugins use the
+ # singular/plural convention for member/collection names
+ if resource.member_name:
+ member_name = resource.member_name
+ else:
+ member_name = resource.collection
+ mapper.resource(member_name, resource.collection,
**kargs)
if resource.custom_routes_fn:
diff --git a/nova/api/openstack/compute/__init__.py b/nova/api/openstack/compute/__init__.py
index 80247705f..e754bc011 100644
--- a/nova/api/openstack/compute/__init__.py
+++ b/nova/api/openstack/compute/__init__.py
@@ -30,6 +30,7 @@ from nova.api.openstack.compute import image_metadata
from nova.api.openstack.compute import images
from nova.api.openstack.compute import ips
from nova.api.openstack.compute import limits
+from nova.api.openstack.compute import plugins
from nova.api.openstack.compute import server_metadata
from nova.api.openstack.compute import servers
from nova.api.openstack.compute import versions
@@ -135,8 +136,13 @@ class APIRouterV3(nova.api.openstack.APIRouterV3):
Routes requests on the OpenStack API to the appropriate controller
and method.
"""
+ def __init__(self, init_only=None):
+ self._loaded_extension_info = plugins.LoadedExtensionInfo()
+ super(APIRouterV3, self).__init__(init_only)
- def _register_extensions(self, ext):
- pass
- # TODO(cyeoh): bp v3-api-extension-framework - Register extension
- # information
+ def _register_extension(self, ext):
+ return self.loaded_extension_info.register_extension(ext.obj)
+
+ @property
+ def loaded_extension_info(self):
+ return self._loaded_extension_info
diff --git a/nova/api/openstack/compute/plugins/__init__.py b/nova/api/openstack/compute/plugins/__init__.py
index e69de29bb..f93f56513 100644
--- a/nova/api/openstack/compute/plugins/__init__.py
+++ b/nova/api/openstack/compute/plugins/__init__.py
@@ -0,0 +1,59 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 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.
+
+
+from nova import exception
+from nova.openstack.common import log as logging
+
+LOG = logging.getLogger(__name__)
+
+
+class LoadedExtensionInfo(object):
+ """Keep track of all loaded API extensions."""
+
+ def __init__(self):
+ self.extensions = {}
+
+ def register_extension(self, ext):
+ if not self._check_extension(ext):
+ return False
+
+ alias = ext.alias
+ LOG.audit(_("Loaded extension %s"), alias)
+
+ if alias in self.extensions:
+ raise exception.NovaException("Found duplicate extension: %s"
+ % alias)
+ self.extensions[alias] = ext
+ return True
+
+ def _check_extension(self, extension):
+ """Checks for required methods in extension objects."""
+ try:
+ LOG.debug(_('Ext name: %s'), extension.name)
+ LOG.debug(_('Ext alias: %s'), extension.alias)
+ LOG.debug(_('Ext description: %s'),
+ ' '.join(extension.__doc__.strip().split()))
+ LOG.debug(_('Ext namespace: %s'), extension.namespace)
+ LOG.debug(_('Ext version: %i'), extension.version)
+ except AttributeError as ex:
+ LOG.exception(_("Exception loading extension: %s"), unicode(ex))
+ return False
+
+ return True
+
+ def get_extensions(self):
+ return self.extensions
diff --git a/nova/api/openstack/compute/plugins/v3/extension_info.py b/nova/api/openstack/compute/plugins/v3/extension_info.py
new file mode 100644
index 000000000..43b0551c7
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/extension_info.py
@@ -0,0 +1,105 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 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
+
+
+def make_ext(elem):
+ elem.set('name')
+ elem.set('namespace')
+ elem.set('alias')
+ elem.set('version')
+
+ desc = xmlutil.SubTemplateElement(elem, 'description')
+ desc.text = 'description'
+
+
+ext_nsmap = {None: xmlutil.XMLNS_COMMON_V10, 'atom': xmlutil.XMLNS_ATOM}
+
+
+class ExtensionTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('extension', selector='extension')
+ make_ext(root)
+ return xmlutil.MasterTemplate(root, 1, nsmap=ext_nsmap)
+
+
+class ExtensionsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('extensions')
+ elem = xmlutil.SubTemplateElement(root, 'extension',
+ selector='extensions')
+ make_ext(elem)
+ return xmlutil.MasterTemplate(root, 1, nsmap=ext_nsmap)
+
+
+class ExtensionInfoController(object):
+
+ def __init__(self, extension_info):
+ self.extension_info = extension_info
+
+ def _translate(self, ext):
+ ext_data = {}
+ ext_data['name'] = ext.name
+ ext_data['alias'] = ext.alias
+ ext_data['description'] = ext.__doc__
+ ext_data['namespace'] = ext.namespace
+ ext_data['version'] = ext.version
+ return ext_data
+
+ @wsgi.serializers(xml=ExtensionsTemplate)
+ def index(self, req):
+
+ sorted_ext_list = sorted(
+ self.extension_info.get_extensions().iteritems())
+
+ extensions = []
+ for _alias, ext in sorted_ext_list:
+ extensions.append(self._translate(ext))
+ return dict(extensions=extensions)
+
+ @wsgi.serializers(xml=ExtensionTemplate)
+ def show(self, req, id):
+ try:
+ # NOTE(dprince): the extensions alias is used as the 'id' for show
+ ext = self.extension_info.get_extensions()[id]
+ except KeyError:
+ raise webob.exc.HTTPNotFound()
+
+ return dict(extension=self._translate(ext))
+
+
+class ExtensionInfo(extensions.V3APIExtensionBase):
+ """Extension information."""
+
+ name = "extensions"
+ alias = "extensions"
+ namespace = "http://docs.openstack.org/compute/core/extension_info/api/v3"
+ version = 1
+
+ def get_resources(self):
+ resources = [
+ extensions.ResourceExtension(
+ 'extensions', ExtensionInfoController(self.extension_info),
+ member_name='extension')]
+ 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
new file mode 100644
index 000000000..4051a3497
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/keypairs.py
@@ -0,0 +1,215 @@
+# 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.
+
+"""Keypair management extension."""
+
+import webob
+import webob.exc
+
+from nova.api.openstack.compute import servers
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova.compute import api as compute_api
+from nova import exception
+
+
+ALIAS = 'os-keypairs'
+authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
+soft_authorize = extensions.soft_extension_authorizer('compute', 'v3:' + ALIAS)
+
+
+class KeypairTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ return xmlutil.MasterTemplate(xmlutil.make_flat_dict('keypair'), 1)
+
+
+class KeypairsTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('keypairs')
+ elem = xmlutil.make_flat_dict('keypair', selector='keypairs',
+ subselector='keypair')
+ root.append(elem)
+
+ return xmlutil.MasterTemplate(root, 1)
+
+
+class KeypairController(object):
+
+ """Keypair API controller for the OpenStack API."""
+ def __init__(self):
+ self.api = compute_api.KeypairAPI()
+
+ @wsgi.serializers(xml=KeypairTemplate)
+ def create(self, req, body):
+ """
+ Create or import keypair.
+
+ Sending name will generate a key and return private_key
+ and fingerprint.
+
+ You can send a public_key to add an existing ssh key
+
+ params: keypair object with:
+ name (required) - string
+ public_key (optional) - string
+ """
+
+ context = req.environ['nova.context']
+ authorize(context)
+
+ try:
+ params = body['keypair']
+ name = params['name']
+ except KeyError:
+ msg = _("Invalid request body")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ try:
+ if 'public_key' in params:
+ keypair = self.api.import_key_pair(context,
+ context.user_id, name,
+ params['public_key'])
+ else:
+ keypair = self.api.create_key_pair(context, context.user_id,
+ name)
+
+ return {'keypair': keypair}
+
+ except exception.KeypairLimitExceeded:
+ msg = _("Quota exceeded, too many key pairs.")
+ raise webob.exc.HTTPRequestEntityTooLarge(
+ explanation=msg,
+ headers={'Retry-After': 0})
+ except exception.InvalidKeypair:
+ msg = _("Keypair data is invalid")
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+ except exception.KeyPairExists:
+ msg = _("Key pair '%s' already exists.") % name
+ raise webob.exc.HTTPConflict(explanation=msg)
+
+ def delete(self, req, id):
+ """
+ Delete a keypair with a given name
+ """
+ context = req.environ['nova.context']
+ authorize(context)
+ try:
+ self.api.delete_key_pair(context, context.user_id, id)
+ except exception.KeypairNotFound:
+ raise webob.exc.HTTPNotFound()
+ return webob.Response(status_int=202)
+
+ @wsgi.serializers(xml=KeypairTemplate)
+ def show(self, req, id):
+ """Return data for the given key name."""
+ context = req.environ['nova.context']
+ authorize(context)
+
+ try:
+ keypair = self.api.get_key_pair(context, context.user_id, id)
+ except exception.KeypairNotFound:
+ raise webob.exc.HTTPNotFound()
+ return {'keypair': keypair}
+
+ @wsgi.serializers(xml=KeypairsTemplate)
+ def index(self, req):
+ """
+ List of keypairs for a user
+ """
+ context = req.environ['nova.context']
+ authorize(context)
+ key_pairs = self.api.get_key_pairs(context, context.user_id)
+ rval = []
+ for key_pair in key_pairs:
+ rval.append({'keypair': {
+ 'name': key_pair['name'],
+ 'public_key': key_pair['public_key'],
+ 'fingerprint': key_pair['fingerprint'],
+ }})
+
+ return {'keypairs': rval}
+
+
+class ServerKeyNameTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('server')
+ root.set('key_name', 'key_name')
+ return xmlutil.SlaveTemplate(root, 1)
+
+
+class ServersKeyNameTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('servers')
+ elem = xmlutil.SubTemplateElement(root, 'server', selector='servers')
+ elem.set('key_name', 'key_name')
+ return xmlutil.SlaveTemplate(root, 1)
+
+
+class Controller(servers.Controller):
+
+ def _add_key_name(self, req, servers):
+ for server in servers:
+ db_server = req.get_db_instance(server['id'])
+ # server['id'] is guaranteed to be in the cache due to
+ # the core API adding it in its 'show'/'detail' methods.
+ server['key_name'] = db_server['key_name']
+
+ def _show(self, req, resp_obj):
+ if 'server' in resp_obj.obj:
+ resp_obj.attach(xml=ServerKeyNameTemplate())
+ server = resp_obj.obj['server']
+ self._add_key_name(req, [server])
+
+ @wsgi.extends
+ def show(self, req, resp_obj, id):
+ context = req.environ['nova.context']
+ if soft_authorize(context):
+ self._show(req, resp_obj)
+
+ @wsgi.extends
+ def detail(self, req, resp_obj):
+ context = req.environ['nova.context']
+ if 'servers' in resp_obj.obj and soft_authorize(context):
+ resp_obj.attach(xml=ServersKeyNameTemplate())
+ servers = resp_obj.obj['servers']
+ self._add_key_name(req, servers)
+
+
+class Keypairs(extensions.V3APIExtensionBase):
+ """Keypair Support."""
+
+ name = "Keypairs"
+ alias = ALIAS
+ namespace = "http://docs.openstack.org/compute/ext/keypairs/api/v3"
+ version = 1
+
+ def get_resources(self):
+ resources = [
+ extensions.ResourceExtension('os-keypairs',
+ KeypairController())]
+ return resources
+
+ def get_controller_extensions(self):
+ controller = Controller()
+ extension = extensions.ControllerExtension(self, 'servers', controller)
+ return [extension]
+
+ # use nova.api.extensions.server.extensions entry point to modify
+ # server create kwargs
+ def server_create(self, server_dict, create_kwargs):
+ create_kwargs['key_name'] = server_dict.get('key_name')
diff --git a/nova/api/openstack/compute/plugins/v3/servers.py b/nova/api/openstack/compute/plugins/v3/servers.py
new file mode 100644
index 000000000..2475af0e4
--- /dev/null
+++ b/nova/api/openstack/compute/plugins/v3/servers.py
@@ -0,0 +1,1533 @@
+# Copyright 2010 OpenStack Foundation
+# Copyright 2011 Piston Cloud Computing, Inc
+# 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 base64
+import os
+import re
+import stevedore
+
+from oslo.config import cfg
+import webob
+from webob import exc
+
+from nova.api.openstack import common
+from nova.api.openstack.compute import ips
+from nova.api.openstack.compute.views import servers as views_servers
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova import compute
+from nova.compute import flavors
+from nova import exception
+from nova.openstack.common import importutils
+from nova.openstack.common import log as logging
+from nova.openstack.common.rpc import common as rpc_common
+from nova.openstack.common import strutils
+from nova.openstack.common import timeutils
+from nova.openstack.common import uuidutils
+from nova import utils
+
+
+server_opts = [
+ cfg.BoolOpt('enable_instance_password',
+ default=True,
+ help='Allows use of instance password during '
+ 'server creation'),
+]
+CONF = cfg.CONF
+CONF.register_opts(server_opts)
+CONF.import_opt('network_api_class', 'nova.network')
+CONF.import_opt('reclaim_instance_interval', 'nova.compute.manager')
+
+LOG = logging.getLogger(__name__)
+
+
+def make_fault(elem):
+ fault = xmlutil.SubTemplateElement(elem, 'fault', selector='fault')
+ fault.set('code')
+ fault.set('created')
+ msg = xmlutil.SubTemplateElement(fault, 'message')
+ msg.text = 'message'
+ det = xmlutil.SubTemplateElement(fault, 'details')
+ det.text = 'details'
+
+
+def make_server(elem, detailed=False):
+ elem.set('name')
+ elem.set('id')
+
+ if detailed:
+ elem.set('userId', 'user_id')
+ elem.set('tenantId', 'tenant_id')
+ elem.set('updated')
+ elem.set('created')
+ elem.set('hostId')
+ elem.set('accessIPv4')
+ elem.set('accessIPv6')
+ elem.set('status')
+ elem.set('progress')
+ elem.set('reservation_id')
+
+ # Attach image node
+ image = xmlutil.SubTemplateElement(elem, 'image', selector='image')
+ image.set('id')
+ xmlutil.make_links(image, 'links')
+
+ # Attach flavor node
+ flavor = xmlutil.SubTemplateElement(elem, 'flavor', selector='flavor')
+ flavor.set('id')
+ xmlutil.make_links(flavor, 'links')
+
+ # Attach fault node
+ make_fault(elem)
+
+ # Attach metadata node
+ elem.append(common.MetadataTemplate())
+
+ # Attach addresses node
+ elem.append(ips.AddressesTemplate())
+
+ xmlutil.make_links(elem, 'links')
+
+
+server_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM}
+
+
+class ServerTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('server', selector='server')
+ make_server(root, detailed=True)
+ return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap)
+
+
+class MinimalServersTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('servers')
+ elem = xmlutil.SubTemplateElement(root, 'server', selector='servers')
+ make_server(elem)
+ xmlutil.make_links(root, 'servers_links')
+ return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap)
+
+
+class ServersTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('servers')
+ elem = xmlutil.SubTemplateElement(root, 'server', selector='servers')
+ make_server(elem, detailed=True)
+ return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap)
+
+
+class ServerAdminPassTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('server')
+ root.set('adminPass')
+ return xmlutil.SlaveTemplate(root, 1, nsmap=server_nsmap)
+
+
+class ServerMultipleCreateTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('server')
+ root.set('reservation_id')
+ return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap)
+
+
+def FullServerTemplate():
+ master = ServerTemplate()
+ master.attach(ServerAdminPassTemplate())
+ return master
+
+
+class CommonDeserializer(wsgi.MetadataXMLDeserializer):
+ """Common deserializer to handle xml-formatted server create requests.
+
+ Handles standard server attributes as well as optional metadata
+ and personality attributes
+ """
+
+ metadata_deserializer = common.MetadataXMLDeserializer()
+
+ def _extract_personality(self, server_node):
+ """Marshal the personality attribute of a parsed request."""
+ node = self.find_first_child_named(server_node, "personality")
+ if node is not None:
+ personality = []
+ for file_node in self.find_children_named(node, "file"):
+ item = {}
+ if file_node.hasAttribute("path"):
+ item["path"] = file_node.getAttribute("path")
+ item["contents"] = self.extract_text(file_node)
+ personality.append(item)
+ return personality
+ else:
+ return None
+
+ def _extract_server(self, node):
+ """Marshal the server attribute of a parsed request."""
+ server = {}
+ server_node = self.find_first_child_named(node, 'server')
+
+ attributes = ["name", "imageRef", "flavorRef", "adminPass",
+ "accessIPv4", "accessIPv6", "key_name",
+ "availability_zone", "min_count", "max_count"]
+ for attr in attributes:
+ if server_node.getAttribute(attr):
+ server[attr] = server_node.getAttribute(attr)
+
+ res_id = server_node.getAttribute('return_reservation_id')
+ if res_id:
+ server['return_reservation_id'] = strutils.bool_from_string(res_id)
+
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an entry point when the OS-SCH-HNT extension is
+ # ported
+ #scheduler_hints = self._extract_scheduler_hints(server_node)
+ #if scheduler_hints:
+ # server['OS-SCH-HNT:scheduler_hints'] = scheduler_hints
+
+ metadata_node = self.find_first_child_named(server_node, "metadata")
+ if metadata_node is not None:
+ server["metadata"] = self.extract_metadata(metadata_node)
+
+ user_data_node = self.find_first_child_named(server_node, "user_data")
+ if user_data_node is not None:
+ server["user_data"] = self.extract_text(user_data_node)
+
+ personality = self._extract_personality(server_node)
+ if personality is not None:
+ server["personality"] = personality
+
+ networks = self._extract_networks(server_node)
+ if networks is not None:
+ server["networks"] = networks
+
+ security_groups = self._extract_security_groups(server_node)
+ if security_groups is not None:
+ server["security_groups"] = security_groups
+
+ # NOTE(vish): this is not namespaced in json, so leave it without a
+ # namespace for now
+ block_device_mapping = self._extract_block_device_mapping(server_node)
+ if block_device_mapping is not None:
+ server["block_device_mapping"] = block_device_mapping
+
+ # NOTE(vish): Support this incorrect version because it was in the code
+ # base for a while and we don't want to accidentally break
+ # anyone that might be using it.
+
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an extension point when the disk_config
+ # extension is ported
+ # auto_disk_config = server_node.getAttribute('auto_disk_config')
+ #if auto_disk_config:
+ # server['OS-DCF:diskConfig'] = auto_disk_config
+
+ # auto_disk_config = server_node.getAttribute('OS-DCF:diskConfig')
+ #if auto_disk_config:
+ # server['OS-DCF:diskConfig'] = auto_disk_config
+
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an extension point when the disk_config
+ # extension is ported
+ #config_drive = server_node.getAttribute('config_drive')
+ #if config_drive:
+ # server['config_drive'] = config_drive
+
+ return server
+
+ def _extract_block_device_mapping(self, server_node):
+ """Marshal the block_device_mapping node of a parsed request."""
+ node = self.find_first_child_named(server_node, "block_device_mapping")
+ if node:
+ block_device_mapping = []
+ for child in self.extract_elements(node):
+ if child.nodeName != "mapping":
+ continue
+ mapping = {}
+ attributes = ["volume_id", "snapshot_id", "device_name",
+ "virtual_name", "volume_size"]
+ for attr in attributes:
+ value = child.getAttribute(attr)
+ if value:
+ mapping[attr] = value
+ attributes = ["delete_on_termination", "no_device"]
+ for attr in attributes:
+ value = child.getAttribute(attr)
+ if value:
+ mapping[attr] = strutils.bool_from_string(value)
+ block_device_mapping.append(mapping)
+ return block_device_mapping
+ else:
+ return None
+
+ def _extract_scheduler_hints(self, server_node):
+ """Marshal the scheduler hints attribute of a parsed request."""
+ node = self.find_first_child_named_in_namespace(server_node,
+ "http://docs.openstack.org/compute/ext/scheduler-hints/api/v2",
+ "scheduler_hints")
+ if node:
+ scheduler_hints = {}
+ for child in self.extract_elements(node):
+ scheduler_hints.setdefault(child.nodeName, [])
+ value = self.extract_text(child).strip()
+ scheduler_hints[child.nodeName].append(value)
+ return scheduler_hints
+ else:
+ return None
+
+ def _extract_networks(self, server_node):
+ """Marshal the networks attribute of a parsed request."""
+ node = self.find_first_child_named(server_node, "networks")
+ if node is not None:
+ networks = []
+ for network_node in self.find_children_named(node,
+ "network"):
+ item = {}
+ if network_node.hasAttribute("uuid"):
+ item["uuid"] = network_node.getAttribute("uuid")
+ if network_node.hasAttribute("fixed_ip"):
+ item["fixed_ip"] = network_node.getAttribute("fixed_ip")
+ if network_node.hasAttribute("port"):
+ item["port"] = network_node.getAttribute("port")
+ networks.append(item)
+ return networks
+ else:
+ return None
+
+ def _extract_security_groups(self, server_node):
+ """Marshal the security_groups attribute of a parsed request."""
+ node = self.find_first_child_named(server_node, "security_groups")
+ if node is not None:
+ security_groups = []
+ for sg_node in self.find_children_named(node, "security_group"):
+ item = {}
+ name = self.find_attribute_or_element(sg_node, 'name')
+ if name:
+ item["name"] = name
+ security_groups.append(item)
+ return security_groups
+ else:
+ return None
+
+
+class ActionDeserializer(CommonDeserializer):
+ """Deserializer to handle xml-formatted server action requests.
+
+ Handles standard server attributes as well as optional metadata
+ and personality attributes
+ """
+
+ def default(self, string):
+ dom = xmlutil.safe_minidom_parse_string(string)
+ action_node = dom.childNodes[0]
+ action_name = action_node.tagName
+
+ action_deserializer = {
+ 'createImage': self._action_create_image,
+ 'changePassword': self._action_change_password,
+ 'reboot': self._action_reboot,
+ 'rebuild': self._action_rebuild,
+ 'resize': self._action_resize,
+ 'confirmResize': self._action_confirm_resize,
+ 'revertResize': self._action_revert_resize,
+ }.get(action_name, super(ActionDeserializer, self).default)
+
+ action_data = action_deserializer(action_node)
+
+ return {'body': {action_name: action_data}}
+
+ def _action_create_image(self, node):
+ return self._deserialize_image_action(node, ('name',))
+
+ def _action_change_password(self, node):
+ if not node.hasAttribute("adminPass"):
+ raise AttributeError("No adminPass was specified in request")
+ return {"adminPass": node.getAttribute("adminPass")}
+
+ def _action_reboot(self, node):
+ if not node.hasAttribute("type"):
+ raise AttributeError("No reboot type was specified in request")
+ return {"type": node.getAttribute("type")}
+
+ def _action_rebuild(self, node):
+ rebuild = {}
+ if node.hasAttribute("name"):
+ name = node.getAttribute("name")
+ if not name:
+ raise AttributeError("Name cannot be blank")
+ rebuild['name'] = name
+
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an extension point when the disk_config
+ # extension is ported
+ # if node.hasAttribute("auto_disk_config"):
+ # rebuild['OS-DCF:diskConfig'] = node.getAttribute(
+ # "auto_disk_config")
+
+ # if node.hasAttribute("OS-DCF:diskConfig"):
+ # rebuild['OS-DCF:diskConfig'] = node.getAttribute(
+ # "OS-DCF:diskConfig")
+
+ metadata_node = self.find_first_child_named(node, "metadata")
+ if metadata_node is not None:
+ rebuild["metadata"] = self.extract_metadata(metadata_node)
+
+ personality = self._extract_personality(node)
+ if personality is not None:
+ rebuild["personality"] = personality
+
+ if not node.hasAttribute("imageRef"):
+ raise AttributeError("No imageRef was specified in request")
+ rebuild["imageRef"] = node.getAttribute("imageRef")
+
+ if node.hasAttribute("adminPass"):
+ rebuild["adminPass"] = node.getAttribute("adminPass")
+
+ if node.hasAttribute("accessIPv4"):
+ rebuild["accessIPv4"] = node.getAttribute("accessIPv4")
+
+ if node.hasAttribute("accessIPv6"):
+ rebuild["accessIPv6"] = node.getAttribute("accessIPv6")
+
+ return rebuild
+
+ def _action_resize(self, node):
+ resize = {}
+
+ if node.hasAttribute("flavorRef"):
+ resize["flavorRef"] = node.getAttribute("flavorRef")
+ else:
+ raise AttributeError("No flavorRef was specified in request")
+
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an extension point when the disk_config
+ # extension is ported
+ # if node.hasAttribute("auto_disk_config"):
+ # resize['OS-DCF:diskConfig'] = node.getAttribute(
+ # "auto_disk_config")
+
+ # if node.hasAttribute("OS-DCF:diskConfig"):
+ # resize['OS-DCF:diskConfig'] = node.getAttribute(
+ # "OS-DCF:diskConfig")
+
+ return resize
+
+ def _action_confirm_resize(self, node):
+ return None
+
+ def _action_revert_resize(self, node):
+ return None
+
+ def _deserialize_image_action(self, node, allowed_attributes):
+ data = {}
+ for attribute in allowed_attributes:
+ value = node.getAttribute(attribute)
+ if value:
+ data[attribute] = value
+ metadata_node = self.find_first_child_named(node, 'metadata')
+ if metadata_node is not None:
+ metadata = self.metadata_deserializer.extract_metadata(
+ metadata_node)
+ data['metadata'] = metadata
+ return data
+
+
+class CreateDeserializer(CommonDeserializer):
+ """Deserializer to handle xml-formatted server create requests.
+
+ Handles standard server attributes as well as optional metadata
+ and personality attributes
+ """
+
+ def default(self, string):
+ """Deserialize an xml-formatted server create request."""
+ dom = xmlutil.safe_minidom_parse_string(string)
+ server = self._extract_server(dom)
+ return {'body': {'server': server}}
+
+
+class ServersController(wsgi.Controller):
+ """The Server API base controller class for the OpenStack API."""
+
+ EXTENSION_CREATE_NAMESPACE = 'nova.api.v3.extensions.server.create'
+ _view_builder_class = views_servers.ViewBuilder
+
+ @staticmethod
+ def _add_location(robj):
+ # Just in case...
+ if 'server' not in robj.obj:
+ return robj
+
+ link = filter(lambda l: l['rel'] == 'self',
+ robj.obj['server']['links'])
+ if link:
+ robj['Location'] = link[0]['href'].encode('utf-8')
+
+ # Convenience return
+ return robj
+
+ def __init__(self, **kwargs):
+ def _check_load_extension(ext):
+ if isinstance(ext.obj, extensions.V3APIExtensionBase):
+ # Filter out for the existence of server_create here
+ # rather than on every request. We don't have a new
+ # abstract base class to reduce duplication in the
+ # extensions as they may want to implement multiple
+ # server (and other) entry points
+ if hasattr(ext.obj, 'server_create'):
+ LOG.debug(_('server create extension %s detected'),
+ ext.obj.alias)
+ return True
+ else:
+ LOG.debug(
+ _('extension %s is missing server_create'), ext.obj)
+ return False
+ else:
+ return False
+
+ self.extension_info = kwargs.pop('extension_info')
+ super(ServersController, self).__init__(**kwargs)
+ self.compute_api = compute.API()
+ self.quantum_attempted = False
+
+ # Look for implmentation of extension point of server creation
+ self.create_extension_manager = \
+ stevedore.enabled.EnabledExtensionManager(
+ namespace=self.EXTENSION_CREATE_NAMESPACE,
+ check_func=_check_load_extension,
+ invoke_on_load=True,
+ invoke_kwds={"extension_info": self.extension_info})
+ if not list(self.create_extension_manager):
+ LOG.debug(_("Did not find any server create extensions"))
+
+ @wsgi.serializers(xml=MinimalServersTemplate)
+ def index(self, req):
+ """Returns a list of server names and ids for a given user."""
+ try:
+ servers = self._get_servers(req, is_detail=False)
+ except exception.Invalid as err:
+ raise exc.HTTPBadRequest(explanation=str(err))
+ return servers
+
+ @wsgi.serializers(xml=ServersTemplate)
+ def detail(self, req):
+ """Returns a list of server details for a given user."""
+ try:
+ servers = self._get_servers(req, is_detail=True)
+ except exception.Invalid as err:
+ raise exc.HTTPBadRequest(explanation=str(err))
+ return servers
+
+ def _add_instance_faults(self, ctxt, instances):
+ faults = self.compute_api.get_instance_faults(ctxt, instances)
+ if faults is not None:
+ for instance in instances:
+ faults_list = faults.get(instance['uuid'], [])
+ try:
+ instance['fault'] = faults_list[0]
+ except IndexError:
+ pass
+
+ return instances
+
+ def _get_servers(self, req, is_detail):
+ """Returns a list of servers, based on any search options specified."""
+
+ search_opts = {}
+ search_opts.update(req.GET)
+
+ context = req.environ['nova.context']
+ remove_invalid_options(context, search_opts,
+ self._get_server_search_options())
+
+ # Verify search by 'status' contains a valid status.
+ # Convert it to filter by vm_state for compute_api.
+ status = search_opts.pop('status', None)
+ if status is not None:
+ state = common.vm_state_from_status(status)
+ if state is None:
+ return {'servers': []}
+ search_opts['vm_state'] = state
+
+ if 'changes-since' in search_opts:
+ try:
+ parsed = timeutils.parse_isotime(search_opts['changes-since'])
+ except ValueError:
+ msg = _('Invalid changes-since value')
+ raise exc.HTTPBadRequest(explanation=msg)
+ search_opts['changes-since'] = parsed
+
+ # By default, compute's get_all() will return deleted instances.
+ # If an admin hasn't specified a 'deleted' search option, we need
+ # to filter out deleted instances by setting the filter ourselves.
+ # ... Unless 'changes-since' is specified, because 'changes-since'
+ # should return recently deleted images according to the API spec.
+
+ if 'deleted' not in search_opts:
+ if 'changes-since' not in search_opts:
+ # No 'changes-since', so we only want non-deleted servers
+ search_opts['deleted'] = False
+
+ if search_opts.get("vm_state") == "deleted":
+ if context.is_admin:
+ search_opts['deleted'] = True
+ else:
+ msg = _("Only administrators may list deleted instances")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ if 'all_tenants' not in search_opts:
+ if context.project_id:
+ search_opts['project_id'] = context.project_id
+ else:
+ search_opts['user_id'] = context.user_id
+
+ limit, marker = common.get_limit_and_marker(req)
+ try:
+ instance_list = self.compute_api.get_all(context,
+ search_opts=search_opts,
+ limit=limit,
+ marker=marker)
+ except exception.MarkerNotFound as e:
+ msg = _('marker [%s] not found') % marker
+ raise exc.HTTPBadRequest(explanation=msg)
+ except exception.FlavorNotFound as e:
+ log_msg = _("Flavor '%s' could not be found ")
+ LOG.debug(log_msg, search_opts['flavor'])
+ instance_list = []
+
+ if is_detail:
+ self._add_instance_faults(context, instance_list)
+ response = self._view_builder.detail(req, instance_list)
+ else:
+ response = self._view_builder.index(req, instance_list)
+ req.cache_db_instances(instance_list)
+ return response
+
+ def _get_server(self, context, req, instance_uuid):
+ """Utility function for looking up an instance by uuid."""
+ try:
+ instance = self.compute_api.get(context, instance_uuid)
+ except exception.NotFound:
+ msg = _("Instance could not be found")
+ raise exc.HTTPNotFound(explanation=msg)
+ req.cache_db_instance(instance)
+ return instance
+
+ def _check_string_length(self, value, name, max_length=None):
+ try:
+ utils.check_string_length(value, name, min_length=1,
+ max_length=max_length)
+ except exception.InvalidInput as e:
+ raise exc.HTTPBadRequest(explanation=str(e))
+
+ def _validate_server_name(self, value):
+ self._check_string_length(value, 'Server name', max_length=255)
+
+ def _validate_device_name(self, value):
+ self._check_string_length(value, 'Device name', max_length=255)
+
+ if ' ' in value:
+ msg = _("Device name cannot include spaces.")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ def _get_injected_files(self, personality):
+ """Create a list of injected files from the personality attribute.
+
+ At this time, injected_files must be formatted as a list of
+ (file_path, file_content) pairs for compatibility with the
+ underlying compute service.
+ """
+ injected_files = []
+
+ for item in personality:
+ try:
+ path = item['path']
+ contents = item['contents']
+ except KeyError as key:
+ expl = _('Bad personality format: missing %s') % key
+ raise exc.HTTPBadRequest(explanation=expl)
+ except TypeError:
+ expl = _('Bad personality format')
+ raise exc.HTTPBadRequest(explanation=expl)
+ if self._decode_base64(contents) is None:
+ expl = _('Personality content for %s cannot be decoded') % path
+ raise exc.HTTPBadRequest(explanation=expl)
+ injected_files.append((path, contents))
+ return injected_files
+
+ def _is_quantum_v2(self):
+ # NOTE(dprince): quantumclient is not a requirement
+ if self.quantum_attempted:
+ return self.have_quantum
+
+ try:
+ self.quantum_attempted = True
+ from nova.network.quantumv2 import api as quantum_api
+ self.have_quantum = issubclass(
+ importutils.import_class(CONF.network_api_class),
+ quantum_api.API)
+ except ImportError:
+ self.have_quantum = False
+
+ return self.have_quantum
+
+ def _get_requested_networks(self, requested_networks):
+ """Create a list of requested networks from the networks attribute."""
+ networks = []
+ for network in requested_networks:
+ try:
+ port_id = network.get('port', None)
+ if port_id:
+ network_uuid = None
+ if not self._is_quantum_v2():
+ # port parameter is only for quantum v2.0
+ msg = _("Unknown argment : port")
+ raise exc.HTTPBadRequest(explanation=msg)
+ if not uuidutils.is_uuid_like(port_id):
+ msg = _("Bad port format: port uuid is "
+ "not in proper format "
+ "(%s)") % port_id
+ raise exc.HTTPBadRequest(explanation=msg)
+ else:
+ network_uuid = network['uuid']
+
+ if not port_id and not uuidutils.is_uuid_like(network_uuid):
+ br_uuid = network_uuid.split('-', 1)[-1]
+ if not uuidutils.is_uuid_like(br_uuid):
+ msg = _("Bad networks format: network uuid is "
+ "not in proper format "
+ "(%s)") % network_uuid
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ #fixed IP address is optional
+ #if the fixed IP address is not provided then
+ #it will use one of the available IP address from the network
+ address = network.get('fixed_ip', None)
+ if address is not None and not utils.is_valid_ipv4(address):
+ msg = _("Invalid fixed IP address (%s)") % address
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ # For quantumv2, requestd_networks
+ # should be tuple of (network_uuid, fixed_ip, port_id)
+ if self._is_quantum_v2():
+ networks.append((network_uuid, address, port_id))
+ else:
+ # check if the network id is already present in the list,
+ # we don't want duplicate networks to be passed
+ # at the boot time
+ for id, ip in networks:
+ if id == network_uuid:
+ expl = (_("Duplicate networks"
+ " (%s) are not allowed") %
+ network_uuid)
+ raise exc.HTTPBadRequest(explanation=expl)
+ networks.append((network_uuid, address))
+ except KeyError as key:
+ expl = _('Bad network format: missing %s') % key
+ raise exc.HTTPBadRequest(explanation=expl)
+ except TypeError:
+ expl = _('Bad networks format')
+ raise exc.HTTPBadRequest(explanation=expl)
+
+ return networks
+
+ # NOTE(vish): Without this regex, b64decode will happily
+ # ignore illegal bytes in the base64 encoded
+ # data.
+ B64_REGEX = re.compile('^(?:[A-Za-z0-9+\/]{4})*'
+ '(?:[A-Za-z0-9+\/]{2}=='
+ '|[A-Za-z0-9+\/]{3}=)?$')
+
+ def _decode_base64(self, data):
+ data = re.sub(r'\s', '', data)
+ if not self.B64_REGEX.match(data):
+ return None
+ try:
+ return base64.b64decode(data)
+ except TypeError:
+ return None
+
+ def _validate_user_data(self, user_data):
+ """Check if the user_data is encoded properly."""
+ if not user_data:
+ return
+ if self._decode_base64(user_data) is None:
+ expl = _('Userdata content cannot be decoded')
+ raise exc.HTTPBadRequest(explanation=expl)
+
+ def _validate_access_ipv4(self, address):
+ if not utils.is_valid_ipv4(address):
+ expl = _('accessIPv4 is not proper IPv4 format')
+ raise exc.HTTPBadRequest(explanation=expl)
+
+ def _validate_access_ipv6(self, address):
+ if not utils.is_valid_ipv6(address):
+ expl = _('accessIPv6 is not proper IPv6 format')
+ raise exc.HTTPBadRequest(explanation=expl)
+
+ @wsgi.serializers(xml=ServerTemplate)
+ def show(self, req, id):
+ """Returns server details by server id."""
+ try:
+ context = req.environ['nova.context']
+ instance = self.compute_api.get(context, id)
+ req.cache_db_instance(instance)
+ self._add_instance_faults(context, [instance])
+ return self._view_builder.show(req, instance)
+ except exception.NotFound:
+ msg = _("Instance could not be found")
+ raise exc.HTTPNotFound(explanation=msg)
+
+ @wsgi.response(202)
+ @wsgi.serializers(xml=FullServerTemplate)
+ @wsgi.deserializers(xml=CreateDeserializer)
+ def create(self, req, body):
+ """Creates a new server for a given user."""
+ if not self.is_valid_body(body, 'server'):
+ raise exc.HTTPUnprocessableEntity()
+
+ context = req.environ['nova.context']
+ server_dict = body['server']
+ password = self._get_server_admin_password(server_dict)
+
+ if 'name' not in server_dict:
+ msg = _("Server name is not defined")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ name = server_dict['name']
+ self._validate_server_name(name)
+ name = name.strip()
+
+ image_uuid = self._image_from_req_data(body)
+
+ # Arguments to be passed to instance create function
+ create_kwargs = {}
+
+ # Query extensions which want to manipulate the keyword
+ # arguments.
+ # NOTE(cyeoh): This is the hook that extensions use
+ # to replace the extension specific code below.
+ # When the extensions are ported this will also result
+ # in some convenience function from this class being
+ # moved to the extension
+ if list(self.create_extension_manager):
+ self.create_extension_manager.map(self._create_extension_point,
+ server_dict, create_kwargs)
+
+ personality = server_dict.get('personality')
+ config_drive = None
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an extension point when the config_drive
+ # extension is ported
+ # if self.ext_mgr.is_loaded('os-config-drive'):
+ # config_drive = server_dict.get('config_drive')
+
+ injected_files = []
+ if personality:
+ injected_files = self._get_injected_files(personality)
+
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an extension point when the security groups
+ # extension is ported
+ sg_names = []
+ #if self.ext_mgr.is_loaded('os-security-groups'):
+ # security_groups = server_dict.get('security_groups')
+ # if security_groups is not None:
+ # sg_names = [sg['name'] for sg in security_groups
+ # if sg.get('name')]
+ if not sg_names:
+ sg_names.append('default')
+
+ sg_names = list(set(sg_names))
+
+ requested_networks = None
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an extension point when the os-networks
+ # extension is ported. Currently reworked
+ # to take into account _is_quantum_v2
+ #if (self.ext_mgr.is_loaded('os-networks')
+ # or self._is_quantum_v2()):
+ # requested_networks = server_dict.get('networks')
+
+ if self._is_quantum_v2():
+ requested_networks = server_dict.get('networks')
+ if requested_networks is not None:
+ requested_networks = self._get_requested_networks(
+ requested_networks)
+
+ (access_ip_v4, ) = server_dict.get('accessIPv4'),
+ if access_ip_v4 is not None:
+ self._validate_access_ipv4(access_ip_v4)
+
+ (access_ip_v6, ) = server_dict.get('accessIPv6'),
+ if access_ip_v6 is not None:
+ self._validate_access_ipv6(access_ip_v6)
+
+ try:
+ flavor_id = self._flavor_id_from_req_data(body)
+ except ValueError as error:
+ msg = _("Invalid flavorRef provided.")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ user_data = None
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an extension point when the os-user-data
+ # extension is ported
+ #if self.ext_mgr.is_loaded('os-user-data'):
+ # user_data = server_dict.get('user_data')
+ #self._validate_user_data(user_data)
+
+ availability_zone = None
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an extension point when the
+ # os-availability-zone extension is ported
+ #if self.ext_mgr.is_loaded('os-availability-zone'):
+ # availability_zone = server_dict.get('availability_zone')
+
+ block_device_mapping = None
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an extension point when the os-volumes
+ # extension is ported
+ #if self.ext_mgr.is_loaded('os-volumes'):
+ # block_device_mapping = server_dict.get('block_device_mapping', [])
+ # for bdm in block_device_mapping:
+ # self._validate_device_name(bdm["device_name"])
+ # if 'delete_on_termination' in bdm:
+ # bdm['delete_on_termination'] = strutils.bool_from_string(
+ # bdm['delete_on_termination'])
+
+ ret_resv_id = False
+ # min_count and max_count are optional. If they exist, they may come
+ # in as strings. Verify that they are valid integers and > 0.
+ # Also, we want to default 'min_count' to 1, and default
+ # 'max_count' to be 'min_count'.
+ min_count = 1
+ max_count = 1
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an extension point when the os-multiple-create
+ # extension is ported
+ #if self.ext_mgr.is_loaded('os-multiple-create'):
+ # ret_resv_id = server_dict.get('return_reservation_id', False)
+ # min_count = server_dict.get('min_count', 1)
+ # max_count = server_dict.get('max_count', min_count)
+
+ try:
+ min_count = int(str(min_count))
+ except ValueError:
+ msg = _('min_count must be an integer value')
+ raise exc.HTTPBadRequest(explanation=msg)
+ if min_count < 1:
+ msg = _('min_count must be > 0')
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ try:
+ max_count = int(str(max_count))
+ except ValueError:
+ msg = _('max_count must be an integer value')
+ raise exc.HTTPBadRequest(explanation=msg)
+ if max_count < 1:
+ msg = _('max_count must be > 0')
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ if min_count > max_count:
+ msg = _('min_count must be <= max_count')
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ auto_disk_config = False
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an extension point when the OS-DCF extension is
+ # ported
+ #if self.ext_mgr.is_loaded('OS-DCF'):
+ # auto_disk_config = server_dict.get('auto_disk_config')
+
+ scheduler_hints = {}
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with an extension point when the scheduler hints
+ # extension is ported
+ #if self.ext_mgr.is_loaded('OS-SCH-HNT'):
+ # scheduler_hints = server_dict.get('scheduler_hints', {})
+
+ try:
+ _get_inst_type = flavors.get_instance_type_by_flavor_id
+ inst_type = _get_inst_type(flavor_id, read_deleted="no")
+
+ (instances, resv_id) = self.compute_api.create(context,
+ inst_type,
+ image_uuid,
+ display_name=name,
+ display_description=name,
+ metadata=server_dict.get('metadata', {}),
+ access_ip_v4=access_ip_v4,
+ access_ip_v6=access_ip_v6,
+ injected_files=injected_files,
+ admin_password=password,
+ min_count=min_count,
+ max_count=max_count,
+ requested_networks=requested_networks,
+ security_group=sg_names,
+ user_data=user_data,
+ availability_zone=availability_zone,
+ config_drive=config_drive,
+ block_device_mapping=block_device_mapping,
+ auto_disk_config=auto_disk_config,
+ scheduler_hints=scheduler_hints,
+ **create_kwargs)
+ except exception.QuotaError as error:
+ raise exc.HTTPRequestEntityTooLarge(
+ explanation=error.format_message(),
+ headers={'Retry-After': 0})
+ except exception.InvalidMetadataSize as error:
+ raise exc.HTTPRequestEntityTooLarge(
+ explanation=error.format_message())
+ except exception.ImageNotFound as error:
+ msg = _("Can not find requested image")
+ raise exc.HTTPBadRequest(explanation=msg)
+ except exception.FlavorNotFound as error:
+ msg = _("Invalid flavorRef provided.")
+ raise exc.HTTPBadRequest(explanation=msg)
+ except exception.KeypairNotFound as error:
+ msg = _("Invalid key_name 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}
+ raise exc.HTTPBadRequest(explanation=msg)
+ except UnicodeDecodeError as error:
+ msg = "UnicodeError: %s" % unicode(error)
+ raise exc.HTTPBadRequest(explanation=msg)
+ except (exception.ImageNotActive,
+ exception.InstanceTypeDiskTooSmall,
+ exception.InstanceTypeMemoryTooSmall,
+ exception.InstanceTypeNotFound,
+ exception.InvalidMetadata,
+ exception.InvalidRequest,
+ exception.SecurityGroupNotFound) as error:
+ raise exc.HTTPBadRequest(explanation=error.format_message())
+
+ # If the caller wanted a reservation_id, return it
+ if ret_resv_id:
+ return wsgi.ResponseObject({'reservation_id': resv_id},
+ xml=ServerMultipleCreateTemplate)
+
+ req.cache_db_instances(instances)
+ server = self._view_builder.create(req, instances[0])
+
+ if '_is_precooked' in server['server'].keys():
+ del server['server']['_is_precooked']
+ else:
+ if CONF.enable_instance_password:
+ server['server']['adminPass'] = password
+
+ robj = wsgi.ResponseObject(server)
+
+ return self._add_location(robj)
+
+ def _create_extension_point(self, ext, server_dict, create_kwargs):
+ handler = ext.obj
+ LOG.debug(_("Running _create_extension_point for %s"), ext.obj)
+
+ handler.server_create(server_dict, create_kwargs)
+
+ def _delete(self, context, req, instance_uuid):
+ instance = self._get_server(context, req, instance_uuid)
+ if CONF.reclaim_instance_interval:
+ self.compute_api.soft_delete(context, instance)
+ else:
+ self.compute_api.delete(context, instance)
+
+ @wsgi.serializers(xml=ServerTemplate)
+ def update(self, req, id, body):
+ """Update server then pass on to version-specific controller."""
+ if not self.is_valid_body(body, 'server'):
+ raise exc.HTTPUnprocessableEntity()
+
+ ctxt = req.environ['nova.context']
+ update_dict = {}
+
+ if 'name' in body['server']:
+ name = body['server']['name']
+ self._validate_server_name(name)
+ update_dict['display_name'] = name.strip()
+
+ if 'accessIPv4' in body['server']:
+ access_ipv4 = body['server']['accessIPv4']
+ if access_ipv4 is None:
+ access_ipv4 = ''
+ if access_ipv4:
+ self._validate_access_ipv4(access_ipv4)
+ update_dict['access_ip_v4'] = access_ipv4.strip()
+
+ if 'accessIPv6' in body['server']:
+ access_ipv6 = body['server']['accessIPv6']
+ if access_ipv6 is None:
+ access_ipv6 = ''
+ if access_ipv6:
+ self._validate_access_ipv6(access_ipv6)
+ update_dict['access_ip_v6'] = access_ipv6.strip()
+
+ if 'auto_disk_config' in body['server']:
+ auto_disk_config = strutils.bool_from_string(
+ body['server']['auto_disk_config'])
+ update_dict['auto_disk_config'] = auto_disk_config
+
+ if 'hostId' in body['server']:
+ msg = _("HostId cannot be updated.")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ if 'personality' in body['server']:
+ msg = _("Personality cannot be updated.")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ try:
+ instance = self.compute_api.get(ctxt, id)
+ req.cache_db_instance(instance)
+ self.compute_api.update(ctxt, instance, **update_dict)
+ except exception.NotFound:
+ msg = _("Instance could not be found")
+ raise exc.HTTPNotFound(explanation=msg)
+
+ instance.update(update_dict)
+
+ self._add_instance_faults(ctxt, [instance])
+ return self._view_builder.show(req, instance)
+
+ @wsgi.response(202)
+ @wsgi.serializers(xml=FullServerTemplate)
+ @wsgi.deserializers(xml=ActionDeserializer)
+ @wsgi.action('confirmResize')
+ def _action_confirm_resize(self, req, id, body):
+ context = req.environ['nova.context']
+ instance = self._get_server(context, req, id)
+ try:
+ self.compute_api.confirm_resize(context, instance)
+ except exception.MigrationNotFound:
+ msg = _("Instance has not been resized.")
+ raise exc.HTTPBadRequest(explanation=msg)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'confirmResize')
+ return exc.HTTPNoContent()
+
+ @wsgi.response(202)
+ @wsgi.serializers(xml=FullServerTemplate)
+ @wsgi.deserializers(xml=ActionDeserializer)
+ @wsgi.action('revertResize')
+ def _action_revert_resize(self, req, id, body):
+ context = req.environ['nova.context']
+ instance = self._get_server(context, req, id)
+ try:
+ self.compute_api.revert_resize(context, instance)
+ except exception.MigrationNotFound:
+ msg = _("Instance has not been resized.")
+ raise exc.HTTPBadRequest(explanation=msg)
+ except exception.InstanceTypeNotFound:
+ msg = _("Flavor used by the instance could not be found.")
+ raise exc.HTTPBadRequest(explanation=msg)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'revertResize')
+ return webob.Response(status_int=202)
+
+ @wsgi.response(202)
+ @wsgi.serializers(xml=FullServerTemplate)
+ @wsgi.deserializers(xml=ActionDeserializer)
+ @wsgi.action('reboot')
+ def _action_reboot(self, req, id, body):
+ if 'reboot' in body and 'type' in body['reboot']:
+ valid_reboot_types = ['HARD', 'SOFT']
+ reboot_type = body['reboot']['type'].upper()
+ if not valid_reboot_types.count(reboot_type):
+ msg = _("Argument 'type' for reboot is not HARD or SOFT")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+ else:
+ msg = _("Missing argument 'type' for reboot")
+ LOG.error(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ context = req.environ['nova.context']
+ instance = self._get_server(context, req, id)
+
+ try:
+ self.compute_api.reboot(context, instance, reboot_type)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'reboot')
+ return webob.Response(status_int=202)
+
+ def _resize(self, req, instance_id, flavor_id, **kwargs):
+ """Begin the resize process with given instance/flavor."""
+ context = req.environ["nova.context"]
+ instance = self._get_server(context, req, instance_id)
+
+ try:
+ self.compute_api.resize(context, instance, flavor_id, **kwargs)
+ except exception.FlavorNotFound:
+ msg = _("Unable to locate requested flavor.")
+ raise exc.HTTPBadRequest(explanation=msg)
+ except exception.CannotResizeToSameFlavor:
+ msg = _("Resize requires a flavor change.")
+ raise exc.HTTPBadRequest(explanation=msg)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'resize')
+ except exception.ImageNotAuthorized as image_error:
+ 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:
+ msg = _("Image that the instance was started "
+ "with could not be found.")
+ raise exc.HTTPBadRequest(explanation=msg)
+ except exception.Invalid:
+ msg = _("Invalid instance image.")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ return webob.Response(status_int=202)
+
+ @wsgi.response(204)
+ def delete(self, req, id):
+ """Destroys a server."""
+ try:
+ self._delete(req.environ['nova.context'], req, id)
+ except exception.NotFound:
+ msg = _("Instance could not be found")
+ raise exc.HTTPNotFound(explanation=msg)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'delete')
+
+ def _image_ref_from_req_data(self, data):
+ try:
+ return unicode(data['server']['imageRef'])
+ except (TypeError, KeyError):
+ msg = _("Missing imageRef attribute")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ def _image_uuid_from_href(self, image_href):
+ # If the image href was generated by nova api, strip image_href
+ # down to an id and use the default glance connection params
+ image_uuid = image_href.split('/').pop()
+
+ if not uuidutils.is_uuid_like(image_uuid):
+ msg = _("Invalid imageRef provided.")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ return image_uuid
+
+ def _image_from_req_data(self, data):
+ """
+ Get image data from the request or raise appropriate
+ exceptions
+
+ If no image is supplied - checks to see if there is
+ block devices set and proper extesions loaded.
+ """
+ image_ref = data['server'].get('imageRef')
+ bdm = data['server'].get('block_device_mapping')
+
+ # TODO(cyeoh): bp v3-api-core-as-extensions
+ # Replace with extension point. For the moment
+ # with no ep, assume os-volumes is not present
+ #if not image_ref and bdm and self.ext_mgr.is_loaded('os-volumes'):
+ # return ''
+ #else:
+ # image_href = self._image_ref_from_req_data(data)
+ # image_uuid = self._image_uuid_from_href(image_href)
+ # return image_uuid
+ image_href = self._image_ref_from_req_data(data)
+ image_uuid = self._image_uuid_from_href(image_href)
+ return image_uuid
+
+ def _flavor_id_from_req_data(self, data):
+ try:
+ flavor_ref = data['server']['flavorRef']
+ except (TypeError, KeyError):
+ msg = _("Missing flavorRef attribute")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ return common.get_id_from_href(flavor_ref)
+
+ @wsgi.response(202)
+ @wsgi.serializers(xml=FullServerTemplate)
+ @wsgi.deserializers(xml=ActionDeserializer)
+ @wsgi.action('changePassword')
+ def _action_change_password(self, req, id, body):
+ context = req.environ['nova.context']
+ if (not 'changePassword' in body
+ or 'adminPass' not in body['changePassword']):
+ msg = _("No adminPass was specified")
+ raise exc.HTTPBadRequest(explanation=msg)
+ password = body['changePassword']['adminPass']
+ if not isinstance(password, basestring):
+ msg = _("Invalid adminPass")
+ raise exc.HTTPBadRequest(explanation=msg)
+ server = self._get_server(context, req, id)
+ try:
+ self.compute_api.set_admin_password(context, server, password)
+ except NotImplementedError:
+ msg = _("Unable to set password on instance")
+ raise exc.HTTPNotImplemented(explanation=msg)
+ return webob.Response(status_int=202)
+
+ def _validate_metadata(self, metadata):
+ """Ensure that we can work with the metadata given."""
+ try:
+ metadata.iteritems()
+ except AttributeError:
+ msg = _("Unable to parse metadata key/value pairs.")
+ LOG.debug(msg)
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ @wsgi.response(202)
+ @wsgi.serializers(xml=FullServerTemplate)
+ @wsgi.deserializers(xml=ActionDeserializer)
+ @wsgi.action('resize')
+ def _action_resize(self, req, id, body):
+ """Resizes a given instance to the flavor size requested."""
+ try:
+ flavor_ref = str(body["resize"]["flavorRef"])
+ if not flavor_ref:
+ msg = _("Resize request has invalid 'flavorRef' attribute.")
+ raise exc.HTTPBadRequest(explanation=msg)
+ except (KeyError, TypeError):
+ msg = _("Resize requests require 'flavorRef' attribute.")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ kwargs = {}
+ if 'auto_disk_config' in body['resize']:
+ kwargs['auto_disk_config'] = body['resize']['auto_disk_config']
+
+ return self._resize(req, id, flavor_ref, **kwargs)
+
+ @wsgi.response(202)
+ @wsgi.serializers(xml=FullServerTemplate)
+ @wsgi.deserializers(xml=ActionDeserializer)
+ @wsgi.action('rebuild')
+ def _action_rebuild(self, req, id, body):
+ """Rebuild an instance with the given attributes."""
+ try:
+ body = body['rebuild']
+ except (KeyError, TypeError):
+ msg = _('Invalid request body')
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ try:
+ image_href = body["imageRef"]
+ except (KeyError, TypeError):
+ msg = _("Could not parse imageRef from request.")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ image_href = self._image_uuid_from_href(image_href)
+
+ try:
+ password = body['adminPass']
+ except (KeyError, TypeError):
+ password = utils.generate_password()
+
+ context = req.environ['nova.context']
+ instance = self._get_server(context, req, id)
+
+ attr_map = {
+ 'personality': 'files_to_inject',
+ 'name': 'display_name',
+ 'accessIPv4': 'access_ip_v4',
+ 'accessIPv6': 'access_ip_v6',
+ 'metadata': 'metadata',
+ 'auto_disk_config': 'auto_disk_config',
+ }
+
+ if 'accessIPv4' in body:
+ self._validate_access_ipv4(body['accessIPv4'])
+
+ if 'accessIPv6' in body:
+ self._validate_access_ipv6(body['accessIPv6'])
+
+ if 'name' in body:
+ self._validate_server_name(body['name'])
+
+ kwargs = {}
+
+ for request_attribute, instance_attribute in attr_map.items():
+ try:
+ kwargs[instance_attribute] = body[request_attribute]
+ except (KeyError, TypeError):
+ pass
+
+ self._validate_metadata(kwargs.get('metadata', {}))
+
+ if 'files_to_inject' in kwargs:
+ personality = kwargs['files_to_inject']
+ kwargs['files_to_inject'] = self._get_injected_files(personality)
+
+ try:
+ self.compute_api.rebuild(context,
+ instance,
+ image_href,
+ password,
+ **kwargs)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'rebuild')
+ except exception.InstanceNotFound:
+ msg = _("Instance could not be found")
+ raise exc.HTTPNotFound(explanation=msg)
+ except exception.InvalidMetadataSize as error:
+ raise exc.HTTPRequestEntityTooLarge(
+ explanation=error.format_message())
+ except exception.ImageNotFound:
+ msg = _("Cannot find image for rebuild")
+ raise exc.HTTPBadRequest(explanation=msg)
+ except (exception.ImageNotActive,
+ exception.InstanceTypeDiskTooSmall,
+ exception.InstanceTypeMemoryTooSmall,
+ exception.InvalidMetadata) as error:
+ raise exc.HTTPBadRequest(explanation=error.format_message())
+
+ instance = self._get_server(context, req, id)
+
+ self._add_instance_faults(context, [instance])
+ view = self._view_builder.show(req, instance)
+
+ # Add on the adminPass attribute since the view doesn't do it
+ # unless instance passwords are disabled
+ if CONF.enable_instance_password:
+ view['server']['adminPass'] = password
+
+ robj = wsgi.ResponseObject(view)
+ return self._add_location(robj)
+
+ @wsgi.response(202)
+ @wsgi.serializers(xml=FullServerTemplate)
+ @wsgi.deserializers(xml=ActionDeserializer)
+ @wsgi.action('createImage')
+ @common.check_snapshots_enabled
+ def _action_create_image(self, req, id, body):
+ """Snapshot a server instance."""
+ context = req.environ['nova.context']
+ entity = body.get("createImage", {})
+
+ image_name = entity.get("name")
+
+ if not image_name:
+ msg = _("createImage entity requires name attribute")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ props = {}
+ metadata = entity.get('metadata', {})
+ common.check_img_metadata_properties_quota(context, metadata)
+ try:
+ props.update(metadata)
+ except ValueError:
+ msg = _("Invalid metadata")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ instance = self._get_server(context, req, id)
+
+ bdms = self.compute_api.get_instance_bdms(context, instance)
+
+ try:
+ if self.compute_api.is_volume_backed_instance(context, instance,
+ bdms):
+ img = instance['image_ref']
+ src_image = self.compute_api.image_service.show(context, img)
+ image_meta = dict(src_image)
+
+ image = self.compute_api.snapshot_volume_backed(
+ context,
+ instance,
+ image_meta,
+ image_name,
+ extra_properties=props)
+ else:
+ image = self.compute_api.snapshot(context,
+ instance,
+ image_name,
+ extra_properties=props)
+ except exception.InstanceInvalidState as state_error:
+ common.raise_http_conflict_for_instance_invalid_state(state_error,
+ 'createImage')
+ except exception.Invalid as err:
+ raise exc.HTTPBadRequest(explanation=str(err))
+
+ # build location of newly-created image entity
+ image_id = str(image['id'])
+ image_ref = os.path.join(req.application_url,
+ context.project_id,
+ 'images',
+ image_id)
+
+ resp = webob.Response(status_int=202)
+ resp.headers['Location'] = image_ref
+ return resp
+
+ def _get_server_admin_password(self, server):
+ """Determine the admin password for a server on creation."""
+ try:
+ password = server['adminPass']
+ self._validate_admin_password(password)
+ except KeyError:
+ password = utils.generate_password()
+ except ValueError:
+ raise exc.HTTPBadRequest(explanation=_("Invalid adminPass"))
+
+ return password
+
+ def _validate_admin_password(self, password):
+ if not isinstance(password, basestring):
+ raise ValueError()
+
+ def _get_server_search_options(self):
+ """Return server search options allowed by non-admin."""
+ return ('reservation_id', 'name', 'status', 'image', 'flavor',
+ 'changes-since', 'all_tenants')
+
+
+def remove_invalid_options(context, search_options, allowed_search_options):
+ """Remove search options that are not valid for non-admin API/context."""
+ if context.is_admin:
+ # Allow all options
+ return
+ # Otherwise, strip out all unknown options
+ unknown_options = [opt for opt in search_options
+ if opt not in allowed_search_options]
+ LOG.debug(_("Removing options '%s' from query"),
+ ", ".join(unknown_options))
+ for opt in unknown_options:
+ search_options.pop(opt, None)
+
+
+class Servers(extensions.V3APIExtensionBase):
+ """Servers."""
+
+ name = "Servers"
+ alias = "servers"
+ namespace = "http://docs.openstack.org/compute/core/servers/v3"
+ version = 1
+
+ def get_resources(self):
+ member_actions = {'action': 'POST'}
+ collection_actions = {'detail': 'GET'}
+ resources = [
+ extensions.ResourceExtension(
+ 'servers',
+ ServersController(extension_info=self.extension_info),
+ member_name='server', collection_actions=collection_actions,
+ member_actions=member_actions)]
+
+ return resources
+
+ def get_controller_extensions(self):
+ return []
diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py
index 83e4b7ba5..6cbc5bb78 100644
--- a/nova/api/openstack/extensions.py
+++ b/nova/api/openstack/extensions.py
@@ -297,7 +297,7 @@ class ResourceExtension(object):
def __init__(self, collection, controller=None, parent=None,
collection_actions=None, member_actions=None,
- custom_routes_fn=None, inherits=None):
+ custom_routes_fn=None, inherits=None, member_name=None):
if not collection_actions:
collection_actions = {}
if not member_actions:
@@ -309,6 +309,7 @@ class ResourceExtension(object):
self.member_actions = member_actions
self.custom_routes_fn = custom_routes_fn
self.inherits = inherits
+ self.member_name = member_name
def load_standard_extensions(ext_mgr, logger, path, package, ext_list=None):
@@ -410,6 +411,9 @@ class V3APIExtensionBase(object):
"""
__metaclass__ = abc.ABCMeta
+ def __init__(self, extension_info):
+ self.extension_info = extension_info
+
@abc.abstractmethod
def get_resources(self):
"""Return a list of resources extensions.
diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_keypairs.py b/nova/tests/api/openstack/compute/plugins/v3/test_keypairs.py
new file mode 100644
index 000000000..529c5eb71
--- /dev/null
+++ b/nova/tests/api/openstack/compute/plugins/v3/test_keypairs.py
@@ -0,0 +1,378 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Eldar Nugaev
+# 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.
+
+from lxml import etree
+import webob
+
+from nova.api.openstack.compute.plugins.v3 import keypairs
+from nova.api.openstack import wsgi
+from nova import db
+from nova import exception
+from nova.openstack.common import jsonutils
+from nova import quota
+from nova import test
+from nova.tests.api.openstack import fakes
+
+
+QUOTAS = quota.QUOTAS
+
+
+def fake_keypair(name):
+ return {'public_key': 'FAKE_KEY',
+ 'fingerprint': 'FAKE_FINGERPRINT',
+ 'name': name}
+
+
+def db_key_pair_get_all_by_user(self, user_id):
+ return [fake_keypair('FAKE')]
+
+
+def db_key_pair_create(self, keypair):
+ return keypair
+
+
+def db_key_pair_destroy(context, user_id, name):
+ if not (user_id and name):
+ raise Exception()
+
+
+def db_key_pair_create_duplicate(context, keypair):
+ raise exception.KeyPairExists(key_name=keypair.get('name', ''))
+
+
+class KeypairsTest(test.TestCase):
+
+ def setUp(self):
+ super(KeypairsTest, self).setUp()
+ self.Controller = keypairs.Controller()
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+
+ self.stubs.Set(db, "key_pair_get_all_by_user",
+ db_key_pair_get_all_by_user)
+ self.stubs.Set(db, "key_pair_create",
+ db_key_pair_create)
+ self.stubs.Set(db, "key_pair_destroy",
+ db_key_pair_destroy)
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Keypairs'])
+ self.app = fakes.wsgi_app_v3(init_only=('os-keypairs', 'servers'))
+
+ def test_keypair_list(self):
+ req = webob.Request.blank('/v3/os-keypairs')
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 200)
+ res_dict = jsonutils.loads(res.body)
+ response = {'keypairs': [{'keypair': fake_keypair('FAKE')}]}
+ self.assertEqual(res_dict, response)
+
+ def test_keypair_create(self):
+ body = {'keypair': {'name': 'create_test'}}
+ req = webob.Request.blank('/v3/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 200)
+ res_dict = jsonutils.loads(res.body)
+ self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0)
+ self.assertTrue(len(res_dict['keypair']['private_key']) > 0)
+
+ def test_keypair_create_with_empty_name(self):
+ body = {'keypair': {'name': ''}}
+ req = webob.Request.blank('/v3/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+
+ def test_keypair_create_with_invalid_name(self):
+ body = {
+ 'keypair': {
+ 'name': 'a' * 256
+ }
+ }
+ req = webob.Request.blank('/v3/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+
+ def test_keypair_create_with_non_alphanumeric_name(self):
+ body = {
+ 'keypair': {
+ 'name': 'test/keypair'
+ }
+ }
+ req = webob.Request.blank('/v3/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ res_dict = jsonutils.loads(res.body)
+ self.assertEqual(res.status_int, 400)
+
+ def test_keypair_import(self):
+ body = {
+ 'keypair': {
+ 'name': 'create_test',
+ 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznA'
+ 'x9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4'
+ 'a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6Y'
+ 'Qj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymi'
+ 'MZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7lj'
+ 'j5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1'
+ 'GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGc'
+ 'j7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwB'
+ 'bHkXa6OciiJDvkRzJXzf',
+ },
+ }
+
+ req = webob.Request.blank('/v3/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 200)
+ # FIXME(ja): sholud we check that public_key was sent to create?
+ res_dict = jsonutils.loads(res.body)
+ self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0)
+ self.assertFalse('private_key' in res_dict['keypair'])
+
+ def test_keypair_import_quota_limit(self):
+
+ def fake_quotas_count(self, context, resource, *args, **kwargs):
+ return 100
+
+ self.stubs.Set(QUOTAS, "count", fake_quotas_count)
+
+ body = {
+ 'keypair': {
+ 'name': 'create_test',
+ 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznA'
+ 'x9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4'
+ 'a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6Y'
+ 'Qj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymi'
+ 'MZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7lj'
+ 'j5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1'
+ 'GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGc'
+ 'j7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwB'
+ 'bHkXa6OciiJDvkRzJXzf',
+ },
+ }
+
+ req = webob.Request.blank('/v3/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 413)
+
+ def test_keypair_create_quota_limit(self):
+
+ def fake_quotas_count(self, context, resource, *args, **kwargs):
+ return 100
+
+ self.stubs.Set(QUOTAS, "count", fake_quotas_count)
+
+ body = {
+ 'keypair': {
+ 'name': 'create_test',
+ },
+ }
+
+ req = webob.Request.blank('/v3/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 413)
+
+ def test_keypair_create_duplicate(self):
+ self.stubs.Set(db, "key_pair_create", db_key_pair_create_duplicate)
+ body = {'keypair': {'name': 'create_duplicate'}}
+ req = webob.Request.blank('/v3/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 409)
+
+ def test_keypair_import_bad_key(self):
+ body = {
+ 'keypair': {
+ 'name': 'create_test',
+ 'public_key': 'ssh-what negative',
+ },
+ }
+
+ req = webob.Request.blank('/v3/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+
+ def test_keypair_delete(self):
+ req = webob.Request.blank('/v3/os-keypairs/FAKE')
+ req.method = 'DELETE'
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 202)
+
+ def test_keypair_get_keypair_not_found(self):
+ req = webob.Request.blank('/v3/os-keypairs/DOESNOTEXIST')
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 404)
+
+ def test_keypair_delete_not_found(self):
+
+ def db_key_pair_get_not_found(context, user_id, name):
+ raise exception.KeypairNotFound(user_id=user_id, name=name)
+
+ self.stubs.Set(db, "key_pair_get",
+ db_key_pair_get_not_found)
+ req = webob.Request.blank('/v3/os-keypairs/WHAT')
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 404)
+
+ def test_keypair_show(self):
+
+ def _db_key_pair_get(context, user_id, name):
+ return {'name': 'foo', 'public_key': 'XXX', 'fingerprint': 'YYY'}
+
+ self.stubs.Set(db, "key_pair_get", _db_key_pair_get)
+
+ req = webob.Request.blank('/v3/os-keypairs/FAKE')
+ req.method = 'GET'
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ res_dict = jsonutils.loads(res.body)
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual('foo', res_dict['keypair']['name'])
+ self.assertEqual('XXX', res_dict['keypair']['public_key'])
+ self.assertEqual('YYY', res_dict['keypair']['fingerprint'])
+
+ def test_keypair_show_not_found(self):
+
+ def _db_key_pair_get(context, user_id, name):
+ raise exception.KeypairNotFound(user_id=user_id, name=name)
+
+ self.stubs.Set(db, "key_pair_get", _db_key_pair_get)
+
+ req = webob.Request.blank('/v3/os-keypairs/FAKE')
+ req.method = 'GET'
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 404)
+
+ def test_show_server(self):
+ self.stubs.Set(db, 'instance_get',
+ fakes.fake_instance_get())
+ req = webob.Request.blank('/v3/servers/1')
+ req.headers['Content-Type'] = 'application/json'
+ response = req.get_response(self.app)
+ self.assertEquals(response.status_int, 200)
+ res_dict = jsonutils.loads(response.body)
+ self.assertTrue('key_name' in res_dict['server'])
+ self.assertEquals(res_dict['server']['key_name'], '')
+
+ def test_detail_servers(self):
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fakes.fake_instance_get_all_by_filters())
+ req = fakes.HTTPRequest.blank('/v3/servers/detail')
+ res = req.get_response(self.app)
+ server_dicts = jsonutils.loads(res.body)['servers']
+ self.assertEquals(len(server_dicts), 5)
+
+ for server_dict in server_dicts:
+ self.assertTrue('key_name' in server_dict)
+ self.assertEquals(server_dict['key_name'], '')
+
+ def test_keypair_create_with_invalid_keypairBody(self):
+ body = {'alpha': {'name': 'create_test'}}
+ req = webob.Request.blank('/v3/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ res_dict = jsonutils.loads(res.body)
+ self.assertEqual(res.status_int, 400)
+ self.assertEqual(res_dict['badRequest']['message'],
+ "Invalid request body")
+
+
+class KeypairsXMLSerializerTest(test.TestCase):
+ def setUp(self):
+ super(KeypairsXMLSerializerTest, self).setUp()
+ self.deserializer = wsgi.XMLDeserializer()
+
+ def test_default_serializer(self):
+ exemplar = dict(keypair=dict(
+ public_key='fake_public_key',
+ private_key='fake_private_key',
+ fingerprint='fake_fingerprint',
+ user_id='fake_user_id',
+ name='fake_key_name'))
+ serializer = keypairs.KeypairTemplate()
+ text = serializer.serialize(exemplar)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('keypair', tree.tag)
+ for child in tree:
+ self.assertTrue(child.tag in exemplar['keypair'])
+ self.assertEqual(child.text, exemplar['keypair'][child.tag])
+
+ def test_index_serializer(self):
+ exemplar = dict(keypairs=[
+ dict(keypair=dict(
+ name='key1_name',
+ public_key='key1_key',
+ fingerprint='key1_fingerprint')),
+ dict(keypair=dict(
+ name='key2_name',
+ public_key='key2_key',
+ fingerprint='key2_fingerprint'))])
+ serializer = keypairs.KeypairsTemplate()
+ text = serializer.serialize(exemplar)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('keypairs', tree.tag)
+ self.assertEqual(len(exemplar['keypairs']), len(tree))
+ for idx, keypair in enumerate(tree):
+ self.assertEqual('keypair', keypair.tag)
+ kp_data = exemplar['keypairs'][idx]['keypair']
+ for child in keypair:
+ self.assertTrue(child.tag in kp_data)
+ self.assertEqual(child.text, kp_data[child.tag])
+
+ def test_deserializer(self):
+ exemplar = dict(keypair=dict(
+ name='key_name',
+ public_key='public_key'))
+ intext = ("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<keypair><name>key_name</name>'
+ '<public_key>public_key</public_key></keypair>')
+
+ result = self.deserializer.deserialize(intext)['body']
+ self.assertEqual(result, exemplar)
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index 2316093c2..86fa3f750 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -100,6 +100,30 @@ def wsgi_app(inner_app_v2=None, fake_auth_context=None,
return mapper
+def wsgi_app_v3(inner_app_v3=None, fake_auth_context=None,
+ use_no_auth=False, ext_mgr=None, init_only=None):
+ if not inner_app_v3:
+ inner_app_v3 = compute.APIRouterV3(init_only)
+
+ if use_no_auth:
+ api_v3 = openstack_api.FaultWrapper(auth.NoAuthMiddleware(
+ limits.RateLimitingMiddleware(inner_app_v3)))
+ else:
+ if fake_auth_context is not None:
+ ctxt = fake_auth_context
+ else:
+ ctxt = context.RequestContext('fake', 'fake', auth_token=True)
+ api_v3 = openstack_api.FaultWrapper(api_auth.InjectContext(ctxt,
+ limits.RateLimitingMiddleware(inner_app_v3)))
+
+ mapper = urlmap.URLMap()
+ mapper['/v3'] = api_v3
+ # TODO(cyeoh): bp nova-api-core-as-extensions
+ # Still need to implement versions for v3 API
+ # mapper['/'] = openstack_api.FaultWrapper(versions.Versions())
+ return mapper
+
+
def stub_out_key_pair_funcs(stubs, have_key_pair=True):
def key_pair(context, user_id):
return [dict(name='key', public_key='public_key')]
diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py
index 1290ef80b..08a85c4de 100644
--- a/nova/tests/fake_policy.py
+++ b/nova/tests/fake_policy.py
@@ -152,6 +152,7 @@ policy_data = """
"compute_extension:instance_actions:events": "is_admin:True",
"compute_extension:instance_usage_audit_log": "",
"compute_extension:keypairs": "",
+ "compute_extension:v3:os-keypairs": "",
"compute_extension:multinic": "",
"compute_extension:networks": "",
"compute_extension:networks:view": "",
diff --git a/setup.cfg b/setup.cfg
index 54127dcfb..7890c02e1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -55,6 +55,12 @@ console_scripts =
nova.api.v3.extensions =
fixed_ips = nova.api.openstack.compute.plugins.v3.fixed_ips:FixedIPs
+ extension_info = nova.api.openstack.compute.plugins.v3.extension_info:ExtensionInfo
+ servers = nova.api.openstack.compute.plugins.v3.servers:Servers
+ keypairs = nova.api.openstack.compute.plugins.v3.keypairs:Keypairs
+
+nova.api.v3.extensions.server.create =
+ keypairs_create = nova.api.openstack.compute.plugins.v3.keypairs:Keypairs
[build_sphinx]
all_files = 1