From 0fab78825ef06310926181f6f97d377058b56b97 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Sat, 24 Sep 2011 23:49:36 +0000 Subject: compute_api create*() and schedulers refactoring Fixes bug 844160 Makes the servers create API call work with all schedulers, removes 'zone boot', and folds create_instance_helper back into servers controller. Notable changes: 1) compute API's create_at_all_once has been removed. It was only used by zone boot. 2) compute API's create() no longer creates Instance DB entries. The schedulers now do this. This makes sense, as only the schedulers will know where the instances will be placed. They could be placed locally or in a child zone. However, this comes at a cost. compute_api.create() now does a 'call' to the scheduler instead of a 'cast' in most cases (* see below). This is so it can receive the instance ID(s) that were created back from the scheduler. Ultimately, we probably need to figure out a way to generate UUIDs before scheduling and return only the information we know about an instance before it is actually scheduled and created. We could then revert this back to a cast. (Or maybe we always return a reservation ID instead of an instance.) 3) scheduler* calls do not return a host now. They return a value that'll be returned if the caller does an rpc.call(). The casts to hosts are now done by the scheduler drivers themselves. 4) There's been an undocumented feature in the OS API to allow multiple instances to be built. I've kept it. 5) If compute_api.create() is creating multiple instances, only a single call is made to the scheduler, vs the old way of sending many casts. All schedulers now check how many instances have been requested. 6) I've added an undocumented option 'return_reservation_id' when building. If set to True, only a reservation ID is returned to the API caller, not the instance. This essentially gives you the old 'nova zone-boot' functionality. 7) It was requested I create a stub for a zones extension, so you'll see the empty extension in here. We'll move some code to it later. 8) Fixes an unrelated bug that merged into trunk recently where zones DB calls were not being done with admin context always, anymore. 9) Scheduler calls were always done with admin context when they should elevate only when needed. 10) Moved stub_network flag so individual tests can run again. * Case #6 above doesn't wait for the scheduler response with instance IDs. It does a 'cast' instead. Change-Id: Ic040780a2e86d7330e225f14056dadbaa9fb3c7e --- nova/api/ec2/cloud.py | 10 +- nova/api/openstack/__init__.py | 3 +- nova/api/openstack/contrib/createserverext.py | 3 +- nova/api/openstack/contrib/volumes.py | 43 +- nova/api/openstack/contrib/zones.py | 50 +++ nova/api/openstack/create_instance_helper.py | 606 ------------------------- nova/api/openstack/servers.py | 616 ++++++++++++++++++++++++-- nova/api/openstack/zones.py | 48 +- 8 files changed, 652 insertions(+), 727 deletions(-) create mode 100644 nova/api/openstack/contrib/zones.py delete mode 100644 nova/api/openstack/create_instance_helper.py (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 3c8cd513f..a71ab8cf0 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -1385,7 +1385,7 @@ class CloudController(object): if image_state != 'available': raise exception.ApiError(_('Image must be available')) - instances = self.compute_api.create(context, + (instances, resv_id) = self.compute_api.create(context, instance_type=instance_types.get_instance_type_by_name( kwargs.get('instance_type', None)), image_href=self._get_image(context, kwargs['image_id'])['id'], @@ -1400,9 +1400,11 @@ class CloudController(object): security_group=kwargs.get('security_group'), availability_zone=kwargs.get('placement', {}).get( 'AvailabilityZone'), - block_device_mapping=kwargs.get('block_device_mapping', {})) - return self._format_run_instances(context, - reservation_id=instances[0]['reservation_id']) + block_device_mapping=kwargs.get('block_device_mapping', {}), + # NOTE(comstud): Unfortunately, EC2 requires that the + # instance DB entries have been created.. + wait_for_instances=True) + return self._format_run_instances(context, resv_id) def _do_instance(self, action, context, ec2_id): instance_id = ec2utils.ec2_id_to_id(ec2_id) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 3b74fefc9..b517eae2c 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -139,8 +139,7 @@ class APIRouter(base_wsgi.Router): controller=zones.create_resource(version), collection={'detail': 'GET', 'info': 'GET', - 'select': 'POST', - 'boot': 'POST'}) + 'select': 'POST'}) mapper.connect("versions", "/", controller=versions.create_resource(version), diff --git a/nova/api/openstack/contrib/createserverext.py b/nova/api/openstack/contrib/createserverext.py index af7f37f13..1e13e10d4 100644 --- a/nova/api/openstack/contrib/createserverext.py +++ b/nova/api/openstack/contrib/createserverext.py @@ -15,7 +15,6 @@ # under the License from nova import utils -from nova.api.openstack import create_instance_helper as helper from nova.api.openstack import extensions from nova.api.openstack import servers from nova.api.openstack import wsgi @@ -66,7 +65,7 @@ class Createserverext(extensions.ExtensionDescriptor): } body_deserializers = { - 'application/xml': helper.ServerXMLDeserializerV11(), + 'application/xml': servers.ServerXMLDeserializerV11(), } serializer = wsgi.ResponseSerializer(body_serializers, diff --git a/nova/api/openstack/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py index 9d4254f1f..6c79a0538 100644 --- a/nova/api/openstack/contrib/volumes.py +++ b/nova/api/openstack/contrib/volumes.py @@ -334,47 +334,8 @@ class VolumeAttachmentController(object): class BootFromVolumeController(servers.ControllerV11): """The boot from volume API controller for the Openstack API.""" - def _create_instance(self, context, instance_type, image_href, **kwargs): - try: - return self.compute_api.create(context, instance_type, - image_href, **kwargs) - except quota.QuotaError as error: - self.helper._handle_quota_error(error) - except exception.ImageNotFound as error: - msg = _("Can not find requested image") - raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) - - def create(self, req, body): - """ Creates a new server for a given user """ - extra_values = None - try: - - def get_kwargs(context, instance_type, image_href, **kwargs): - kwargs['context'] = context - kwargs['instance_type'] = instance_type - kwargs['image_href'] = image_href - return kwargs - - extra_values, kwargs = self.helper.create_instance(req, body, - get_kwargs) - - block_device_mapping = body['server'].get('block_device_mapping') - kwargs['block_device_mapping'] = block_device_mapping - - instances = self._create_instance(**kwargs) - except faults.Fault, f: - return f - - # We can only return 1 instance via the API, if we happen to - # build more than one... instances is a list, so we'll just - # use the first one.. - inst = instances[0] - for key in ['instance_type', 'image_ref']: - inst[key] = extra_values[key] - - server = self._build_view(req, inst, is_detail=True) - server['server']['adminPass'] = extra_values['password'] - return server + def _get_block_device_mapping(self, data): + return data.get('block_device_mapping') class Volumes(extensions.ExtensionDescriptor): diff --git a/nova/api/openstack/contrib/zones.py b/nova/api/openstack/contrib/zones.py new file mode 100644 index 000000000..5901e321e --- /dev/null +++ b/nova/api/openstack/contrib/zones.py @@ -0,0 +1,50 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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 zones extension.""" + + +from nova import flags +from nova import log as logging +from nova.api.openstack import extensions + + +LOG = logging.getLogger("nova.api.zones") +FLAGS = flags.FLAGS + + +class Zones(extensions.ExtensionDescriptor): + def get_name(self): + return "Zones" + + def get_alias(self): + return "os-zones" + + def get_description(self): + return """Enables zones-related functionality such as adding +child zones, listing child zones, getting the capabilities of the +local zone, and returning build plans to parent zones' schedulers""" + + def get_namespace(self): + return "http://docs.openstack.org/ext/zones/api/v1.1" + + def get_updated(self): + return "2011-09-21T00:00:00+00:00" + + def get_resources(self): + # Nothing yet. + return [] diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py deleted file mode 100644 index dde9187cd..000000000 --- a/nova/api/openstack/create_instance_helper.py +++ /dev/null @@ -1,606 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# 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 - -from webob import exc -from xml.dom import minidom - -from nova import exception -from nova import flags -from nova import log as logging -import nova.image -from nova import quota -from nova import utils - -from nova.compute import instance_types -from nova.api.openstack import common -from nova.api.openstack import wsgi -from nova.rpc.common import RemoteError - -LOG = logging.getLogger('nova.api.openstack.create_instance_helper') -FLAGS = flags.FLAGS - - -class CreateFault(exception.NovaException): - message = _("Invalid parameters given to create_instance.") - - def __init__(self, fault): - self.fault = fault - super(CreateFault, self).__init__() - - -class CreateInstanceHelper(object): - """This is the base class for OS API Controllers that - are capable of creating instances (currently Servers and Zones). - - Once we stabilize the Zones portion of the API we may be able - to move this code back into servers.py - """ - - def __init__(self, controller): - """We need the image service to create an instance.""" - self.controller = controller - self._image_service = utils.import_object(FLAGS.image_service) - super(CreateInstanceHelper, self).__init__() - - def create_instance(self, req, body, create_method): - """Creates a new server for the given user. The approach - used depends on the create_method. For example, the standard - POST /server call uses compute.api.create(), while - POST /zones/server uses compute.api.create_all_at_once(). - - The problem is, both approaches return different values (i.e. - [instance dicts] vs. reservation_id). So the handling of the - return type from this method is left to the caller. - """ - if not body: - raise exc.HTTPUnprocessableEntity() - - if not 'server' in body: - raise exc.HTTPUnprocessableEntity() - - context = req.environ['nova.context'] - server_dict = body['server'] - password = self.controller._get_server_admin_password(server_dict) - - if not 'name' 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_href = self.controller._image_ref_from_req_data(body) - # If the image href was generated by nova api, strip image_href - # down to an id and use the default glance connection params - - if str(image_href).startswith(req.application_url): - image_href = image_href.split('/').pop() - try: - image_service, image_id = nova.image.get_image_service(context, - image_href) - kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image( - req, image_service, image_id) - images = set([str(x['id']) for x in image_service.index(context)]) - assert str(image_id) in images - except Exception, e: - msg = _("Cannot find requested image %(image_href)s: %(e)s" % - locals()) - raise exc.HTTPBadRequest(explanation=msg) - - personality = server_dict.get('personality') - config_drive = server_dict.get('config_drive') - - injected_files = [] - if personality: - injected_files = self._get_injected_files(personality) - - sg_names = [] - 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 = server_dict.get('networks') - if requested_networks is not None: - requested_networks = self._get_requested_networks( - requested_networks) - - try: - flavor_id = self.controller._flavor_id_from_req_data(body) - except ValueError as error: - msg = _("Invalid flavorRef provided.") - raise exc.HTTPBadRequest(explanation=msg) - - zone_blob = server_dict.get('blob') - - # optional openstack extensions: - key_name = server_dict.get('key_name') - user_data = server_dict.get('user_data') - self._validate_user_data(user_data) - - availability_zone = server_dict.get('availability_zone') - name = server_dict['name'] - self._validate_server_name(name) - name = name.strip() - - reservation_id = server_dict.get('reservation_id') - min_count = server_dict.get('min_count') - max_count = server_dict.get('max_count') - # min_count and max_count are optional. If they exist, they come - # in as strings. We want to default 'min_count' to 1, and default - # 'max_count' to be 'min_count'. - min_count = int(min_count) if min_count else 1 - max_count = int(max_count) if max_count else min_count - if min_count > max_count: - min_count = max_count - - try: - inst_type = \ - instance_types.get_instance_type_by_flavor_id(flavor_id) - extra_values = { - 'instance_type': inst_type, - 'image_ref': image_href, - 'config_drive': config_drive, - 'password': password} - - return (extra_values, - create_method(context, - inst_type, - image_id, - kernel_id=kernel_id, - ramdisk_id=ramdisk_id, - display_name=name, - display_description=name, - key_name=key_name, - metadata=server_dict.get('metadata', {}), - access_ip_v4=server_dict.get('accessIPv4'), - access_ip_v6=server_dict.get('accessIPv6'), - injected_files=injected_files, - admin_password=password, - zone_blob=zone_blob, - reservation_id=reservation_id, - 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,)) - except quota.QuotaError as error: - self._handle_quota_error(error) - except exception.InstanceTypeMemoryTooSmall as error: - raise exc.HTTPBadRequest(explanation=unicode(error)) - except exception.InstanceTypeDiskTooSmall as error: - raise exc.HTTPBadRequest(explanation=unicode(error)) - 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 exception.SecurityGroupNotFound as error: - raise exc.HTTPBadRequest(explanation=unicode(error)) - except RemoteError as err: - msg = "%(err_type)s: %(err_msg)s" % \ - {'err_type': err.exc_type, 'err_msg': err.value} - raise exc.HTTPBadRequest(explanation=msg) - # Let the caller deal with unhandled exceptions. - - def _handle_quota_error(self, error): - """ - Reraise quota errors as api-specific http exceptions - """ - if error.code == "OnsetFileLimitExceeded": - expl = _("Personality file limit exceeded") - raise exc.HTTPRequestEntityTooLarge(explanation=error.message, - headers={'Retry-After': 0}) - if error.code == "OnsetFilePathLimitExceeded": - expl = _("Personality file path too long") - raise exc.HTTPRequestEntityTooLarge(explanation=error.message, - headers={'Retry-After': 0}) - if error.code == "OnsetFileContentLimitExceeded": - expl = _("Personality file content too long") - raise exc.HTTPRequestEntityTooLarge(explanation=error.message, - headers={'Retry-After': 0}) - if error.code == "InstanceLimitExceeded": - expl = _("Instance quotas have been exceeded") - raise exc.HTTPRequestEntityTooLarge(explanation=error.message, - headers={'Retry-After': 0}) - # if the original error is okay, just reraise it - raise error - - def _deserialize_create(self, request): - """ - Deserialize a create request - - Overrides normal behavior in the case of xml content - """ - if request.content_type == "application/xml": - deserializer = ServerXMLDeserializer() - return deserializer.deserialize(request.body) - else: - return self._deserialize(request.body, request.get_content_type()) - - def _validate_server_name(self, value): - if not isinstance(value, basestring): - msg = _("Server name is not a string or unicode") - raise exc.HTTPBadRequest(explanation=msg) - - if value.strip() == '': - msg = _("Server name is an empty string") - raise exc.HTTPBadRequest(explanation=msg) - - def _get_kernel_ramdisk_from_image(self, req, image_service, image_id): - """Fetch an image from the ImageService, then if present, return the - associated kernel and ramdisk image IDs. - """ - context = req.environ['nova.context'] - image_meta = image_service.show(context, image_id) - # NOTE(sirp): extracted to a separate method to aid unit-testing, the - # new method doesn't need a request obj or an ImageService stub - kernel_id, ramdisk_id = self._do_get_kernel_ramdisk_from_image( - image_meta) - return kernel_id, ramdisk_id - - @staticmethod - def _do_get_kernel_ramdisk_from_image(image_meta): - """Given an ImageService image_meta, return kernel and ramdisk image - ids if present. - - This is only valid for `ami` style images. - """ - image_id = image_meta['id'] - if image_meta['status'] != 'active': - raise exception.ImageUnacceptable(image_id=image_id, - reason=_("status is not active")) - - if image_meta.get('container_format') != 'ami': - return None, None - - try: - kernel_id = image_meta['properties']['kernel_id'] - except KeyError: - raise exception.KernelNotFoundForImage(image_id=image_id) - - try: - ramdisk_id = image_meta['properties']['ramdisk_id'] - except KeyError: - ramdisk_id = None - - return kernel_id, ramdisk_id - - 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) - try: - contents = base64.b64decode(contents) - except TypeError: - expl = _('Personality content for %s cannot be decoded') % path - raise exc.HTTPBadRequest(explanation=expl) - injected_files.append((path, contents)) - return injected_files - - def _get_server_admin_password_old_style(self, server): - """ Determine the admin password for a server on creation """ - return utils.generate_password(FLAGS.password_length) - - def _get_server_admin_password_new_style(self, server): - """ Determine the admin password for a server on creation """ - password = server.get('adminPass') - - if password is None: - return utils.generate_password(FLAGS.password_length) - if not isinstance(password, basestring) or password == '': - msg = _("Invalid adminPass") - raise exc.HTTPBadRequest(explanation=msg) - return password - - def _get_requested_networks(self, requested_networks): - """ - Create a list of requested networks from the networks attribute - """ - networks = [] - for network in requested_networks: - try: - network_uuid = network['uuid'] - - if not utils.is_uuid_like(network_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) - # 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 - - def _validate_user_data(self, user_data): - """Check if the user_data is encoded properly""" - if not user_data: - return - try: - user_data = base64.b64decode(user_data) - except TypeError: - expl = _('Userdata content cannot be decoded') - raise exc.HTTPBadRequest(explanation=expl) - - -class ServerXMLDeserializer(wsgi.XMLDeserializer): - """ - 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 create(self, string): - """Deserialize an xml-formatted server create request""" - dom = minidom.parseString(string) - server = self._extract_server(dom) - return {'body': {'server': server}} - - 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", "imageId", "flavorId", "adminPass"] - for attr in attributes: - if server_node.getAttribute(attr): - server[attr] = server_node.getAttribute(attr) - - metadata_node = self.find_first_child_named(server_node, "metadata") - server["metadata"] = self.metadata_deserializer.extract_metadata( - metadata_node) - - server["personality"] = self._extract_personality(server_node) - - return server - - def _extract_personality(self, server_node): - """Marshal the personality attribute of a parsed request""" - node = self.find_first_child_named(server_node, "personality") - personality = [] - if node is not None: - 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 - - -class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): - """ - 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 action(self, string): - dom = minidom.parseString(string) - action_node = dom.childNodes[0] - action_name = action_node.tagName - - action_deserializer = { - 'createImage': self._action_create_image, - 'createBackup': self._action_create_backup, - '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, 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_create_backup(self, node): - attributes = ('name', 'backup_type', 'rotation') - return self._deserialize_image_action(node, attributes) - - 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"): - rebuild['name'] = node.getAttribute("name") - - 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") - - return rebuild - - def _action_resize(self, node): - if not node.hasAttribute("flavorRef"): - raise AttributeError("No flavorRef was specified in request") - return {"flavorRef": node.getAttribute("flavorRef")} - - 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 - - def create(self, string): - """Deserialize an xml-formatted server create request""" - dom = minidom.parseString(string) - server = self._extract_server(dom) - return {'body': {'server': server}} - - 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"] - for attr in attributes: - if server_node.getAttribute(attr): - server[attr] = server_node.getAttribute(attr) - - metadata_node = self.find_first_child_named(server_node, "metadata") - if metadata_node is not None: - server["metadata"] = self.extract_metadata(metadata_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 - - return server - - 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_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") - 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_node = self.find_first_child_named(sg_node, "name") - if name_node: - item["name"] = self.extract_text(name_node) - security_groups.append(item) - return security_groups - else: - return None diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index e92ab9bda..4a2d0bcbc 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -1,4 +1,5 @@ # Copyright 2010 OpenStack LLC. +# Copyright 2011 Piston Cloud Computing, Inc # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -21,18 +22,21 @@ from novaclient import exceptions as novaclient_exceptions from lxml import etree from webob import exc import webob +from xml.dom import minidom from nova import compute from nova import network from nova import db from nova import exception from nova import flags +from nova import image from nova import log as logging from nova import utils +from nova import quota from nova.api.openstack import common -from nova.api.openstack import create_instance_helper as helper from nova.api.openstack import ips from nova.api.openstack import wsgi +from nova.compute import instance_types from nova.scheduler import api as scheduler_api import nova.api.openstack import nova.api.openstack.views.addresses @@ -40,6 +44,7 @@ import nova.api.openstack.views.flavors import nova.api.openstack.views.images import nova.api.openstack.views.servers from nova.api.openstack import xmlutil +from nova.rpc import common as rpc_common LOG = logging.getLogger('nova.api.openstack.servers') @@ -73,7 +78,6 @@ class Controller(object): def __init__(self): self.compute_api = compute.API() self.network_api = network.API() - self.helper = helper.CreateInstanceHelper(self) def index(self, req): """ Returns a list of server names and ids for a given user """ @@ -107,11 +111,23 @@ class Controller(object): def _action_rebuild(self, info, request, instance_id): raise NotImplementedError() + def _get_block_device_mapping(self, data): + """Get block_device_mapping from 'server' dictionary. + Overidden by volumes controller. + """ + return None + def _get_networks_for_instance(self, req, instance): return ips._get_networks_for_instance(req.environ['nova.context'], self.network_api, instance) + def _get_block_device_mapping(self, data): + """Get block_device_mapping from 'server' dictionary. + Overidden by volumes controller. + """ + return None + def _get_servers(self, req, is_detail): """Returns a list of servers, taking into account any search options specified. @@ -163,6 +179,181 @@ class Controller(object): limited_list = self._limit_items(instance_list, req) return self._build_list(req, limited_list, is_detail=is_detail) + def _handle_quota_error(self, error): + """ + Reraise quota errors as api-specific http exceptions + """ + + code_mappings = { + "OnsetFileLimitExceeded": + _("Personality file limit exceeded"), + "OnsetFilePathLimitExceeded": + _("Personality file path too long"), + "OnsetFileContentLimitExceeded": + _("Personality file content too long"), + "InstanceLimitExceeded": + _("Instance quotas have been exceeded")} + + expl = code_mappings.get(error.code) + if expl: + raise exc.HTTPRequestEntityTooLarge(explanation=expl, + headers={'Retry-After': 0}) + # if the original error is okay, just reraise it + raise error + + def _deserialize_create(self, request): + """ + Deserialize a create request + + Overrides normal behavior in the case of xml content + """ + if request.content_type == "application/xml": + deserializer = ServerXMLDeserializer() + return deserializer.deserialize(request.body) + else: + return self._deserialize(request.body, request.get_content_type()) + + def _validate_server_name(self, value): + if not isinstance(value, basestring): + msg = _("Server name is not a string or unicode") + raise exc.HTTPBadRequest(explanation=msg) + + if value.strip() == '': + msg = _("Server name is an empty string") + raise exc.HTTPBadRequest(explanation=msg) + + def _get_kernel_ramdisk_from_image(self, req, image_service, image_id): + """Fetch an image from the ImageService, then if present, return the + associated kernel and ramdisk image IDs. + """ + context = req.environ['nova.context'] + image_meta = image_service.show(context, image_id) + # NOTE(sirp): extracted to a separate method to aid unit-testing, the + # new method doesn't need a request obj or an ImageService stub + kernel_id, ramdisk_id = self._do_get_kernel_ramdisk_from_image( + image_meta) + return kernel_id, ramdisk_id + + @staticmethod + def _do_get_kernel_ramdisk_from_image(image_meta): + """Given an ImageService image_meta, return kernel and ramdisk image + ids if present. + + This is only valid for `ami` style images. + """ + image_id = image_meta['id'] + if image_meta['status'] != 'active': + raise exception.ImageUnacceptable(image_id=image_id, + reason=_("status is not active")) + + if image_meta.get('container_format') != 'ami': + return None, None + + try: + kernel_id = image_meta['properties']['kernel_id'] + except KeyError: + raise exception.KernelNotFoundForImage(image_id=image_id) + + try: + ramdisk_id = image_meta['properties']['ramdisk_id'] + except KeyError: + ramdisk_id = None + + return kernel_id, ramdisk_id + + 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) + try: + contents = base64.b64decode(contents) + except TypeError: + expl = _('Personality content for %s cannot be decoded') % path + raise exc.HTTPBadRequest(explanation=expl) + injected_files.append((path, contents)) + return injected_files + + def _get_server_admin_password_old_style(self, server): + """ Determine the admin password for a server on creation """ + return utils.generate_password(FLAGS.password_length) + + def _get_server_admin_password_new_style(self, server): + """ Determine the admin password for a server on creation """ + password = server.get('adminPass') + + if password is None: + return utils.generate_password(FLAGS.password_length) + if not isinstance(password, basestring) or password == '': + msg = _("Invalid adminPass") + raise exc.HTTPBadRequest(explanation=msg) + return password + + def _get_requested_networks(self, requested_networks): + """ + Create a list of requested networks from the networks attribute + """ + networks = [] + for network in requested_networks: + try: + network_uuid = network['uuid'] + + if not utils.is_uuid_like(network_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) + # 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 + + def _validate_user_data(self, user_data): + """Check if the user_data is encoded properly""" + if not user_data: + return + try: + user_data = base64.b64decode(user_data) + except TypeError: + expl = _('Userdata content cannot be decoded') + raise exc.HTTPBadRequest(explanation=expl) + @novaclient_exception_converter @scheduler_api.redirect_handler def show(self, req, id): @@ -180,22 +371,172 @@ class Controller(object): def create(self, req, body): """ Creates a new server for a given user """ - if 'server' in body: - body['server']['key_name'] = self._get_key_name(req, body) - extra_values = None - extra_values, instances = self.helper.create_instance( - req, body, self.compute_api.create) + if not body: + raise exc.HTTPUnprocessableEntity() + + if not 'server' in body: + raise exc.HTTPUnprocessableEntity() + + body['server']['key_name'] = self._get_key_name(req, body) + + context = req.environ['nova.context'] + server_dict = body['server'] + password = self._get_server_admin_password(server_dict) + + if not 'name' 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() - # We can only return 1 instance via the API, if we happen to - # build more than one... instances is a list, so we'll just - # use the first one.. - inst = instances[0] - for key in ['instance_type', 'image_ref']: - inst[key] = extra_values[key] + image_href = self._image_ref_from_req_data(body) + # If the image href was generated by nova api, strip image_href + # down to an id and use the default glance connection params - server = self._build_view(req, inst, is_detail=True) - server['server']['adminPass'] = extra_values['password'] + if str(image_href).startswith(req.application_url): + image_href = image_href.split('/').pop() + try: + image_service, image_id = image.get_image_service(context, + image_href) + kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image( + req, image_service, image_id) + images = set([str(x['id']) for x in image_service.index(context)]) + assert str(image_id) in images + except Exception, e: + msg = _("Cannot find requested image %(image_href)s: %(e)s" % + locals()) + raise exc.HTTPBadRequest(explanation=msg) + + personality = server_dict.get('personality') + config_drive = server_dict.get('config_drive') + + injected_files = [] + if personality: + injected_files = self._get_injected_files(personality) + + sg_names = [] + 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 = server_dict.get('networks') + if requested_networks is not None: + requested_networks = self._get_requested_networks( + requested_networks) + + try: + flavor_id = self._flavor_id_from_req_data(body) + except ValueError as error: + msg = _("Invalid flavorRef provided.") + raise exc.HTTPBadRequest(explanation=msg) + + zone_blob = server_dict.get('blob') + + # optional openstack extensions: + key_name = server_dict.get('key_name') + user_data = server_dict.get('user_data') + self._validate_user_data(user_data) + + availability_zone = server_dict.get('availability_zone') + name = server_dict['name'] + self._validate_server_name(name) + name = name.strip() + + block_device_mapping = self._get_block_device_mapping(server_dict) + + # Only allow admins to specify their own reservation_ids + # This is really meant to allow zones to work. + reservation_id = server_dict.get('reservation_id') + if all([reservation_id is not None, + reservation_id != '', + not context.is_admin]): + reservation_id = None + + ret_resv_id = server_dict.get('return_reservation_id', False) + + min_count = server_dict.get('min_count') + max_count = server_dict.get('max_count') + # min_count and max_count are optional. If they exist, they come + # in as strings. We want to default 'min_count' to 1, and default + # 'max_count' to be 'min_count'. + min_count = int(min_count) if min_count else 1 + max_count = int(max_count) if max_count else min_count + if min_count > max_count: + min_count = max_count + + try: + inst_type = \ + instance_types.get_instance_type_by_flavor_id(flavor_id) + + (instances, resv_id) = self.compute_api.create(context, + inst_type, + image_id, + kernel_id=kernel_id, + ramdisk_id=ramdisk_id, + display_name=name, + display_description=name, + key_name=key_name, + metadata=server_dict.get('metadata', {}), + access_ip_v4=server_dict.get('accessIPv4'), + access_ip_v6=server_dict.get('accessIPv6'), + injected_files=injected_files, + admin_password=password, + zone_blob=zone_blob, + reservation_id=reservation_id, + 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, + wait_for_instances=not ret_resv_id) + except quota.QuotaError as error: + self._handle_quota_error(error) + except exception.InstanceTypeMemoryTooSmall as error: + raise exc.HTTPBadRequest(explanation=unicode(error)) + except exception.InstanceTypeDiskTooSmall as error: + raise exc.HTTPBadRequest(explanation=unicode(error)) + 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 exception.SecurityGroupNotFound as error: + raise exc.HTTPBadRequest(explanation=unicode(error)) + 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) + # Let the caller deal with unhandled exceptions. + + # If the caller wanted a reservation_id, return it + if ret_resv_id: + return {'reservation_id': resv_id} + + # Instances is a list + instance = instances[0] + if not instance.get('_is_precooked', False): + instance['instance_type'] = inst_type + instance['image_ref'] = image_href + + server = self._build_view(req, instance, is_detail=True) + if '_is_precooked' in server['server']: + del server['server']['_is_precooked'] + else: + server['server']['adminPass'] = password return server def _delete(self, context, id): @@ -218,7 +559,7 @@ class Controller(object): if 'name' in body['server']: name = body['server']['name'] - self.helper._validate_server_name(name) + self._validate_server_name(name) update_dict['display_name'] = name.strip() if 'accessIPv4' in body['server']: @@ -290,17 +631,17 @@ class Controller(object): except KeyError as missing_key: msg = _("createBackup entity requires %s attribute") % missing_key - raise webob.exc.HTTPBadRequest(explanation=msg) + raise exc.HTTPBadRequest(explanation=msg) except TypeError: msg = _("Malformed createBackup entity") - raise webob.exc.HTTPBadRequest(explanation=msg) + raise exc.HTTPBadRequest(explanation=msg) try: rotation = int(rotation) except ValueError: msg = _("createBackup attribute 'rotation' must be an integer") - raise webob.exc.HTTPBadRequest(explanation=msg) + raise exc.HTTPBadRequest(explanation=msg) # preserve link to server in image properties server_ref = os.path.join(req.application_url, @@ -315,7 +656,7 @@ class Controller(object): props.update(metadata) except ValueError: msg = _("Invalid metadata") - raise webob.exc.HTTPBadRequest(explanation=msg) + raise exc.HTTPBadRequest(explanation=msg) image = self.compute_api.backup(context, instance_id, @@ -696,7 +1037,7 @@ class ControllerV10(Controller): def _get_server_admin_password(self, server): """ Determine the admin password for a server on creation """ - return self.helper._get_server_admin_password_old_style(server) + return self._get_server_admin_password_old_style(server) def _get_server_search_options(self): """Return server search options allowed by non-admin""" @@ -884,11 +1225,11 @@ class ControllerV11(Controller): except KeyError: msg = _("createImage entity requires name attribute") - raise webob.exc.HTTPBadRequest(explanation=msg) + raise exc.HTTPBadRequest(explanation=msg) except TypeError: msg = _("Malformed createImage entity") - raise webob.exc.HTTPBadRequest(explanation=msg) + raise exc.HTTPBadRequest(explanation=msg) # preserve link to server in image properties server_ref = os.path.join(req.application_url, @@ -903,7 +1244,7 @@ class ControllerV11(Controller): props.update(metadata) except ValueError: msg = _("Invalid metadata") - raise webob.exc.HTTPBadRequest(explanation=msg) + raise exc.HTTPBadRequest(explanation=msg) image = self.compute_api.snapshot(context, instance_id, @@ -923,7 +1264,7 @@ class ControllerV11(Controller): def _get_server_admin_password(self, server): """ Determine the admin password for a server on creation """ - return self.helper._get_server_admin_password_new_style(server) + return self._get_server_admin_password_new_style(server) def _get_server_search_options(self): """Return server search options allowed by non-admin""" @@ -1068,6 +1409,227 @@ class ServerXMLSerializer(wsgi.XMLDictSerializer): return self._to_xml(server) +class ServerXMLDeserializer(wsgi.XMLDeserializer): + """ + 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 create(self, string): + """Deserialize an xml-formatted server create request""" + dom = minidom.parseString(string) + server = self._extract_server(dom) + return {'body': {'server': server}} + + 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", "imageId", "flavorId", "adminPass"] + for attr in attributes: + if server_node.getAttribute(attr): + server[attr] = server_node.getAttribute(attr) + + metadata_node = self.find_first_child_named(server_node, "metadata") + server["metadata"] = self.metadata_deserializer.extract_metadata( + metadata_node) + + server["personality"] = self._extract_personality(server_node) + + return server + + def _extract_personality(self, server_node): + """Marshal the personality attribute of a parsed request""" + node = self.find_first_child_named(server_node, "personality") + personality = [] + if node is not None: + 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 + + +class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): + """ + 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 action(self, string): + dom = minidom.parseString(string) + action_node = dom.childNodes[0] + action_name = action_node.tagName + + action_deserializer = { + 'createImage': self._action_create_image, + 'createBackup': self._action_create_backup, + '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, 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_create_backup(self, node): + attributes = ('name', 'backup_type', 'rotation') + return self._deserialize_image_action(node, attributes) + + 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"): + rebuild['name'] = node.getAttribute("name") + + 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") + + return rebuild + + def _action_resize(self, node): + if not node.hasAttribute("flavorRef"): + raise AttributeError("No flavorRef was specified in request") + return {"flavorRef": node.getAttribute("flavorRef")} + + 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 + + def create(self, string): + """Deserialize an xml-formatted server create request""" + dom = minidom.parseString(string) + server = self._extract_server(dom) + return {'body': {'server': server}} + + 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"] + for attr in attributes: + if server_node.getAttribute(attr): + server[attr] = server_node.getAttribute(attr) + + metadata_node = self.find_first_child_named(server_node, "metadata") + if metadata_node is not None: + server["metadata"] = self.extract_metadata(metadata_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 + + return server + + 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_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") + 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_node = self.find_first_child_named(sg_node, "name") + if name_node: + item["name"] = self.extract_text(name_node) + security_groups.append(item) + return security_groups + else: + return None + + def create_resource(version='1.0'): controller = { '1.0': ControllerV10, @@ -1107,8 +1669,8 @@ def create_resource(version='1.0'): } xml_deserializer = { - '1.0': helper.ServerXMLDeserializer(), - '1.1': helper.ServerXMLDeserializerV11(), + '1.0': ServerXMLDeserializer(), + '1.1': ServerXMLDeserializerV11(), }[version] body_deserializers = { diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index 998498e82..95e9eaa51 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -25,8 +25,8 @@ from nova import log as logging from nova.compute import api as compute from nova.scheduler import api -from nova.api.openstack import create_instance_helper as helper from nova.api.openstack import common +from nova.api.openstack import servers from nova.api.openstack import wsgi @@ -67,7 +67,6 @@ class Controller(object): def __init__(self): self.compute_api = compute.API() - self.helper = helper.CreateInstanceHelper(self) def index(self, req): """Return all zones in brief""" @@ -120,18 +119,6 @@ class Controller(object): zone = api.zone_update(context, zone_id, body["zone"]) return dict(zone=_scrub_zone(zone)) - def boot(self, req, body): - """Creates a new server for a given user while being Zone aware. - - Returns a reservation ID (a UUID). - """ - result = None - extra_values, result = self.helper.create_instance(req, body, - self.compute_api.create_all_at_once) - - reservation_id = result - return {'reservation_id': reservation_id} - @check_encryption_key def select(self, req, body): """Returns a weighted list of costs to create instances @@ -155,37 +142,8 @@ class Controller(object): blob=cipher_text)) return cooked - def _image_ref_from_req_data(self, data): - return data['server']['imageId'] - - def _flavor_id_from_req_data(self, data): - return data['server']['flavorId'] - - def _get_server_admin_password(self, server): - """ Determine the admin password for a server on creation """ - return self.helper._get_server_admin_password_old_style(server) - - -class ControllerV11(Controller): - """Controller for 1.1 Zone resources.""" - - def _get_server_admin_password(self, server): - """ Determine the admin password for a server on creation """ - return self.helper._get_server_admin_password_new_style(server) - - def _image_ref_from_req_data(self, data): - return data['server']['imageRef'] - - def _flavor_id_from_req_data(self, data): - return data['server']['flavorRef'] - def create_resource(version): - controller = { - '1.0': Controller, - '1.1': ControllerV11, - }[version]() - metadata = { "attributes": { "zone": ["id", "api_url", "name", "capabilities"], @@ -199,8 +157,8 @@ def create_resource(version): serializer = wsgi.ResponseSerializer(body_serializers) body_deserializers = { - 'application/xml': helper.ServerXMLDeserializer(), + 'application/xml': servers.ServerXMLDeserializer(), } deserializer = wsgi.RequestDeserializer(body_deserializers) - return wsgi.Resource(controller, deserializer, serializer) + return wsgi.Resource(Controller(), deserializer, serializer) -- cgit