diff --git a/bin/nova-manage b/bin/nova-manage index 807753a2eb5c..40f22c19cf56 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -56,6 +56,7 @@ import gettext import glob import json +import math import netaddr import os import sys @@ -591,6 +592,31 @@ class FixedIpCommands(object): fixed_ip['address'], mac_address, hostname, host) + @args('--address', dest="address", metavar='', + help='IP address') + def reserve(self, address): + """Mark fixed ip as reserved + arguments: address""" + self._set_reserved(address, True) + + @args('--address', dest="address", metavar='', + help='IP address') + def unreserve(self, address): + """Mark fixed ip as free to use + arguments: address""" + self._set_reserved(address, False) + + def _set_reserved(self, address, reserved): + ctxt = context.get_admin_context() + + try: + fixed_ip = db.fixed_ip_get_by_address(ctxt, address) + db.fixed_ip_update(ctxt, fixed_ip['address'], + {'reserved': reserved}) + except exception.NotFound as ex: + print "error: %s" % ex + sys.exit(2) + class FloatingIpCommands(object): """Class for managing floating ip.""" @@ -694,7 +720,17 @@ class NetworkCommands(object): if not num_networks: num_networks = FLAGS.num_networks if not network_size: - network_size = FLAGS.network_size + fixnet = netaddr.IPNetwork(fixed_range_v4) + each_subnet_size = fixnet.size / int(num_networks) + if each_subnet_size > FLAGS.network_size: + network_size = FLAGS.network_size + subnet = 32 - int(math.log(network_size, 2)) + oversize_msg = _('Subnet(s) too large, defaulting to /%s.' + ' To override, specify network_size flag.' + ) % subnet + print oversize_msg + else: + network_size = fixnet.size if not multi_host: multi_host = FLAGS.multi_host else: @@ -1224,11 +1260,12 @@ class ImageCommands(object): is_public, architecture) def _lookup(self, old_image_id): + elevated = context.get_admin_context() try: internal_id = ec2utils.ec2_id_to_id(old_image_id) - image = self.image_service.show(context, internal_id) + image = self.image_service.show(elevated, internal_id) except (exception.InvalidEc2Id, exception.ImageNotFound): - image = self.image_service.show_by_name(context, old_image_id) + image = self.image_service.show_by_name(elevated, old_image_id) return image['id'] def _old_to_new(self, old): diff --git a/nova/api/direct.py b/nova/api/direct.py index 993815fc708d..fdd2943d25de 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -296,8 +296,8 @@ class ServiceWrapper(object): 'application/json': nova.api.openstack.wsgi.JSONDictSerializer(), }[content_type] return serializer.serialize(result) - except: - raise exception.Error("returned non-serializable type: %s" + except Exception, e: + raise exception.Error(_("Returned non-serializable type: %s") % result) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index af232edda361..804e54ef9850 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -147,7 +147,7 @@ class Authenticate(wsgi.Middleware): try: signature = req.params['Signature'] access = req.params['AWSAccessKeyId'] - except: + except KeyError, e: raise webob.exc.HTTPBadRequest() # Make a copy of args for authentication and signature verification. @@ -211,7 +211,7 @@ class Requestify(wsgi.Middleware): for non_arg in non_args: # Remove, but raise KeyError if omitted args.pop(non_arg) - except: + except KeyError, e: raise webob.exc.HTTPBadRequest() LOG.debug(_('action: %s'), action) diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py index 7d78c5cfada5..9a3e55925760 100644 --- a/nova/api/ec2/apirequest.py +++ b/nova/api/ec2/apirequest.py @@ -104,7 +104,7 @@ class APIRequest(object): for key in data.keys(): val = data[key] el.appendChild(self._render_data(xml, key, val)) - except: + except Exception: LOG.debug(data) raise diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index d6a98c2cd43b..4d49df2ad1fc 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -50,6 +50,9 @@ FLAGS = flags.FLAGS flags.DEFINE_bool('allow_admin_api', False, 'When True, this API service will accept admin operations.') +flags.DEFINE_bool('allow_instance_snapshots', + True, + 'When True, this API service will permit instance snapshot operations.') class FaultWrapper(base_wsgi.Middleware): diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 829028169a9f..ac2104a5fa73 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -15,8 +15,9 @@ # License for the specific language governing permissions and limitations # under the License. +import functools import re -from urlparse import urlparse +import urlparse from xml.dom import minidom import webob @@ -137,8 +138,8 @@ def get_id_from_href(href): if re.match(r'\d+$', str(href)): return int(href) try: - return int(urlparse(href).path.split('/')[-1]) - except: + return int(urlparse.urlsplit(href).path.split('/')[-1]) + except ValueError, e: LOG.debug(_("Error extracting id from href: %s") % href) raise ValueError(_('could not parse id from href')) @@ -153,22 +154,18 @@ def remove_version_from_href(href): Returns: 'http://www.nova.com' """ - try: - #removes the first instance that matches /v#.#/ - new_href = re.sub(r'[/][v][0-9]+\.[0-9]+[/]', '/', href, count=1) + parsed_url = urlparse.urlsplit(href) + new_path = re.sub(r'^/v[0-9]+\.[0-9]+(/|$)', r'\1', parsed_url.path, + count=1) - #if no version was found, try finding /v#.# at the end of the string - if new_href == href: - new_href = re.sub(r'[/][v][0-9]+\.[0-9]+$', '', href, count=1) - except: - LOG.debug(_("Error removing version from href: %s") % href) - msg = _('could not parse version from href') + if new_path == parsed_url.path: + msg = _('href %s does not contain version') % href + LOG.debug(msg) raise ValueError(msg) - if new_href == href: - msg = _('href does not contain version') - raise ValueError(msg) - return new_href + parsed_url = list(parsed_url) + parsed_url[2] = new_path + return urlparse.urlunsplit(parsed_url) def get_version_from_href(href): @@ -284,3 +281,15 @@ class MetadataXMLSerializer(wsgi.XMLDictSerializer): def default(self, *args, **kwargs): return '' + + +def check_snapshots_enabled(f): + @functools.wraps(f) + def inner(*args, **kwargs): + if not FLAGS.allow_instance_snapshots: + LOG.warn(_('Rejecting snapshot request, snapshots currently' + ' disabled')) + msg = _("Instance snapshots are not permitted at this time.") + raise webob.exc.HTTPBadRequest(explanation=msg) + return f(*args, **kwargs) + return inner diff --git a/nova/api/openstack/contrib/admin_only.py b/nova/api/openstack/contrib/admin_only.py new file mode 100644 index 000000000000..e821c9e1f1e7 --- /dev/null +++ b/nova/api/openstack/contrib/admin_only.py @@ -0,0 +1,30 @@ +# Copyright (c) 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. + +"""Decorator for limiting extensions that should be admin-only.""" + +from functools import wraps +from nova import flags +FLAGS = flags.FLAGS + + +def admin_only(fnc): + @wraps(fnc) + def _wrapped(self, *args, **kwargs): + if FLAGS.allow_admin_api: + return fnc(self, *args, **kwargs) + return [] + _wrapped.func_name = fnc.func_name + return _wrapped diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py index 55e57e1a4254..ecaa365b754b 100644 --- a/nova/api/openstack/contrib/hosts.py +++ b/nova/api/openstack/contrib/hosts.py @@ -24,6 +24,7 @@ from nova import log as logging from nova.api.openstack import common from nova.api.openstack import extensions from nova.api.openstack import faults +from nova.api.openstack.contrib import admin_only from nova.scheduler import api as scheduler_api @@ -70,7 +71,7 @@ class HostController(object): key = raw_key.lower().strip() val = raw_val.lower().strip() # NOTE: (dabo) Right now only 'status' can be set, but other - # actions may follow. + # settings may follow. if key == "status": if val[:6] in ("enable", "disabl"): return self._set_enabled_status(req, id, @@ -89,8 +90,30 @@ class HostController(object): LOG.audit(_("Setting host %(host)s to %(state)s.") % locals()) result = self.compute_api.set_host_enabled(context, host=host, enabled=enabled) + if result not in ("enabled", "disabled"): + # An error message was returned + raise webob.exc.HTTPBadRequest(explanation=result) return {"host": host, "status": result} + def _host_power_action(self, req, host, action): + """Reboots, shuts down or powers up the host.""" + context = req.environ['nova.context'] + try: + result = self.compute_api.host_power_action(context, host=host, + action=action) + except NotImplementedError as e: + raise webob.exc.HTTPBadRequest(explanation=e.msg) + return {"host": host, "power_action": result} + + def startup(self, req, id): + return self._host_power_action(req, host=id, action="startup") + + def shutdown(self, req, id): + return self._host_power_action(req, host=id, action="shutdown") + + def reboot(self, req, id): + return self._host_power_action(req, host=id, action="reboot") + class Hosts(extensions.ExtensionDescriptor): def get_name(self): @@ -108,7 +131,10 @@ class Hosts(extensions.ExtensionDescriptor): def get_updated(self): return "2011-06-29T00:00:00+00:00" + @admin_only.admin_only def get_resources(self): - resources = [extensions.ResourceExtension('os-hosts', HostController(), - collection_actions={'update': 'PUT'}, member_actions={})] + resources = [extensions.ResourceExtension('os-hosts', + HostController(), collection_actions={'update': 'PUT'}, + member_actions={"startup": "GET", "shutdown": "GET", + "reboot": "GET"})] return resources diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 2a8e7fd7eec2..894d47beb72f 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -304,38 +304,6 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): 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, - }.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 _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') - 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) @@ -347,15 +315,14 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): server = {} server_node = self.find_first_child_named(node, 'server') - attributes = ["name", "imageId", "flavorId", "imageRef", - "flavorRef", "adminPass"] + 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) + metadata_node) server["personality"] = self._extract_personality(server_node) @@ -373,3 +340,135 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): 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"] + 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 + + 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 diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index c76738d301c4..0aabb9e565b5 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -106,6 +106,7 @@ class Controller(object): class ControllerV10(Controller): """Version 1.0 specific controller logic.""" + @common.check_snapshots_enabled def create(self, req, body): """Snapshot a server instance and save the image.""" try: @@ -143,7 +144,7 @@ class ControllerV10(Controller): """ context = req.environ['nova.context'] filters = self._get_filters(req) - images = self._image_service.index(context, filters) + images = self._image_service.index(context, filters=filters) images = common.limited(images, req) builder = self.get_builder(req).build return dict(images=[builder(image, detail=False) for image in images]) @@ -156,7 +157,7 @@ class ControllerV10(Controller): """ context = req.environ['nova.context'] filters = self._get_filters(req) - images = self._image_service.detail(context, filters) + images = self._image_service.detail(context, filters=filters) images = common.limited(images, req) builder = self.get_builder(req).build return dict(images=[builder(image, detail=True) for image in images]) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index d1771437177f..f1a27a98c576 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -240,6 +240,7 @@ class Controller(object): resp.headers['Location'] = image_ref return resp + @common.check_snapshots_enabled def _action_create_image(self, input_dict, req, id): return exc.HTTPNotImplemented() @@ -267,10 +268,16 @@ class Controller(object): def _action_reboot(self, input_dict, req, id): if 'reboot' in input_dict and 'type' in input_dict['reboot']: - reboot_type = input_dict['reboot']['type'] + valid_reboot_types = ['HARD', 'SOFT'] + reboot_type = input_dict['reboot']['type'].upper() + if not valid_reboot_types.count(reboot_type): + msg = _("Argument 'type' for reboot is not HARD or SOFT") + LOG.exception(msg) + raise exc.HTTPBadRequest(explanation=msg) else: - LOG.exception(_("Missing argument 'type' for reboot")) - raise exc.HTTPUnprocessableEntity() + msg = _("Missing argument 'type' for reboot") + LOG.exception(msg) + raise exc.HTTPBadRequest(explanation=msg) try: # TODO(gundlach): pass reboot_type, support soft reboot in # virt driver @@ -290,7 +297,7 @@ class Controller(object): context = req.environ['nova.context'] try: self.compute_api.lock(context, id) - except: + except Exception: readable = traceback.format_exc() LOG.exception(_("Compute.api::lock %s"), readable) raise exc.HTTPUnprocessableEntity() @@ -306,7 +313,7 @@ class Controller(object): context = req.environ['nova.context'] try: self.compute_api.unlock(context, id) - except: + except Exception: readable = traceback.format_exc() LOG.exception(_("Compute.api::unlock %s"), readable) raise exc.HTTPUnprocessableEntity() @@ -321,7 +328,7 @@ class Controller(object): context = req.environ['nova.context'] try: self.compute_api.get_lock(context, id) - except: + except Exception: readable = traceback.format_exc() LOG.exception(_("Compute.api::get_lock %s"), readable) raise exc.HTTPUnprocessableEntity() @@ -336,7 +343,7 @@ class Controller(object): context = req.environ['nova.context'] try: self.compute_api.reset_network(context, id) - except: + except Exception: readable = traceback.format_exc() LOG.exception(_("Compute.api::reset_network %s"), readable) raise exc.HTTPUnprocessableEntity() @@ -351,7 +358,7 @@ class Controller(object): context = req.environ['nova.context'] try: self.compute_api.inject_network_info(context, id) - except: + except Exception: readable = traceback.format_exc() LOG.exception(_("Compute.api::inject_network_info %s"), readable) raise exc.HTTPUnprocessableEntity() @@ -363,7 +370,7 @@ class Controller(object): ctxt = req.environ['nova.context'] try: self.compute_api.pause(ctxt, id) - except: + except Exception: readable = traceback.format_exc() LOG.exception(_("Compute.api::pause %s"), readable) raise exc.HTTPUnprocessableEntity() @@ -375,7 +382,7 @@ class Controller(object): ctxt = req.environ['nova.context'] try: self.compute_api.unpause(ctxt, id) - except: + except Exception: readable = traceback.format_exc() LOG.exception(_("Compute.api::unpause %s"), readable) raise exc.HTTPUnprocessableEntity() @@ -387,7 +394,7 @@ class Controller(object): context = req.environ['nova.context'] try: self.compute_api.suspend(context, id) - except: + except Exception: readable = traceback.format_exc() LOG.exception(_("compute.api::suspend %s"), readable) raise exc.HTTPUnprocessableEntity() @@ -399,7 +406,7 @@ class Controller(object): context = req.environ['nova.context'] try: self.compute_api.resume(context, id) - except: + except Exception: readable = traceback.format_exc() LOG.exception(_("compute.api::resume %s"), readable) raise exc.HTTPUnprocessableEntity() @@ -420,7 +427,7 @@ class Controller(object): context = req.environ["nova.context"] try: self.compute_api.rescue(context, id) - except: + except Exception: readable = traceback.format_exc() LOG.exception(_("compute.api::rescue %s"), readable) raise exc.HTTPUnprocessableEntity() @@ -432,7 +439,7 @@ class Controller(object): context = req.environ["nova.context"] try: self.compute_api.unrescue(context, id) - except: + except Exception: readable = traceback.format_exc() LOG.exception(_("compute.api::unrescue %s"), readable) raise exc.HTTPUnprocessableEntity() @@ -646,6 +653,9 @@ class ControllerV11(Controller): """ Resizes a given instance to the flavor size requested """ try: flavor_ref = input_dict["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) @@ -680,6 +690,7 @@ class ControllerV11(Controller): return webob.Response(status_int=202) + @common.check_snapshots_enabled def _action_create_image(self, input_dict, req, instance_id): """Snapshot a server instance.""" entity = input_dict.get("createImage", {}) @@ -891,8 +902,13 @@ def create_resource(version='1.0'): 'application/xml': xml_serializer, } + xml_deserializer = { + '1.0': helper.ServerXMLDeserializer(), + '1.1': helper.ServerXMLDeserializerV11(), + }[version] + body_deserializers = { - 'application/xml': helper.ServerXMLDeserializer(), + 'application/xml': xml_deserializer, } serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 873ce212ad86..912303d144d0 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -77,7 +77,9 @@ class ViewBuilder(object): "status": image_obj.get("status"), }) - if image["status"] == "SAVING": + if image["status"].upper() == "ACTIVE": + image["progress"] = 100 + else: image["progress"] = 0 return image diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index f7fd87bcd52f..a2bf267ed4d8 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -166,7 +166,7 @@ class Controller(object): return self.helper._get_server_admin_password_old_style(server) -class ControllerV11(object): +class ControllerV11(Controller): """Controller for 1.1 Zone resources.""" def _get_server_admin_password(self, server): diff --git a/nova/auth/novarc.template b/nova/auth/novarc.template index d05c099d7ca8..978ffb2100a6 100644 --- a/nova/auth/novarc.template +++ b/nova/auth/novarc.template @@ -16,3 +16,4 @@ export NOVA_API_KEY="%(access)s" export NOVA_USERNAME="%(user)s" export NOVA_PROJECT_ID="%(project)s" export NOVA_URL="%(os)s" +export NOVA_VERSION="1.1" diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py index 521525205307..2c4673f9e6f3 100644 --- a/nova/cloudpipe/pipelib.py +++ b/nova/cloudpipe/pipelib.py @@ -141,15 +141,12 @@ class CloudPipe(object): try: result = cloud._gen_key(context, context.user_id, key_name) private_key = result['private_key'] - try: - key_dir = os.path.join(FLAGS.keys_path, context.user_id) - if not os.path.exists(key_dir): - os.makedirs(key_dir) - key_path = os.path.join(key_dir, '%s.pem' % key_name) - with open(key_path, 'w') as f: - f.write(private_key) - except: - pass - except exception.Duplicate: + key_dir = os.path.join(FLAGS.keys_path, context.user_id) + if not os.path.exists(key_dir): + os.makedirs(key_dir) + key_path = os.path.join(key_dir, '%s.pem' % key_name) + with open(key_path, 'w') as f: + f.write(private_key) + except (exception.Duplicate, os.error, IOError): pass return key_name diff --git a/nova/compute/api.py b/nova/compute/api.py index 80d54d029b86..d2c08678b8e8 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -360,6 +360,7 @@ class API(base.Base): instance_type, zone_blob, availability_zone, injected_files, admin_password, + image, instance_id=None, num_instances=1): """Send the run_instance request to the schedulers for processing.""" pid = context.project_id @@ -373,6 +374,7 @@ class API(base.Base): filter_class = 'nova.scheduler.host_filter.InstanceTypeFilter' request_spec = { + 'image': image, 'instance_properties': base_options, 'instance_type': instance_type, 'filter': filter_class, @@ -415,6 +417,7 @@ class API(base.Base): instance_type, zone_blob, availability_zone, injected_files, admin_password, + image, num_instances=num_instances) return base_options['reservation_id'] @@ -463,6 +466,7 @@ class API(base.Base): instance_type, zone_blob, availability_zone, injected_files, admin_password, + image, instance_id=instance_id) return [dict(x.iteritems()) for x in instances] @@ -993,7 +997,12 @@ class API(base.Base): def set_host_enabled(self, context, host, enabled): """Sets the specified host's ability to accept new instances.""" return self._call_compute_message("set_host_enabled", context, - instance_id=None, host=host, params={"enabled": enabled}) + host=host, params={"enabled": enabled}) + + def host_power_action(self, context, host, action): + """Reboots, shuts down or powers up the host.""" + return self._call_compute_message("host_power_action", context, + host=host, params={"action": action}) @scheduler_api.reroute_compute("diagnostics") def get_diagnostics(self, context, instance_id): diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 9f566dea7aad..cb6617c331b1 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -748,7 +748,8 @@ class ComputeManager(manager.SchedulerDependentManager): instance_ref['host']) rpc.cast(context, topic, {'method': 'finish_revert_resize', - 'args': {'migration_id': migration_ref['id']}, + 'args': {'instance_id': instance_ref['uuid'], + 'migration_id': migration_ref['id']}, }) @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @@ -957,8 +958,12 @@ class ComputeManager(manager.SchedulerDependentManager): result)) @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) - def set_host_enabled(self, context, instance_id=None, host=None, - enabled=None): + def host_power_action(self, context, host=None, action=None): + """Reboots, shuts down or powers up the host.""" + return self.driver.host_power_action(host, action) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + def set_host_enabled(self, context, host=None, enabled=None): """Sets the specified host's ability to accept new instances.""" return self.driver.set_host_enabled(host, enabled) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 4f14452171fb..003eb8bcb187 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1426,9 +1426,14 @@ def instance_action_create(context, values): def instance_get_actions(context, instance_id): """Return the actions associated to the given instance id""" session = get_session() + + if utils.is_uuid_like(instance_id): + instance = instance_get_by_uuid(context, instance_id, session) + instance_id = instance.id + return session.query(models.InstanceActions).\ filter_by(instance_id=instance_id).\ - all() + all() ################### @@ -3178,8 +3183,9 @@ def instance_metadata_delete_all(context, instance_id): @require_context @require_instance_exists -def instance_metadata_get_item(context, instance_id, key): - session = get_session() +def instance_metadata_get_item(context, instance_id, key, session=None): + if not session: + session = get_session() meta_result = session.query(models.InstanceMetadata).\ filter_by(instance_id=instance_id).\ @@ -3205,7 +3211,7 @@ def instance_metadata_update_or_create(context, instance_id, metadata): try: meta_ref = instance_metadata_get_item(context, instance_id, key, session) - except: + except exception.InstanceMetadataNotFound, e: meta_ref = models.InstanceMetadata() meta_ref.update({"key": key, "value": value, "instance_id": instance_id, @@ -3300,8 +3306,10 @@ def instance_type_extra_specs_delete(context, instance_type_id, key): @require_context -def instance_type_extra_specs_get_item(context, instance_type_id, key): - session = get_session() +def instance_type_extra_specs_get_item(context, instance_type_id, key, + session=None): + if not session: + session = get_session() spec_result = session.query(models.InstanceTypeExtraSpecs).\ filter_by(instance_type_id=instance_type_id).\ @@ -3327,7 +3335,7 @@ def instance_type_extra_specs_update_or_create(context, instance_type_id, instance_type_id, key, session) - except: + except exception.InstanceTypeExtraSpecsNotFound, e: spec_ref = models.InstanceTypeExtraSpecs() spec_ref.update({"key": key, "value": value, "instance_type_id": instance_type_id, diff --git a/nova/exception.py b/nova/exception.py index 68e6ac937e0b..a87728fff81c 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -25,6 +25,7 @@ SHOULD include dedicated exception logging. """ from functools import wraps +import sys from nova import log as logging @@ -96,6 +97,10 @@ def wrap_exception(notifier=None, publisher_id=None, event_type=None, try: return f(*args, **kw) except Exception, e: + # Save exception since it can be clobbered during processing + # below before we can re-raise + exc_info = sys.exc_info() + if notifier: payload = dict(args=args, exception=e) payload.update(kw) @@ -122,7 +127,9 @@ def wrap_exception(notifier=None, publisher_id=None, event_type=None, LOG.exception(_('Uncaught exception')) #logging.error(traceback.extract_stack(exc_traceback)) raise Error(str(e)) - raise + + # re-raise original exception since it may have been clobbered + raise exc_info[0], exc_info[1], exc_info[2] return wraps(f)(wrapped) return inner @@ -150,6 +157,10 @@ class NovaException(Exception): return self._error_string +class ImagePaginationFailed(NovaException): + message = _("Failed to paginate through images from image service") + + class VirtualInterfaceCreateException(NovaException): message = _("Virtual Interface creation failed") diff --git a/nova/flags.py b/nova/flags.py index 12c6d1356f24..eb6366ed9fb8 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -317,7 +317,7 @@ DEFINE_string('osapi_extensions_path', '/var/lib/nova/extensions', DEFINE_string('osapi_host', '$my_ip', 'ip of api server') DEFINE_string('osapi_scheme', 'http', 'prefix for openstack') DEFINE_integer('osapi_port', 8774, 'OpenStack API port') -DEFINE_string('osapi_path', '/v1.0/', 'suffix for openstack') +DEFINE_string('osapi_path', '/v1.1/', 'suffix for openstack') DEFINE_integer('osapi_max_limit', 1000, 'max number of items returned in a collection response') diff --git a/nova/image/__init__.py b/nova/image/__init__.py index a27d649d44c5..5447c8a3ac79 100644 --- a/nova/image/__init__.py +++ b/nova/image/__init__.py @@ -35,6 +35,7 @@ def _parse_image_ref(image_href): :param image_href: href of an image :returns: a tuple of the form (image_id, host, port) + :raises ValueError """ o = urlparse(image_href) @@ -72,7 +73,7 @@ def get_glance_client(image_href): try: (image_id, host, port) = _parse_image_ref(image_href) - except: + except ValueError: raise exception.InvalidImageRef(image_href=image_href) glance_client = GlanceClient(host, port) return (glance_client, image_id) diff --git a/nova/image/fake.py b/nova/image/fake.py index 28e912534d3a..97af81711c01 100644 --- a/nova/image/fake.py +++ b/nova/image/fake.py @@ -45,9 +45,12 @@ class _FakeImageService(service.BaseImageService): 'name': 'fakeimage123456', 'created_at': timestamp, 'updated_at': timestamp, + 'deleted_at': None, + 'deleted': False, 'status': 'active', - 'container_format': 'ami', - 'disk_format': 'raw', + 'is_public': False, +# 'container_format': 'ami', +# 'disk_format': 'raw', 'properties': {'kernel_id': FLAGS.null_kernel, 'ramdisk_id': FLAGS.null_kernel, 'architecture': 'x86_64'}} @@ -56,9 +59,12 @@ class _FakeImageService(service.BaseImageService): 'name': 'fakeimage123456', 'created_at': timestamp, 'updated_at': timestamp, + 'deleted_at': None, + 'deleted': False, 'status': 'active', - 'container_format': 'ami', - 'disk_format': 'raw', + 'is_public': True, +# 'container_format': 'ami', +# 'disk_format': 'raw', 'properties': {'kernel_id': FLAGS.null_kernel, 'ramdisk_id': FLAGS.null_kernel}} @@ -66,9 +72,12 @@ class _FakeImageService(service.BaseImageService): 'name': 'fakeimage123456', 'created_at': timestamp, 'updated_at': timestamp, + 'deleted_at': None, + 'deleted': False, 'status': 'active', - 'container_format': 'ami', - 'disk_format': 'raw', + 'is_public': True, +# 'container_format': 'ami', +# 'disk_format': 'raw', 'properties': {'kernel_id': FLAGS.null_kernel, 'ramdisk_id': FLAGS.null_kernel}} @@ -76,9 +85,12 @@ class _FakeImageService(service.BaseImageService): 'name': 'fakeimage123456', 'created_at': timestamp, 'updated_at': timestamp, + 'deleted_at': None, + 'deleted': False, 'status': 'active', - 'container_format': 'ami', - 'disk_format': 'raw', + 'is_public': True, +# 'container_format': 'ami', +# 'disk_format': 'raw', 'properties': {'kernel_id': FLAGS.null_kernel, 'ramdisk_id': FLAGS.null_kernel}} @@ -86,9 +98,12 @@ class _FakeImageService(service.BaseImageService): 'name': 'fakeimage123456', 'created_at': timestamp, 'updated_at': timestamp, + 'deleted_at': None, + 'deleted': False, 'status': 'active', - 'container_format': 'ami', - 'disk_format': 'raw', + 'is_public': True, +# 'container_format': 'ami', +# 'disk_format': 'raw', 'properties': {'kernel_id': FLAGS.null_kernel, 'ramdisk_id': FLAGS.null_kernel}} @@ -101,7 +116,11 @@ class _FakeImageService(service.BaseImageService): def index(self, context, filters=None, marker=None, limit=None): """Returns list of images.""" - return copy.deepcopy(self.images.values()) + retval = [] + for img in self.images.values(): + retval += [dict([(k, v) for k, v in img.iteritems() + if k in ['id', 'name']])] + return retval def detail(self, context, filters=None, marker=None, limit=None): """Return list of detailed image information.""" diff --git a/nova/image/glance.py b/nova/image/glance.py index 44a3c6f838c7..da93f0d1ccb9 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -87,42 +87,71 @@ class GlanceImageService(service.BaseImageService): """Sets the client's auth token.""" self.client.set_auth_token(context.auth_token) - def index(self, context, filters=None, marker=None, limit=None): + def index(self, context, **kwargs): """Calls out to Glance for a list of images available.""" - # NOTE(sirp): We need to use `get_images_detailed` and not - # `get_images` here because we need `is_public` and `properties` - # included so we can filter by user - self._set_client_context(context) - filtered = [] - filters = filters or {} - if 'is_public' not in filters: - # NOTE(vish): don't filter out private images - filters['is_public'] = 'none' - image_metas = self.client.get_images_detailed(filters=filters, - marker=marker, - limit=limit) + params = self._extract_query_params(kwargs) + image_metas = self._get_images(context, **params) + + images = [] for image_meta in image_metas: + # NOTE(sirp): We need to use `get_images_detailed` and not + # `get_images` here because we need `is_public` and `properties` + # included so we can filter by user if self._is_image_available(context, image_meta): meta_subset = utils.subset_dict(image_meta, ('id', 'name')) - filtered.append(meta_subset) - return filtered + images.append(meta_subset) + return images - def detail(self, context, filters=None, marker=None, limit=None): + def detail(self, context, **kwargs): """Calls out to Glance for a list of detailed image information.""" - self._set_client_context(context) - filtered = [] - filters = filters or {} - if 'is_public' not in filters: - # NOTE(vish): don't filter out private images - filters['is_public'] = 'none' - image_metas = self.client.get_images_detailed(filters=filters, - marker=marker, - limit=limit) + params = self._extract_query_params(kwargs) + image_metas = self._get_images(context, **params) + + images = [] for image_meta in image_metas: if self._is_image_available(context, image_meta): base_image_meta = self._translate_to_base(image_meta) - filtered.append(base_image_meta) - return filtered + images.append(base_image_meta) + return images + + def _extract_query_params(self, params): + _params = {} + accepted_params = ('filters', 'marker', 'limit', + 'sort_key', 'sort_dir') + for param in accepted_params: + if param in params: + _params[param] = params.get(param) + + return _params + + def _get_images(self, context, **kwargs): + """Get image entitites from images service""" + self._set_client_context(context) + + # ensure filters is a dict + kwargs['filters'] = kwargs.get('filters') or {} + # NOTE(vish): don't filter out private images + kwargs['filters'].setdefault('is_public', 'none') + + return self._fetch_images(self.client.get_images_detailed, **kwargs) + + def _fetch_images(self, fetch_func, **kwargs): + """Paginate through results from glance server""" + images = fetch_func(**kwargs) + + for image in images: + yield image + else: + # break out of recursive loop to end pagination + return + + try: + # attempt to advance the marker in order to fetch next page + kwargs['marker'] = images[-1]['id'] + except KeyError: + raise exception.ImagePaginationFailed() + + self._fetch_images(fetch_func, **kwargs) def show(self, context, image_id): """Returns a dict with image data for the given opaque image id.""" diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py index 137b671c0616..55cea5f8f27d 100644 --- a/nova/scheduler/api.py +++ b/nova/scheduler/api.py @@ -17,7 +17,8 @@ Handles all requests relating to schedulers. """ -import novaclient +from novaclient import v1_1 as novaclient +from novaclient import exceptions as novaclient_exceptions from nova import db from nova import exception @@ -112,7 +113,7 @@ def _wrap_method(function, self): def _process(func, zone): """Worker stub for green thread pool. Give the worker an authenticated nova client and zone info.""" - nova = novaclient.OpenStack(zone.username, zone.password, None, + nova = novaclient.Client(zone.username, zone.password, None, zone.api_url) nova.authenticate() return func(nova, zone) @@ -132,10 +133,10 @@ def call_zone_method(context, method_name, errors_to_ignore=None, zones = db.zone_get_all(context) for zone in zones: try: - nova = novaclient.OpenStack(zone.username, zone.password, None, + nova = novaclient.Client(zone.username, zone.password, None, zone.api_url) nova.authenticate() - except novaclient.exceptions.BadRequest, e: + except novaclient_exceptions.BadRequest, e: url = zone.api_url LOG.warn(_("Failed request to zone; URL=%(url)s: %(e)s") % locals()) @@ -188,7 +189,7 @@ def _issue_novaclient_command(nova, zone, collection, if method_name in ['find', 'findall']: try: return getattr(manager, method_name)(**kwargs) - except novaclient.NotFound: + except novaclient_exceptions.NotFound: url = zone.api_url LOG.debug(_("%(collection)s.%(method_name)s didn't find " "anything matching '%(kwargs)s' on '%(url)s'" % @@ -200,7 +201,7 @@ def _issue_novaclient_command(nova, zone, collection, item = args.pop(0) try: result = manager.get(item) - except novaclient.NotFound: + except novaclient_exceptions.NotFound: url = zone.api_url LOG.debug(_("%(collection)s '%(item)s' not found on '%(url)s'" % locals())) diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index d99d7214c9c1..047dafa6f39f 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -24,7 +24,9 @@ import operator import json import M2Crypto -import novaclient + +from novaclient import v1_1 as novaclient +from novaclient import exceptions as novaclient_exceptions from nova import crypto from nova import db @@ -58,12 +60,13 @@ class ZoneAwareScheduler(driver.Scheduler): """Create the requested resource in this Zone.""" host = build_plan_item['hostname'] base_options = request_spec['instance_properties'] + image = request_spec['image'] # TODO(sandy): I guess someone needs to add block_device_mapping # support at some point? Also, OS API has no concept of security # groups. instance = compute_api.API().create_db_entry_for_new_instance(context, - base_options, None, []) + image, base_options, None, []) instance_id = instance['id'] kwargs['instance_id'] = instance_id @@ -117,10 +120,9 @@ class ZoneAwareScheduler(driver.Scheduler): % locals()) nova = None try: - nova = novaclient.OpenStack(zone.username, zone.password, None, - url) + nova = novaclient.Client(zone.username, zone.password, None, url) nova.authenticate() - except novaclient.exceptions.BadRequest, e: + except novaclient_exceptions.BadRequest, e: raise exception.NotAuthorized(_("Bad credentials attempting " "to talk to zone at %(url)s.") % locals()) diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py index efdac06e1679..97bdf3d449fc 100644 --- a/nova/scheduler/zone_manager.py +++ b/nova/scheduler/zone_manager.py @@ -18,10 +18,11 @@ ZoneManager oversees all communications with child Zones. """ import datetime -import novaclient import thread import traceback +from novaclient import v1_1 as novaclient + from eventlet import greenpool from nova import db @@ -89,8 +90,8 @@ class ZoneState(object): def _call_novaclient(zone): """Call novaclient. Broken out for testing purposes.""" - client = novaclient.OpenStack(zone.username, zone.password, None, - zone.api_url) + client = novaclient.Client(zone.username, zone.password, None, + zone.api_url) return client.zones.info()._info diff --git a/nova/test.py b/nova/test.py index 5760d7a82ef5..88f1489e81f1 100644 --- a/nova/test.py +++ b/nova/test.py @@ -60,11 +60,42 @@ class skip_test(object): self.message = msg def __call__(self, func): + @functools.wraps(func) def _skipper(*args, **kw): """Wrapped skipper function.""" raise nose.SkipTest(self.message) - _skipper.__name__ = func.__name__ - _skipper.__doc__ = func.__doc__ + return _skipper + + +class skip_if(object): + """Decorator that skips a test if contition is true.""" + def __init__(self, condition, msg): + self.condition = condition + self.message = msg + + def __call__(self, func): + @functools.wraps(func) + def _skipper(*args, **kw): + """Wrapped skipper function.""" + if self.condition: + raise nose.SkipTest(self.message) + func(*args, **kw) + return _skipper + + +class skip_unless(object): + """Decorator that skips a test if condition is not true.""" + def __init__(self, condition, msg): + self.condition = condition + self.message = msg + + def __call__(self, func): + @functools.wraps(func) + def _skipper(*args, **kw): + """Wrapped skipper function.""" + if not self.condition: + raise nose.SkipTest(self.message) + func(*args, **kw) return _skipper diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 8e2e3f3903c9..383ed2e031ea 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -379,6 +379,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): "updated": self.NOW_API_FORMAT, "created": self.NOW_API_FORMAT, "status": "ACTIVE", + "progress": 100, }, } @@ -402,6 +403,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): "updated": self.NOW_API_FORMAT, "created": self.NOW_API_FORMAT, "status": "QUEUED", + "progress": 0, 'server': { 'id': 42, "links": [{ @@ -444,6 +446,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): updated="%(expected_now)s" created="%(expected_now)s" status="ACTIVE" + progress="100" xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" /> """ % (locals())) @@ -463,6 +466,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): updated="%(expected_now)s" created="%(expected_now)s" status="ACTIVE" + progress="100" xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" /> """ % (locals())) @@ -587,6 +591,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'progress': 100, }, { 'id': 124, @@ -594,6 +599,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'QUEUED', + 'progress': 0, }, { 'id': 125, @@ -608,7 +614,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'name': 'active snapshot', 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, - 'status': 'ACTIVE' + 'status': 'ACTIVE', + 'progress': 100, }, { 'id': 127, @@ -616,6 +623,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'FAILED', + 'progress': 0, }, { 'id': 129, @@ -623,6 +631,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'progress': 100, }] self.assertDictListMatch(expected, response_list) @@ -643,6 +652,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'progress': 100, "links": [{ "rel": "self", "href": "http://localhost/v1.1/images/123", @@ -662,6 +672,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'QUEUED', + 'progress': 0, 'server': { 'id': 42, "links": [{ @@ -723,6 +734,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'progress': 100, 'server': { 'id': 42, "links": [{ @@ -753,6 +765,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'FAILED', + 'progress': 0, 'server': { 'id': 42, "links": [{ @@ -780,6 +793,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'progress': 100, "links": [{ "rel": "self", "href": "http://localhost/v1.1/images/129", @@ -1001,7 +1015,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): image_meta = json.loads(res.body)['image'] expected = {'id': 123, 'name': 'public image', 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE'} + 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'progress': 100} self.assertDictMatch(image_meta, expected) def test_get_image_non_existent(self): @@ -1049,6 +1064,16 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): response = req.get_response(fakes.wsgi_app()) self.assertEqual(400, response.status_int) + def test_create_image_snapshots_disabled(self): + self.flags(allow_instance_snapshots=False) + body = dict(image=dict(serverId='123', name='Snapshot 1')) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + @classmethod def _make_image_fixtures(cls): image_id = 123 diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index 562cefe90f06..bf18bc1b0e82 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -458,6 +458,7 @@ class ServerActionsTestV11(test.TestCase): self.service.delete_all() self.sent_to_glance = {} fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance) + self.flags(allow_instance_snapshots=True) def tearDown(self): self.stubs.UnsetAll() @@ -475,6 +476,21 @@ class ServerActionsTestV11(test.TestCase): self.assertEqual(mock_method.instance_id, '1') self.assertEqual(mock_method.password, '1234pass') + def test_server_change_password_xml(self): + mock_method = MockSetAdminPassword() + self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method) + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = "application/xml" + req.body = """ + """ + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + self.assertEqual(mock_method.instance_id, '1') + self.assertEqual(mock_method.password, '1234pass') + def test_server_change_password_not_a_string(self): body = {'changePassword': {'adminPass': 1234}} req = webob.Request.blank('/v1.1/servers/1/action') @@ -511,6 +527,42 @@ class ServerActionsTestV11(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_server_reboot_hard(self): + body = dict(reboot=dict(type="HARD")) + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + + def test_server_reboot_soft(self): + body = dict(reboot=dict(type="SOFT")) + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + + def test_server_reboot_incorrect_type(self): + body = dict(reboot=dict(type="NOT_A_TYPE")) + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_server_reboot_missing_type(self): + body = dict(reboot=dict()) + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + def test_server_rebuild_accepted_minimum(self): body = { "rebuild": { @@ -653,6 +705,62 @@ class ServerActionsTestV11(test.TestCase): self.assertEqual(res.status_int, 202) self.assertEqual(self.resize_called, True) + def test_resize_server_no_flavor(self): + req = webob.Request.blank('/v1.1/servers/1/action') + req.content_type = 'application/json' + req.method = 'POST' + body_dict = dict(resize=dict()) + req.body = json.dumps(body_dict) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_resize_server_no_flavor_ref(self): + req = webob.Request.blank('/v1.1/servers/1/action') + req.content_type = 'application/json' + req.method = 'POST' + body_dict = dict(resize=dict(flavorRef=None)) + req.body = json.dumps(body_dict) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_confirm_resize_server(self): + req = webob.Request.blank('/v1.1/servers/1/action') + req.content_type = 'application/json' + req.method = 'POST' + body_dict = dict(confirmResize=None) + req.body = json.dumps(body_dict) + + self.confirm_resize_called = False + + def cr_mock(*args): + self.confirm_resize_called = True + + self.stubs.Set(nova.compute.api.API, 'confirm_resize', cr_mock) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 204) + self.assertEqual(self.confirm_resize_called, True) + + def test_revert_resize_server(self): + req = webob.Request.blank('/v1.1/servers/1/action') + req.content_type = 'application/json' + req.method = 'POST' + body_dict = dict(revertResize=None) + req.body = json.dumps(body_dict) + + self.revert_resize_called = False + + def revert_mock(*args): + self.revert_resize_called = True + + self.stubs.Set(nova.compute.api.API, 'revert_resize', revert_mock) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + self.assertEqual(self.revert_resize_called, True) + def test_create_image(self): body = { 'createImage': { @@ -668,6 +776,23 @@ class ServerActionsTestV11(test.TestCase): location = response.headers['Location'] self.assertEqual('http://localhost/v1.1/images/123', location) + def test_create_image_snapshots_disabled(self): + """Don't permit a snapshot if the allow_instance_snapshots flag is + False + """ + self.flags(allow_instance_snapshots=False) + body = { + 'createImage': { + 'name': 'Snapshot 1', + }, + } + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + def test_create_image_with_metadata(self): body = { 'createImage': { @@ -730,10 +855,10 @@ class ServerActionsTestV11(test.TestCase): self.assertTrue(response.headers['Location']) -class TestServerActionXMLDeserializer(test.TestCase): +class TestServerActionXMLDeserializerV11(test.TestCase): def setUp(self): - self.deserializer = create_instance_helper.ServerXMLDeserializer() + self.deserializer = create_instance_helper.ServerXMLDeserializerV11() def tearDown(self): pass @@ -746,7 +871,6 @@ class TestServerActionXMLDeserializer(test.TestCase): expected = { "createImage": { "name": "new-server-test", - "metadata": {}, }, } self.assertEquals(request['body'], expected) @@ -767,3 +891,147 @@ class TestServerActionXMLDeserializer(test.TestCase): }, } self.assertEquals(request['body'], expected) + + def test_change_pass(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "changePassword": { + "adminPass": "1234pass", + }, + } + self.assertEquals(request['body'], expected) + + def test_change_pass_no_pass(self): + serial_request = """ + """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_reboot(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "reboot": { + "type": "HARD", + }, + } + self.assertEquals(request['body'], expected) + + def test_reboot_no_type(self): + serial_request = """ + """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_resize(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "resize": { + "flavorRef": "http://localhost/flavors/3" + }, + } + self.assertEquals(request['body'], expected) + + def test_resize_no_flavor_ref(self): + serial_request = """ + """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_confirm_resize(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "confirmResize": None, + } + self.assertEquals(request['body'], expected) + + def test_revert_resize(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "revertResize": None, + } + self.assertEquals(request['body'], expected) + + def test_rebuild(self): + serial_request = """ + + + Apache1 + + + Mg== + + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "rebuild": { + "name": "new-server-test", + "imageRef": "http://localhost/images/1", + "metadata": { + "My Server Name": "Apache1", + }, + "personality": [ + {"path": "/etc/banner.txt", "contents": "Mg=="}, + ], + }, + } + self.assertDictMatch(request['body'], expected) + + def test_rebuild_minimum(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "rebuild": { + "imageRef": "http://localhost/images/1", + }, + } + self.assertDictMatch(request['body'], expected) + + def test_rebuild_no_imageRef(self): + serial_request = """ + + + Apache1 + + + Mg== + + """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 0477f6d92b1c..fd06b2e64263 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -2177,7 +2177,7 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): def setUp(self): super(TestServerCreateRequestXMLDeserializerV11, self).setUp() - self.deserializer = create_instance_helper.ServerXMLDeserializer() + self.deserializer = create_instance_helper.ServerXMLDeserializerV11() def test_minimal_request(self): serial_request = """ @@ -2191,8 +2191,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "name": "new-server-test", "imageRef": "1", "flavorRef": "2", - "metadata": {}, - "personality": [], }, } self.assertEquals(request['body'], expected) @@ -2211,8 +2209,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "imageRef": "1", "flavorRef": "2", "adminPass": "1234", - "metadata": {}, - "personality": [], }, } self.assertEquals(request['body'], expected) @@ -2229,8 +2225,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "name": "new-server-test", "imageRef": "http://localhost:8774/v1.1/images/2", "flavorRef": "3", - "metadata": {}, - "personality": [], }, } self.assertEquals(request['body'], expected) @@ -2247,8 +2241,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "name": "new-server-test", "imageRef": "1", "flavorRef": "http://localhost:8774/v1.1/flavors/3", - "metadata": {}, - "personality": [], }, } self.assertEquals(request['body'], expected) @@ -2292,7 +2284,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "imageRef": "1", "flavorRef": "2", "metadata": {"one": "two", "open": "snack"}, - "personality": [], }, } self.assertEquals(request['body'], expected) @@ -2314,7 +2305,6 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "name": "new-server-test", "imageRef": "1", "flavorRef": "2", - "metadata": {}, "personality": [ {"path": "/etc/banner.txt", "contents": "MQ=="}, {"path": "/etc/hosts", "contents": "Mg=="}, diff --git a/nova/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py index f60eb6433fde..330dab5e5582 100644 --- a/nova/tests/scheduler/test_scheduler.py +++ b/nova/tests/scheduler/test_scheduler.py @@ -21,9 +21,11 @@ Tests For Scheduler import datetime import mox -import novaclient.exceptions import stubout +from novaclient import v1_1 as novaclient +from novaclient import exceptions as novaclient_exceptions + from mox import IgnoreArg from nova import context from nova import db @@ -1036,10 +1038,10 @@ class FakeServerCollection(object): class FakeEmptyServerCollection(object): def get(self, f): - raise novaclient.NotFound(1) + raise novaclient_exceptions.NotFound(1) def find(self, name): - raise novaclient.NotFound(2) + raise novaclient_exceptions.NotFound(2) class FakeNovaClient(object): @@ -1085,7 +1087,7 @@ class FakeZonesProxy(object): raise Exception('testing') -class FakeNovaClientOpenStack(object): +class FakeNovaClientZones(object): def __init__(self, *args, **kwargs): self.zones = FakeZonesProxy() @@ -1098,7 +1100,7 @@ class CallZoneMethodTest(test.TestCase): super(CallZoneMethodTest, self).setUp() self.stubs = stubout.StubOutForTesting() self.stubs.Set(db, 'zone_get_all', zone_get_all) - self.stubs.Set(novaclient, 'OpenStack', FakeNovaClientOpenStack) + self.stubs.Set(novaclient, 'Client', FakeNovaClientZones) def tearDown(self): self.stubs.UnsetAll() diff --git a/nova/tests/scheduler/test_zone_aware_scheduler.py b/nova/tests/scheduler/test_zone_aware_scheduler.py index 7833028c3d34..788efca52344 100644 --- a/nova/tests/scheduler/test_zone_aware_scheduler.py +++ b/nova/tests/scheduler/test_zone_aware_scheduler.py @@ -21,7 +21,9 @@ import json import nova.db from nova import exception +from nova import rpc from nova import test +from nova.compute import api as compute_api from nova.scheduler import driver from nova.scheduler import zone_aware_scheduler from nova.scheduler import zone_manager @@ -114,7 +116,7 @@ def fake_provision_resource_from_blob(context, item, instance_id, def fake_decrypt_blob_returns_local_info(blob): - return {'foo': True} # values aren't important. + return {'hostname': 'foooooo'} # values aren't important. def fake_decrypt_blob_returns_child_info(blob): @@ -283,14 +285,29 @@ class ZoneAwareSchedulerTestCase(test.TestCase): global was_called sched = FakeZoneAwareScheduler() was_called = False + + def fake_create_db_entry_for_new_instance(self, context, + image, base_options, security_group, + block_device_mapping, num=1): + global was_called + was_called = True + # return fake instances + return {'id': 1, 'uuid': 'f874093c-7b17-49c0-89c3-22a5348497f9'} + + def fake_rpc_cast(*args, **kwargs): + pass + self.stubs.Set(sched, '_decrypt_blob', fake_decrypt_blob_returns_local_info) - self.stubs.Set(sched, '_provision_resource_locally', - fake_provision_resource_locally) + self.stubs.Set(compute_api.API, + 'create_db_entry_for_new_instance', + fake_create_db_entry_for_new_instance) + self.stubs.Set(rpc, 'cast', fake_rpc_cast) - request_spec = {'blob': "Non-None blob data"} + build_plan_item = {'blob': "Non-None blob data"} + request_spec = {'image': {}, 'instance_properties': {}} - sched._provision_resource_from_blob(None, request_spec, 1, + sched._provision_resource_from_blob(None, build_plan_item, 1, request_spec, {}) self.assertTrue(was_called) diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py index 548f81f8babe..a724db9dae15 100644 --- a/nova/tests/test_hosts.py +++ b/nova/tests/test_hosts.py @@ -48,6 +48,10 @@ def stub_set_host_enabled(context, host, enabled): return status +def stub_host_power_action(context, host, action): + return action + + class FakeRequest(object): environ = {"nova.context": context.get_admin_context()} @@ -62,6 +66,8 @@ class HostTestCase(test.TestCase): self.stubs.Set(scheduler_api, 'get_host_list', stub_get_host_list) self.stubs.Set(self.controller.compute_api, 'set_host_enabled', stub_set_host_enabled) + self.stubs.Set(self.controller.compute_api, 'host_power_action', + stub_host_power_action) def test_list_hosts(self): """Verify that the compute hosts are returned.""" @@ -87,6 +93,18 @@ class HostTestCase(test.TestCase): result_c2 = self.controller.update(self.req, "host_c2", body=en_body) self.assertEqual(result_c2["status"], "disabled") + def test_host_startup(self): + result = self.controller.startup(self.req, "host_c1") + self.assertEqual(result["power_action"], "startup") + + def test_host_shutdown(self): + result = self.controller.shutdown(self.req, "host_c1") + self.assertEqual(result["power_action"], "shutdown") + + def test_host_reboot(self): + result = self.controller.reboot(self.req, "host_c1") + self.assertEqual(result["power_action"], "reboot") + def test_bad_status_value(self): bad_body = {"status": "bad"} self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, diff --git a/nova/tests/test_image.py b/nova/tests/test_image.py new file mode 100644 index 000000000000..9680d6f2b2a1 --- /dev/null +++ b/nova/tests/test_image.py @@ -0,0 +1,134 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC +# Author: Soren Hansen +# +# 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 datetime + +from nova import context +from nova import exception +from nova import test +import nova.image + + +class _ImageTestCase(test.TestCase): + def setUp(self): + super(_ImageTestCase, self).setUp() + self.context = context.get_admin_context() + + def test_index(self): + res = self.image_service.index(self.context) + for image in res: + self.assertEquals(set(image.keys()), set(['id', 'name'])) + + def test_detail(self): + res = self.image_service.detail(self.context) + for image in res: + keys = set(image.keys()) + self.assertEquals(keys, set(['id', 'name', 'created_at', + 'updated_at', 'deleted_at', 'deleted', + 'status', 'is_public', 'properties'])) + self.assertTrue(isinstance(image['created_at'], datetime.datetime)) + self.assertTrue(isinstance(image['updated_at'], datetime.datetime)) + + if not (isinstance(image['deleted_at'], datetime.datetime) or + image['deleted_at'] is None): + self.fail('image\'s "deleted_at" attribute was neither a ' + 'datetime object nor None') + + def check_is_bool(image, key): + val = image.get('deleted') + if not isinstance(val, bool): + self.fail('image\'s "%s" attribute wasn\'t ' + 'a bool: %r' % (key, val)) + + check_is_bool(image, 'deleted') + check_is_bool(image, 'is_public') + + def test_index_and_detail_have_same_results(self): + index = self.image_service.index(self.context) + detail = self.image_service.detail(self.context) + index_set = set([(i['id'], i['name']) for i in index]) + detail_set = set([(i['id'], i['name']) for i in detail]) + self.assertEqual(index_set, detail_set) + + def test_show_raises_imagenotfound_for_invalid_id(self): + self.assertRaises(exception.ImageNotFound, + self.image_service.show, + self.context, + 'this image does not exist') + + def test_show_by_name(self): + self.assertRaises(exception.ImageNotFound, + self.image_service.show_by_name, + self.context, + 'this image does not exist') + + def test_create_adds_id(self): + index = self.image_service.index(self.context) + image_count = len(index) + + self.image_service.create(self.context, {}) + + index = self.image_service.index(self.context) + self.assertEquals(len(index), image_count + 1) + + self.assertTrue(index[0]['id']) + + def test_create_keeps_id(self): + self.image_service.create(self.context, {'id': '34'}) + self.image_service.show(self.context, '34') + + def test_create_rejects_duplicate_ids(self): + self.image_service.create(self.context, {'id': '34'}) + self.assertRaises(exception.Duplicate, + self.image_service.create, + self.context, + {'id': '34'}) + + # Make sure there's still one left + self.image_service.show(self.context, '34') + + def test_update(self): + self.image_service.create(self.context, + {'id': '34', 'foo': 'bar'}) + + self.image_service.update(self.context, '34', + {'id': '34', 'foo': 'baz'}) + + img = self.image_service.show(self.context, '34') + self.assertEquals(img['foo'], 'baz') + + def test_delete(self): + self.image_service.create(self.context, {'id': '34', 'foo': 'bar'}) + self.image_service.delete(self.context, '34') + self.assertRaises(exception.NotFound, + self.image_service.show, + self.context, + '34') + + def test_delete_all(self): + self.image_service.create(self.context, {'id': '32', 'foo': 'bar'}) + self.image_service.create(self.context, {'id': '33', 'foo': 'bar'}) + self.image_service.create(self.context, {'id': '34', 'foo': 'bar'}) + self.image_service.delete_all() + index = self.image_service.index(self.context) + self.assertEquals(len(index), 0) + + +class FakeImageTestCase(_ImageTestCase): + def setUp(self): + super(FakeImageTestCase, self).setUp() + self.image_service = nova.image.fake.FakeImageService() diff --git a/nova/tests/test_nova_manage.py b/nova/tests/test_nova_manage.py new file mode 100644 index 000000000000..9c6563f14c71 --- /dev/null +++ b/nova/tests/test_nova_manage.py @@ -0,0 +1,82 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC +# Copyright 2011 Ilya Alekseyev +# +# 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 os +import sys + +TOPDIR = os.path.normpath(os.path.join( + os.path.dirname(os.path.abspath(__file__)), + os.pardir, + os.pardir)) +NOVA_MANAGE_PATH = os.path.join(TOPDIR, 'bin', 'nova-manage') + +sys.dont_write_bytecode = True +import imp +nova_manage = imp.load_source('nova_manage.py', NOVA_MANAGE_PATH) +sys.dont_write_bytecode = False + +import netaddr +from nova import context +from nova import db +from nova import flags +from nova import test + +FLAGS = flags.FLAGS + + +class FixedIpCommandsTestCase(test.TestCase): + def setUp(self): + super(FixedIpCommandsTestCase, self).setUp() + cidr = '10.0.0.0/24' + net = netaddr.IPNetwork(cidr) + net_info = {'bridge': 'fakebr', + 'bridge_interface': 'fakeeth', + 'dns': FLAGS.flat_network_dns, + 'cidr': cidr, + 'netmask': str(net.netmask), + 'gateway': str(net[1]), + 'broadcast': str(net.broadcast), + 'dhcp_start': str(net[2])} + self.network = db.network_create_safe(context.get_admin_context(), + net_info) + num_ips = len(net) + for index in range(num_ips): + address = str(net[index]) + reserved = (index == 1 or index == 2) + db.fixed_ip_create(context.get_admin_context(), + {'network_id': self.network['id'], + 'address': address, + 'reserved': reserved}) + self.commands = nova_manage.FixedIpCommands() + + def tearDown(self): + db.network_delete_safe(context.get_admin_context(), self.network['id']) + super(FixedIpCommandsTestCase, self).tearDown() + + def test_reserve(self): + self.commands.reserve('10.0.0.100') + address = db.fixed_ip_get_by_address(context.get_admin_context(), + '10.0.0.100') + self.assertEqual(address['reserved'], True) + + def test_unreserve(self): + db.fixed_ip_update(context.get_admin_context(), '10.0.0.100', + {'reserved': True}) + self.commands.unreserve('10.0.0.100') + address = db.fixed_ip_get_by_address(context.get_admin_context(), + '10.0.0.100') + self.assertEqual(address['reserved'], False) diff --git a/nova/tests/test_skip_examples.py b/nova/tests/test_skip_examples.py new file mode 100644 index 000000000000..8ca2034423f9 --- /dev/null +++ b/nova/tests/test_skip_examples.py @@ -0,0 +1,47 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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 nova import test + + +class ExampleSkipTestCase(test.TestCase): + test_counter = 0 + + @test.skip_test("Example usage of @test.skip_test()") + def test_skip_test_example(self): + self.fail("skip_test failed to work properly.") + + @test.skip_if(True, "Example usage of @test.skip_if()") + def test_skip_if_example(self): + self.fail("skip_if failed to work properly.") + + @test.skip_unless(False, "Example usage of @test.skip_unless()") + def test_skip_unless_example(self): + self.fail("skip_unless failed to work properly.") + + @test.skip_if(False, "This test case should never be skipped.") + def test_001_increase_test_counter(self): + ExampleSkipTestCase.test_counter += 1 + + @test.skip_unless(True, "This test case should never be skipped.") + def test_002_increase_test_counter(self): + ExampleSkipTestCase.test_counter += 1 + + def test_003_verify_test_counter(self): + self.assertEquals(ExampleSkipTestCase.test_counter, 2, + "Tests were not skipped appropriately") diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 39ab23d9b4a5..dfc1eeb0aa75 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -767,6 +767,52 @@ class XenAPIMigrateInstance(test.TestCase): conn = xenapi_conn.get_connection(False) conn.migrate_disk_and_power_off(instance, '127.0.0.1') + def test_revert_migrate(self): + instance = db.instance_create(self.context, self.values) + self.called = False + self.fake_vm_start_called = False + self.fake_revert_migration_called = False + + def fake_vm_start(*args, **kwargs): + self.fake_vm_start_called = True + + def fake_vdi_resize(*args, **kwargs): + self.called = True + + def fake_revert_migration(*args, **kwargs): + self.fake_revert_migration_called = True + + self.stubs.Set(stubs.FakeSessionForMigrationTests, + "VDI_resize_online", fake_vdi_resize) + self.stubs.Set(vmops.VMOps, '_start', fake_vm_start) + self.stubs.Set(vmops.VMOps, 'revert_migration', fake_revert_migration) + + stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests) + stubs.stubout_loopingcall_start(self.stubs) + conn = xenapi_conn.get_connection(False) + network_info = [({'bridge': 'fa0', 'id': 0, 'injected': False}, + {'broadcast': '192.168.0.255', + 'dns': ['192.168.0.1'], + 'gateway': '192.168.0.1', + 'gateway6': 'dead:beef::1', + 'ip6s': [{'enabled': '1', + 'ip': 'dead:beef::dcad:beff:feef:0', + 'netmask': '64'}], + 'ips': [{'enabled': '1', + 'ip': '192.168.0.100', + 'netmask': '255.255.255.0'}], + 'label': 'fake', + 'mac': 'DE:AD:BE:EF:00:00', + 'rxtx_cap': 3})] + conn.finish_migration(self.context, instance, + dict(base_copy='hurr', cow='durr'), + network_info, resize_instance=True) + self.assertEqual(self.called, True) + self.assertEqual(self.fake_vm_start_called, True) + + conn.revert_migration(instance) + self.assertEqual(self.fake_revert_migration_called, True) + def test_finish_migrate(self): instance = db.instance_create(self.context, self.values) self.called = False diff --git a/nova/tests/test_zones.py b/nova/tests/test_zones.py index a943fee27b82..9efa23015ddd 100644 --- a/nova/tests/test_zones.py +++ b/nova/tests/test_zones.py @@ -18,7 +18,6 @@ Tests For ZoneManager import datetime import mox -import novaclient from nova import context from nova import db diff --git a/nova/utils.py b/nova/utils.py index da8826f1f454..372358b42a0b 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -126,6 +126,22 @@ def fetchfile(url, target): def execute(*cmd, **kwargs): + """ + Helper method to execute command with optional retry. + + :cmd Passed to subprocess.Popen. + :process_input Send to opened process. + :addl_env Added to the processes env. + :check_exit_code Defaults to 0. Raise exception.ProcessExecutionError + unless program exits with this code. + :delay_on_retry True | False. Defaults to True. If set to True, wait a + short amount of time before retrying. + :attempts How many times to retry cmd. + + :raises exception.Error on receiving unknown arguments + :raises exception.ProcessExecutionError + """ + process_input = kwargs.pop('process_input', None) addl_env = kwargs.pop('addl_env', None) check_exit_code = kwargs.pop('check_exit_code', 0) @@ -790,7 +806,7 @@ def parse_server_string(server_str): (address, port) = server_str.split(':') return (address, port) - except: + except Exception: LOG.debug(_('Invalid server_string: %s' % server_str)) return ('', '') diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 4f3cfefaddae..5d73eefc75cd 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -282,6 +282,10 @@ class ComputeDriver(object): # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() + def host_power_action(self, host, action): + """Reboots, shuts down or powers up the host.""" + raise NotImplementedError() + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" # TODO(Vek): Need to pass context in for access to auth_token diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 80abcc644235..89ad20494e60 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -512,6 +512,10 @@ class FakeConnection(driver.ComputeDriver): """Return fake Host Status of ram, disk, network.""" return self.host_status + def host_power_action(self, host, action): + """Reboots, shuts down or powers up the host.""" + pass + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 3428a7fc180c..ae30c62f0145 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -499,6 +499,10 @@ class HyperVConnection(driver.ComputeDriver): """See xenapi_conn.py implementation.""" pass + def host_power_action(self, host, action): + """Reboots, shuts down or powers up the host.""" + pass + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index f7aae41128b9..7655bf386a64 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -349,7 +349,7 @@ class LibvirtConnection(driver.ComputeDriver): """Returns the xml for the disk mounted at device""" try: doc = libxml2.parseDoc(xml) - except: + except Exception: return None ctx = doc.xpathNewContext() try: @@ -392,9 +392,7 @@ class LibvirtConnection(driver.ComputeDriver): nova.image.get_image_service(image_href) snapshot = snapshot_image_service.show(context, snapshot_image_id) - metadata = {'disk_format': base['disk_format'], - 'container_format': base['container_format'], - 'is_public': False, + metadata = {'is_public': False, 'status': 'active', 'name': snapshot['name'], 'properties': { @@ -409,6 +407,12 @@ class LibvirtConnection(driver.ComputeDriver): arch = base['properties']['architecture'] metadata['properties']['architecture'] = arch + if 'disk_format' in base: + metadata['disk_format'] = base['disk_format'] + + if 'container_format' in base: + metadata['container_format'] = base['container_format'] + # Make the snapshot snapshot_name = uuid.uuid4().hex snapshot_xml = """ @@ -880,7 +884,7 @@ class LibvirtConnection(driver.ComputeDriver): 'netmask': netmask, 'gateway': mapping['gateway'], 'broadcast': mapping['broadcast'], - 'dns': mapping['dns'], + 'dns': ' '.join(mapping['dns']), 'address_v6': address_v6, 'gateway6': gateway_v6, 'netmask_v6': netmask_v6} @@ -1077,7 +1081,7 @@ class LibvirtConnection(driver.ComputeDriver): try: doc = libxml2.parseDoc(xml) - except: + except Exception: return [] ctx = doc.xpathNewContext() @@ -1118,7 +1122,7 @@ class LibvirtConnection(driver.ComputeDriver): try: doc = libxml2.parseDoc(xml) - except: + except Exception: return [] ctx = doc.xpathNewContext() @@ -1558,6 +1562,10 @@ class LibvirtConnection(driver.ComputeDriver): """See xenapi_conn.py implementation.""" pass + def host_power_action(self, host, action): + """Reboots, shuts down or powers up the host.""" + pass + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py index eef582fac6df..711b05baed15 100644 --- a/nova/virt/libvirt/vif.py +++ b/nova/virt/libvirt/vif.py @@ -25,6 +25,7 @@ from nova.network import linux_net from nova.virt.libvirt import netutils from nova import utils from nova.virt.vif import VIFDriver +from nova import exception LOG = logging.getLogger('nova.virt.libvirt.vif') @@ -128,7 +129,7 @@ class LibvirtOpenVswitchDriver(VIFDriver): utils.execute('sudo', 'ovs-vsctl', 'del-port', network['bridge'], dev) utils.execute('sudo', 'ip', 'link', 'delete', dev) - except: + except exception.ProcessExecutionError: LOG.warning(_("Failed while unplugging vif of instance '%s'"), instance['name']) raise diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py index 3d209fa99009..aaa384374716 100644 --- a/nova/virt/vmwareapi_conn.py +++ b/nova/virt/vmwareapi_conn.py @@ -191,6 +191,10 @@ class VMWareESXConnection(driver.ComputeDriver): """This method is supported only by libvirt.""" return + def host_power_action(self, host, action): + """Reboots, shuts down or powers up the host.""" + pass + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" pass diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index a78413370174..b913e764e3fb 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -1031,11 +1031,23 @@ class VMOps(object): # TODO: implement this! return 'http://fakeajaxconsole/fake_url' + def host_power_action(self, host, action): + """Reboots or shuts down the host.""" + args = {"action": json.dumps(action)} + methods = {"reboot": "host_reboot", "shutdown": "host_shutdown"} + json_resp = self._call_xenhost(methods[action], args) + resp = json.loads(json_resp) + return resp["power_action"] + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" args = {"enabled": json.dumps(enabled)} - json_resp = self._call_xenhost("set_host_enabled", args) - resp = json.loads(json_resp) + xenapi_resp = self._call_xenhost("set_host_enabled", args) + try: + resp = json.loads(xenapi_resp) + except TypeError as e: + # Already logged; return the message + return xenapi_resp.details[-1] return resp["status"] def _call_xenhost(self, method, arg_dict): @@ -1051,7 +1063,7 @@ class VMOps(object): #args={"params": arg_dict}) ret = self._session.wait_for_task(task, task_id) except self.XenAPI.Failure as e: - ret = None + ret = e LOG.error(_("The call to %(method)s returned an error: %(e)s.") % locals()) return ret diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 91d9d8530370..a1c9a1e30937 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -191,7 +191,7 @@ class XenAPIConnection(driver.ComputeDriver): def revert_migration(self, instance): """Reverts a resize, powering back on the instance""" - self._vmops.revert_resize(instance) + self._vmops.revert_migration(instance) def finish_migration(self, context, instance, disk_info, network_info, resize_instance=False): @@ -332,6 +332,19 @@ class XenAPIConnection(driver.ComputeDriver): True, run the update first.""" return self.HostState.get_host_stats(refresh=refresh) + def host_power_action(self, host, action): + """The only valid values for 'action' on XenServer are 'reboot' or + 'shutdown', even though the API also accepts 'startup'. As this is + not technically possible on XenServer, since the host is the same + physical machine as the hypervisor, if this is requested, we need to + raise an exception. + """ + if action in ("reboot", "shutdown"): + return self._vmops.host_power_action(host, action) + else: + msg = _("Host startup on XenServer is not supported.") + raise NotImplementedError(msg) + def set_host_enabled(self, host, enabled): """Sets the specified host's ability to accept new instances.""" return self._vmops.set_host_enabled(host, enabled) @@ -440,7 +453,7 @@ class XenAPISession(object): params = None try: params = eval(exc.details[3]) - except: + except Exception: raise exc raise self.XenAPI.Failure(params) else: diff --git a/nova/vnc/proxy.py b/nova/vnc/proxy.py index c4603803b10e..2e3e38ca9506 100644 --- a/nova/vnc/proxy.py +++ b/nova/vnc/proxy.py @@ -60,7 +60,7 @@ class WebsocketVNCProxy(object): break d = base64.b64encode(d) dest.send(d) - except: + except Exception: source.close() dest.close() @@ -72,7 +72,7 @@ class WebsocketVNCProxy(object): break d = base64.b64decode(d) dest.sendall(d) - except: + except Exception: source.close() dest.close() diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance index 17a5dbbfb019..a063128901a8 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance @@ -261,6 +261,9 @@ def _upload_tarball(staging_path, image_id, glance_host, glance_port, os_type, if header.lower().startswith("x-image-meta-property-"): headers[header.lower()] = value + # Toss body so connection state-machine is ready for next request/response + resp.read() + # NOTE(sirp): httplib under python2.4 won't accept a file-like object # to request conn.putrequest('PUT', '/v1/images/%s' % image_id) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index 292bbce12440..cd9694ce121b 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -39,6 +39,7 @@ import pluginlib_nova as pluginlib pluginlib.configure_logging("xenhost") host_data_pattern = re.compile(r"\s*(\S+) \([^\)]+\) *: ?(.*)") +config_file_path = "/usr/etc/xenhost.conf" def jsonify(fnc): @@ -103,6 +104,104 @@ def set_host_enabled(self, arg_dict): return {"status": status} +def _write_config_dict(dct): + conf_file = file(config_file_path, "w") + json.dump(dct, conf_file) + conf_file.close() + + +def _get_config_dict(): + """Returns a dict containing the key/values in the config file. + If the file doesn't exist, it is created, and an empty dict + is returned. + """ + try: + conf_file = file(config_file_path) + config_dct = json.load(conf_file) + conf_file.close() + except IOError: + # File doesn't exist + config_dct = {} + # Create the file + _write_config_dict(config_dct) + return config_dct + + +@jsonify +def get_config(self, arg_dict): + """Return the value stored for the specified key, or None if no match.""" + conf = _get_config_dict() + params = arg_dict["params"] + try: + dct = json.loads(params) + except Exception, e: + dct = params + key = dct["key"] + ret = conf.get(key) + if ret is None: + # Can't jsonify None + return "None" + return ret + + +@jsonify +def set_config(self, arg_dict): + """Write the specified key/value pair, overwriting any existing value.""" + conf = _get_config_dict() + params = arg_dict["params"] + try: + dct = json.loads(params) + except Exception, e: + dct = params + key = dct["key"] + val = dct["value"] + if val is None: + # Delete the key, if present + conf.pop(key, None) + else: + conf.update({key: val}) + _write_config_dict(conf) + + +def _power_action(action): + host_uuid = _get_host_uuid() + # Host must be disabled first + result = _run_command("xe host-disable") + if result: + raise pluginlib.PluginError(result) + # All running VMs must be shutdown + result = _run_command("xe vm-shutdown --multiple power-state=running") + if result: + raise pluginlib.PluginError(result) + cmds = {"reboot": "xe host-reboot", "startup": "xe host-power-on", + "shutdown": "xe host-shutdown"} + result = _run_command(cmds[action]) + # Should be empty string + if result: + raise pluginlib.PluginError(result) + return {"power_action": action} + + +@jsonify +def host_reboot(self, arg_dict): + """Reboots the host.""" + return _power_action("reboot") + + +@jsonify +def host_shutdown(self, arg_dict): + """Reboots the host.""" + return _power_action("shutdown") + + +@jsonify +def host_start(self, arg_dict): + """Starts the host. Currently not feasible, since the host + runs on the same machine as Xen. + """ + return _power_action("startup") + + @jsonify def host_data(self, arg_dict): """Runs the commands on the xenstore host to return the current status @@ -115,6 +214,9 @@ def host_data(self, arg_dict): # We have the raw dict of values. Extract those that we need, # and convert the data types as needed. ret_dict = cleanup(parsed_data) + # Add any config settings + config = _get_config_dict() + ret_dict.update(config) return ret_dict @@ -217,4 +319,9 @@ def cleanup(dct): if __name__ == "__main__": XenAPIPlugin.dispatch( {"host_data": host_data, - "set_host_enabled": set_host_enabled}) + "set_host_enabled": set_host_enabled, + "host_shutdown": host_shutdown, + "host_reboot": host_reboot, + "host_start": host_start, + "get_config": get_config, + "set_config": set_config}) diff --git a/smoketests/test_netadmin.py b/smoketests/test_netadmin.py index 60086f0651ea..de69c98a2804 100644 --- a/smoketests/test_netadmin.py +++ b/smoketests/test_netadmin.py @@ -115,7 +115,8 @@ class SecurityGroupTests(base.UserSmokeTestCase): if not instance_id: return False if instance_id != self.data['instance'].id: - raise Exception("Wrong instance id") + raise Exception("Wrong instance id. Expected: %s, Got: %s" % + (self.data['instance'].id, instance_id)) return True def test_001_can_create_security_group(self): diff --git a/tools/pip-requires b/tools/pip-requires index b98b70937d63..60b502ffd23e 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -10,7 +10,7 @@ carrot==0.10.5 eventlet lockfile==0.8 lxml==2.3 -python-novaclient==2.5.9 +python-novaclient==2.6.0 python-daemon==1.5.5 python-gflags==1.3 redis==2.0.0