diff options
192 files changed, 10906 insertions, 1802 deletions
diff --git a/bin/nova-cells b/bin/nova-cells new file mode 100755 index 000000000..a7e16ef53 --- /dev/null +++ b/bin/nova-cells @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (c) 2012 Rackspace Hosting +# 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. + +"""Starter script for Nova Cells Service.""" + +import eventlet +eventlet.monkey_patch() + +import os +import sys + +# If ../nova/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): + sys.path.insert(0, possible_topdir) + +from nova import config +from nova.openstack.common import cfg +from nova.openstack.common import log as logging +from nova import service +from nova import utils + +CONF = cfg.CONF +CONF.import_opt('topic', 'nova.cells.opts', group='cells') +CONF.import_opt('manager', 'nova.cells.opts', group='cells') + +if __name__ == '__main__': + config.parse_args(sys.argv) + logging.setup('nova') + utils.monkey_patch() + server = service.Service.create(binary='nova-cells', + topic=CONF.cells.topic, + manager=CONF.cells.manager) + service.serve(server) + service.wait() diff --git a/bin/nova-novncproxy b/bin/nova-novncproxy index 6c911af1e..beee143f5 100755 --- a/bin/nova-novncproxy +++ b/bin/nova-novncproxy @@ -17,8 +17,8 @@ # under the License. ''' -Websocket proxy that is compatible with OpenStack Nova. -Leverages websockify.py by Joel Martin +Websocket proxy that is compatible with OpenStack Nova +noVNC consoles. Leverages websockify.py by Joel Martin ''' import Cookie diff --git a/doc/api_samples/OS-FLV-DISABLED/flavor-detail-get-resp.json b/doc/api_samples/OS-FLV-DISABLED/flavor-detail-get-resp.json new file mode 100644 index 000000000..42e0e21ce --- /dev/null +++ b/doc/api_samples/OS-FLV-DISABLED/flavor-detail-get-resp.json @@ -0,0 +1,94 @@ +{ + "flavors": [ + { + "OS-FLV-DISABLED:disabled": false, + "disk": 0, + "id": "1", + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/flavors/1", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/flavors/1", + "rel": "bookmark" + } + ], + "name": "m1.tiny", + "ram": 512, + "vcpus": 1 + }, + { + "OS-FLV-DISABLED:disabled": false, + "disk": 20, + "id": "2", + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/flavors/2", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/flavors/2", + "rel": "bookmark" + } + ], + "name": "m1.small", + "ram": 2048, + "vcpus": 1 + }, + { + "OS-FLV-DISABLED:disabled": false, + "disk": 40, + "id": "3", + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/flavors/3", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/flavors/3", + "rel": "bookmark" + } + ], + "name": "m1.medium", + "ram": 4096, + "vcpus": 2 + }, + { + "OS-FLV-DISABLED:disabled": false, + "disk": 80, + "id": "4", + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/flavors/4", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/flavors/4", + "rel": "bookmark" + } + ], + "name": "m1.large", + "ram": 8192, + "vcpus": 4 + }, + { + "OS-FLV-DISABLED:disabled": false, + "disk": 160, + "id": "5", + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/flavors/5", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/flavors/5", + "rel": "bookmark" + } + ], + "name": "m1.xlarge", + "ram": 16384, + "vcpus": 8 + } + ] +}
\ No newline at end of file diff --git a/doc/api_samples/OS-FLV-DISABLED/flavor-detail-get-resp.xml b/doc/api_samples/OS-FLV-DISABLED/flavor-detail-get-resp.xml new file mode 100644 index 000000000..92b51a866 --- /dev/null +++ b/doc/api_samples/OS-FLV-DISABLED/flavor-detail-get-resp.xml @@ -0,0 +1,23 @@ +<?xml version='1.0' encoding='UTF-8'?> +<flavors xmlns:OS-FLV-DISABLED="http://docs.openstack.org/compute/ext/flavor_disabled/api/v1.1" xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1"> + <flavor disk="0" vcpus="1" ram="512" name="m1.tiny" id="1" OS-FLV-DISABLED:disabled="False"> + <atom:link href="http://openstack.example.com/v2/openstack/flavors/1" rel="self"/> + <atom:link href="http://openstack.example.com/openstack/flavors/1" rel="bookmark"/> + </flavor> + <flavor disk="20" vcpus="1" ram="2048" name="m1.small" id="2" OS-FLV-DISABLED:disabled="False"> + <atom:link href="http://openstack.example.com/v2/openstack/flavors/2" rel="self"/> + <atom:link href="http://openstack.example.com/openstack/flavors/2" rel="bookmark"/> + </flavor> + <flavor disk="40" vcpus="2" ram="4096" name="m1.medium" id="3" OS-FLV-DISABLED:disabled="False"> + <atom:link href="http://openstack.example.com/v2/openstack/flavors/3" rel="self"/> + <atom:link href="http://openstack.example.com/openstack/flavors/3" rel="bookmark"/> + </flavor> + <flavor disk="80" vcpus="4" ram="8192" name="m1.large" id="4" OS-FLV-DISABLED:disabled="False"> + <atom:link href="http://openstack.example.com/v2/openstack/flavors/4" rel="self"/> + <atom:link href="http://openstack.example.com/openstack/flavors/4" rel="bookmark"/> + </flavor> + <flavor disk="160" vcpus="8" ram="16384" name="m1.xlarge" id="5" OS-FLV-DISABLED:disabled="False"> + <atom:link href="http://openstack.example.com/v2/openstack/flavors/5" rel="self"/> + <atom:link href="http://openstack.example.com/openstack/flavors/5" rel="bookmark"/> + </flavor> +</flavors>
\ No newline at end of file diff --git a/doc/api_samples/OS-FLV-DISABLED/flavor-show-get-resp.json b/doc/api_samples/OS-FLV-DISABLED/flavor-show-get-resp.json new file mode 100644 index 000000000..3fd5fa1d7 --- /dev/null +++ b/doc/api_samples/OS-FLV-DISABLED/flavor-show-get-resp.json @@ -0,0 +1,20 @@ +{ + "flavor": { + "OS-FLV-DISABLED:disabled": false, + "disk": 0, + "id": "1", + "links": [ + { + "href": "http://openstack.example.com/v2/openstack/flavors/1", + "rel": "self" + }, + { + "href": "http://openstack.example.com/openstack/flavors/1", + "rel": "bookmark" + } + ], + "name": "m1.tiny", + "ram": 512, + "vcpus": 1 + } +}
\ No newline at end of file diff --git a/doc/api_samples/OS-FLV-DISABLED/flavor-show-get-resp.xml b/doc/api_samples/OS-FLV-DISABLED/flavor-show-get-resp.xml new file mode 100644 index 000000000..6abd22641 --- /dev/null +++ b/doc/api_samples/OS-FLV-DISABLED/flavor-show-get-resp.xml @@ -0,0 +1,5 @@ +<?xml version='1.0' encoding='UTF-8'?> +<flavor xmlns:OS-FLV-DISABLED="http://docs.openstack.org/compute/ext/flavor_disabled/api/v1.1" xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" disk="0" vcpus="1" ram="512" name="m1.tiny" id="1" OS-FLV-DISABLED:disabled="False"> + <atom:link href="http://openstack.example.com/v2/openstack/flavors/1" rel="self"/> + <atom:link href="http://openstack.example.com/openstack/flavors/1" rel="bookmark"/> +</flavor>
\ No newline at end of file diff --git a/doc/api_samples/os-coverage/coverage-stop-post-resp.json b/doc/api_samples/os-coverage/coverage-stop-post-resp.json new file mode 100644 index 000000000..d3caf3a5a --- /dev/null +++ b/doc/api_samples/os-coverage/coverage-stop-post-resp.json @@ -0,0 +1,3 @@ +{ + "path": "/tmp/tmpua9HvB/nova-coverage_rs2CaS" +}
\ No newline at end of file diff --git a/doc/api_samples/os-coverage/coverage-stop-post-resp.xml b/doc/api_samples/os-coverage/coverage-stop-post-resp.xml new file mode 100644 index 000000000..f0c921847 --- /dev/null +++ b/doc/api_samples/os-coverage/coverage-stop-post-resp.xml @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8'?> +<path>/tmp/tmpCLve38/nova-coverage_GJ4BZ_</path>
\ No newline at end of file diff --git a/doc/source/man/nova-novncproxy.rst b/doc/source/man/nova-novncproxy.rst index 92371a1a1..5302fd063 100644 --- a/doc/source/man/nova-novncproxy.rst +++ b/doc/source/man/nova-novncproxy.rst @@ -2,9 +2,9 @@ nova-novncproxy =============== -------------------------------------------- -Websocket novnc Proxy for OpenStack Nova. -------------------------------------------- +-------------------------------------------------------- +Websocket novnc Proxy for OpenStack Nova noVNC consoles. +-------------------------------------------------------- :Author: openstack@lists.launchpad.net :Date: 2012-09-27 @@ -21,7 +21,8 @@ SYNOPSIS DESCRIPTION =========== -Websocket proxy that is compatible with OpenStack Nova. +Websocket proxy that is compatible with OpenStack Nova +noVNC consoles. OPTIONS ======= @@ -40,7 +41,6 @@ SEE ALSO ======== * `OpenStack Nova <http://nova.openstack.org>`__ -* `OpenStack Nova <http://nova.openstack.org>`__ BUGS ==== diff --git a/etc/nova/rootwrap.d/baremetal-compute-ipmi.filters b/etc/nova/rootwrap.d/baremetal-compute-ipmi.filters new file mode 100644 index 000000000..a2858cd11 --- /dev/null +++ b/etc/nova/rootwrap.d/baremetal-compute-ipmi.filters @@ -0,0 +1,9 @@ +# nova-rootwrap command filters for compute nodes +# This file should be owned by (and only-writeable by) the root user + +[Filters] +# nova/virt/baremetal/ipmi.py: 'ipmitool', .. +ipmitool: CommandFilter, /usr/bin/ipmitool, root + +# nova/virt/baremetal/ipmi.py: 'kill', '-TERM', str(console_pid) +kill_shellinaboxd: KillFilter, root, /usr/local/bin/shellinaboxd, -15, -TERM diff --git a/etc/nova/rootwrap.d/baremetal-compute-pxe.filters b/etc/nova/rootwrap.d/baremetal-compute-pxe.filters new file mode 100644 index 000000000..35fa61723 --- /dev/null +++ b/etc/nova/rootwrap.d/baremetal-compute-pxe.filters @@ -0,0 +1,11 @@ +# nova-rootwrap command filters for compute nodes +# This file should be owned by (and only-writeable by) the root user + +[Filters] + +# nova/virt/baremetal/pxe.py: 'dnsmasq', ... +dnsmasq: CommandFilter, /usr/sbin/dnsmasq, root + +# nova/virt/baremetal/pxe.py: 'kill', '-TERM', str(dnsmasq_pid) +kill_dnsmasq: KillFilter, root, /usr/sbin/dnsmasq, -15, -TERM + diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 22771b589..f1ee0154e 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -331,7 +331,7 @@ class Requestify(wsgi.Middleware): for non_arg in non_args: # Remove, but raise KeyError if omitted args.pop(non_arg) - except KeyError, e: + except KeyError: raise webob.exc.HTTPBadRequest() except exception.InvalidRequest as err: raise webob.exc.HTTPBadRequest(explanation=unicode(err)) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 156042833..d40f25c4d 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -219,6 +219,17 @@ class CloudController(object): def __str__(self): return 'CloudController' + def _enforce_valid_instance_ids(self, context, instance_ids): + # NOTE(mikal): Amazon's implementation of the EC2 API requires that + # _all_ instance ids passed in be valid. + instances = {} + if instance_ids: + for ec2_id in instance_ids: + instance_uuid = ec2utils.ec2_inst_id_to_uuid(context, ec2_id) + instance = self.compute_api.get(context, instance_uuid) + instances[ec2_id] = instance + return instances + def _get_image_state(self, image): # NOTE(vish): fallback status if image_state isn't set state = image.get('status') @@ -977,14 +988,19 @@ class CloudController(object): def describe_instances(self, context, **kwargs): # Optional DescribeInstances argument instance_id = kwargs.get('instance_id', None) + instances = self._enforce_valid_instance_ids(context, instance_id) return self._format_describe_instances(context, - instance_id=instance_id) + instance_id=instance_id, + instance_cache=instances) def describe_instances_v6(self, context, **kwargs): # Optional DescribeInstancesV6 argument instance_id = kwargs.get('instance_id', None) + instances = self._enforce_valid_instance_ids(context, instance_id) return self._format_describe_instances(context, - instance_id=instance_id, use_v6=True) + instance_id=instance_id, + instance_cache=instances, + use_v6=True) def _format_describe_instances(self, context, **kwargs): return {'reservationSet': self._format_instances(context, **kwargs)} @@ -1066,23 +1082,30 @@ class CloudController(object): security_group_names, 'groupId') def _format_instances(self, context, instance_id=None, use_v6=False, - **search_opts): + instances_cache=None, **search_opts): # TODO(termie): this method is poorly named as its name does not imply # that it will be making a variety of database calls # rather than simply formatting a bunch of instances that # were handed to it reservations = {} + + if not instances_cache: + instances_cache = {} + # NOTE(vish): instance_id is an optional list of ids to filter by if instance_id: instances = [] for ec2_id in instance_id: - try: - instance_uuid = ec2utils.ec2_inst_id_to_uuid(context, - ec2_id) - instance = self.compute_api.get(context, instance_uuid) - except exception.NotFound: - continue - instances.append(instance) + if ec2_id in instances_cache: + instances.append(instances_cache[ec2_id]) + else: + try: + instance_uuid = ec2utils.ec2_inst_id_to_uuid(context, + ec2_id) + instance = self.compute_api.get(context, instance_uuid) + except exception.NotFound: + continue + instances.append(instance) else: try: # always filter out deleted instances @@ -1092,6 +1115,7 @@ class CloudController(object): sort_dir='asc') except exception.NotFound: instances = [] + for instance in instances: if not context.is_admin: if instance['image_ref'] == str(CONF.vpn_image_id): diff --git a/nova/api/metadata/base.py b/nova/api/metadata/base.py index 2377da7b7..34d412268 100644 --- a/nova/api/metadata/base.py +++ b/nova/api/metadata/base.py @@ -136,12 +136,7 @@ class InstanceMetadata(): for item in instance.get('metadata', []): self.launch_metadata[item['key']] = item['value'] - self.password = '' - # get password if set - for item in instance.get('system_metadata', []): - if item['key'] == 'password': - self.password = item['value'] or '' - break + self.password = password.extract_password(instance) self.uuid = instance.get('uuid') diff --git a/nova/api/metadata/password.py b/nova/api/metadata/password.py index 3cda67eee..b2bb83b15 100644 --- a/nova/api/metadata/password.py +++ b/nova/api/metadata/password.py @@ -19,7 +19,34 @@ from nova import context from nova import db -MAX_SIZE = 256 +CHUNKS = 4 +CHUNK_LENGTH = 255 +MAX_SIZE = CHUNKS * CHUNK_LENGTH + + +def extract_password(instance): + result = '' + for datum in sorted(instance.get('system_metadata', []), + key=lambda x: x['key']): + if datum['key'].startswith('password_'): + result += datum['value'] + return result or None + + +def set_password(context, instance_uuid, password): + """Stores password as system_metadata items. + + Password is stored with the keys 'password_0' -> 'password_3'. + """ + password = password or '' + meta = {} + for i in xrange(CHUNKS): + meta['password_%d' % i] = password[:CHUNK_LENGTH] + password = password[CHUNK_LENGTH:] + db.instance_system_metadata_update(context, + instance_uuid, + meta, + False) def handle_password(req, meta_data): @@ -36,9 +63,6 @@ def handle_password(req, meta_data): if (req.content_length > MAX_SIZE or len(req.body) > MAX_SIZE): msg = _("Request is too large.") raise exc.HTTPBadRequest(explanation=msg) - db.instance_system_metadata_update(ctxt, - meta_data.uuid, - {'password': req.body}, - False) + set_password(ctxt, meta_data.uuid, req.body) else: raise exc.HTTPBadRequest() diff --git a/nova/api/openstack/compute/__init__.py b/nova/api/openstack/compute/__init__.py index f88671733..92c84c13f 100644 --- a/nova/api/openstack/compute/__init__.py +++ b/nova/api/openstack/compute/__init__.py @@ -57,7 +57,8 @@ class APIRouter(nova.api.openstack.APIRouter): self.resources['versions'] = versions.create_resource() mapper.connect("versions", "/", controller=self.resources['versions'], - action='show') + action='show', + conditions={"method": ['GET']}) mapper.redirect("", "/") diff --git a/nova/api/openstack/compute/contrib/coverage_ext.py b/nova/api/openstack/compute/contrib/coverage_ext.py index e97185f82..7ad549d4c 100644 --- a/nova/api/openstack/compute/contrib/coverage_ext.py +++ b/nova/api/openstack/compute/contrib/coverage_ext.py @@ -27,10 +27,15 @@ from coverage import coverage from webob import exc from nova.api.openstack import extensions +from nova.cert import rpcapi as cert_api from nova.compute import api as compute_api +from nova.conductor import api as conductor_api +from nova.console import api as console_api +from nova.consoleauth import rpcapi as consoleauth_api from nova import db from nova.network import api as network_api from nova.openstack.common import log as logging +from nova.scheduler import rpcapi as scheduler_api LOG = logging.getLogger(__name__) @@ -45,6 +50,11 @@ class CoverageController(object): self.coverInst = coverage(data_file=data_out) self.compute_api = compute_api.API() self.network_api = network_api.API() + self.conductor_api = conductor_api.API() + self.consoleauth_api = consoleauth_api.ConsoleAuthAPI() + self.console_api = console_api.API() + self.scheduler_api = scheduler_api.SchedulerAPI() + self.cert_api = cert_api.CertAPI() self.services = [] self.combine = False super(CoverageController, self).__init__() @@ -65,9 +75,13 @@ class CoverageController(object): apicommands = { "compute": self.compute_api.get_backdoor_port, "network": self.network_api.get_backdoor_port, + "conductor": self.conductor_api.get_backdoor_port, + "consoleauth": self.consoleauth_api.get_backdoor_port, + "console": self.console_api.get_backdoor_port, + "scheduler": self.scheduler_api.get_backdoor_port, + "cert": self.cert_api.get_backdoor_port, } ports = [] - temp = {} #TODO(mtreinish): Figure out how to bind the backdoor socket to 0.0.0.0 # Currently this will only work if the host is resolved as loopback on # the same host as api-server @@ -106,7 +120,7 @@ class CoverageController(object): def _start_coverage(self, req, body): '''Begin recording coverage information.''' - LOG.debug("Coverage begin") + LOG.debug(_("Coverage begin")) body = body['start'] self.combine = False if 'combine' in body.keys(): @@ -140,8 +154,9 @@ class CoverageController(object): for service in self.services: self._stop_coverage_telnet(service['telnet']) if self._check_coverage(): - msg = ("Coverage not running") + msg = _("Coverage not running") raise exc.HTTPNotFound(explanation=msg) + return {'path': self.data_path} def _report_coverage_telnet(self, tn, path, xml=False): if xml: @@ -161,26 +176,34 @@ class CoverageController(object): def _report_coverage(self, req, body): self._stop_coverage(req) xml = False + html = False path = None body = body['report'] if 'file' in body.keys(): path = body['file'] if path != os.path.basename(path): - msg = ("Invalid path") + msg = _("Invalid path") raise exc.HTTPBadRequest(explanation=msg) path = os.path.join(self.data_path, path) else: - msg = ("No path given for report file") + msg = _("No path given for report file") raise exc.HTTPBadRequest(explanation=msg) if 'xml' in body.keys(): xml = body['xml'] + elif 'html' in body.keys(): + if not self.combine: + msg = _("You can't use html reports without combining") + raise exc.HTTPBadRequest(explanation=msg) + html = body['html'] if self.combine: self.coverInst.combine() if xml: self.coverInst.xml_report(outfile=path) + elif html: + self.coverInst.html_report(directory=path) else: output = open(path, 'w') self.coverInst.report(file=output) diff --git a/nova/api/openstack/compute/contrib/volumes.py b/nova/api/openstack/compute/contrib/volumes.py index abdef3a7d..9564921f4 100644 --- a/nova/api/openstack/compute/contrib/volumes.py +++ b/nova/api/openstack/compute/contrib/volumes.py @@ -26,6 +26,7 @@ from nova.api.openstack import xmlutil from nova import compute from nova import exception from nova.openstack.common import log as logging +from nova.openstack.common import uuidutils from nova import utils from nova import volume @@ -365,6 +366,12 @@ class VolumeAttachmentController(wsgi.Controller): instance['uuid'], assigned_mountpoint)} + def _validate_volume_id(self, volume_id): + if not uuidutils.is_uuid_like(volume_id): + msg = _("Bad volumeId format: volumeId is " + "not in proper format (%s)") % volume_id + raise exc.HTTPBadRequest(explanation=msg) + @wsgi.serializers(xml=VolumeAttachmentTemplate) def create(self, req, server_id, body): """Attach a volume to an instance.""" @@ -377,6 +384,8 @@ class VolumeAttachmentController(wsgi.Controller): volume_id = body['volumeAttachment']['volumeId'] device = body['volumeAttachment'].get('device') + self._validate_volume_id(volume_id) + msg = _("Attach volume %(volume_id)s to instance %(server_id)s" " at %(device)s") % locals() LOG.audit(msg, context=context) diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index a62740681..7a8d7d5a8 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -452,9 +452,6 @@ class Controller(wsgi.Controller): servers = self._get_servers(req, is_detail=False) except exception.Invalid as err: raise exc.HTTPBadRequest(explanation=str(err)) - except exception.NotFound: - msg = _("Instance could not be found") - raise exc.HTTPNotFound(explanation=msg) return servers @wsgi.serializers(xml=ServersTemplate) @@ -464,9 +461,6 @@ class Controller(wsgi.Controller): servers = self._get_servers(req, is_detail=True) except exception.Invalid as err: raise exc.HTTPBadRequest(explanation=str(err)) - except exception.NotFound as err: - msg = _("Instance could not be found") - raise exc.HTTPNotFound(explanation=msg) return servers def _add_instance_faults(self, ctxt, instances): @@ -981,6 +975,10 @@ class Controller(wsgi.Controller): msg = _("HostId cannot be updated.") raise exc.HTTPBadRequest(explanation=msg) + if 'personality' in body['server']: + msg = _("Personality cannot be updated.") + raise exc.HTTPBadRequest(explanation=msg) + try: instance = self.compute_api.get(ctxt, id) req.cache_db_instance(instance) diff --git a/nova/api/openstack/compute/versions.py b/nova/api/openstack/compute/versions.py index 76e37cf41..5c416908e 100644 --- a/nova/api/openstack/compute/versions.py +++ b/nova/api/openstack/compute/versions.py @@ -26,9 +26,9 @@ from nova.openstack.common import timeutils LINKS = { 'v2.0': { 'pdf': 'http://docs.openstack.org/' - 'api/openstack-compute/1.1/os-compute-devguide-1.1.pdf', + 'api/openstack-compute/2/os-compute-devguide-2.pdf', 'wadl': 'http://docs.openstack.org/' - 'api/openstack-compute/1.1/wadl/os-compute-1.1.wadl', + 'api/openstack-compute/2/wadl/os-compute-2.wadl' }, } diff --git a/nova/cells/__init__.py b/nova/cells/__init__.py new file mode 100644 index 000000000..47d21a14b --- /dev/null +++ b/nova/cells/__init__.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 Rackspace Hosting +# +# 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. + +""" +Cells +""" diff --git a/nova/cells/driver.py b/nova/cells/driver.py new file mode 100644 index 000000000..04e29dddf --- /dev/null +++ b/nova/cells/driver.py @@ -0,0 +1,41 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. + +""" +Base Cells Communication Driver +""" + + +class BaseCellsDriver(object): + """The base class for cells communication. + + One instance of this class will be created for every neighbor cell + that we find in the DB and it will be associated with the cell in + its CellState. + + One instance is also created by the cells manager for setting up + the consumers. + """ + def start_consumers(self, msg_runner): + """Start any consumers the driver may need.""" + raise NotImplementedError() + + def stop_consumers(self): + """Stop consuming messages.""" + raise NotImplementedError() + + def send_message_to_cell(self, cell_state, message): + """Send a message to a cell.""" + raise NotImplementedError() diff --git a/nova/cells/manager.py b/nova/cells/manager.py new file mode 100644 index 000000000..0942bae28 --- /dev/null +++ b/nova/cells/manager.py @@ -0,0 +1,220 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. + +""" +Cells Service Manager +""" +import datetime +import time + +from nova.cells import messaging +from nova.cells import state as cells_state +from nova.cells import utils as cells_utils +from nova import context +from nova import exception +from nova import manager +from nova.openstack.common import cfg +from nova.openstack.common import importutils +from nova.openstack.common import log as logging +from nova.openstack.common import timeutils + +cell_manager_opts = [ + cfg.StrOpt('driver', + default='nova.cells.rpc_driver.CellsRPCDriver', + help='Cells communication driver to use'), + cfg.IntOpt("instance_updated_at_threshold", + default=3600, + help="Number of seconds after an instance was updated " + "or deleted to continue to update cells"), + cfg.IntOpt("instance_update_num_instances", + default=1, + help="Number of instances to update per periodic task run") +] + + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF +CONF.register_opts(cell_manager_opts, group='cells') + + +class CellsManager(manager.Manager): + """The nova-cells manager class. This class defines RPC + methods that the local cell may call. This class is NOT used for + messages coming from other cells. That communication is + driver-specific. + + Communication to other cells happens via the messaging module. The + MessageRunner from that module will handle routing the message to + the correct cell via the communications driver. Most methods below + create 'targeted' (where we want to route a message to a specific cell) + or 'broadcast' (where we want a message to go to multiple cells) + messages. + + Scheduling requests get passed to the scheduler class. + """ + RPC_API_VERSION = '1.0' + + def __init__(self, *args, **kwargs): + # Mostly for tests. + cell_state_manager = kwargs.pop('cell_state_manager', None) + super(CellsManager, self).__init__(*args, **kwargs) + if cell_state_manager is None: + cell_state_manager = cells_state.CellStateManager + self.state_manager = cell_state_manager() + self.msg_runner = messaging.MessageRunner(self.state_manager) + cells_driver_cls = importutils.import_class( + CONF.cells.driver) + self.driver = cells_driver_cls() + self.instances_to_heal = iter([]) + + def post_start_hook(self): + """Have the driver start its consumers for inter-cell communication. + Also ask our child cells for their capacities and capabilities so + we get them more quickly than just waiting for the next periodic + update. Receiving the updates from the children will cause us to + update our parents. If we don't have any children, just update + our parents immediately. + """ + # FIXME(comstud): There's currently no hooks when services are + # stopping, so we have no way to stop consumers cleanly. + self.driver.start_consumers(self.msg_runner) + ctxt = context.get_admin_context() + if self.state_manager.get_child_cells(): + self.msg_runner.ask_children_for_capabilities(ctxt) + self.msg_runner.ask_children_for_capacities(ctxt) + else: + self._update_our_parents(ctxt) + + @manager.periodic_task + def _update_our_parents(self, ctxt): + """Update our parent cells with our capabilities and capacity + if we're at the bottom of the tree. + """ + self.msg_runner.tell_parents_our_capabilities(ctxt) + self.msg_runner.tell_parents_our_capacities(ctxt) + + @manager.periodic_task + def _heal_instances(self, ctxt): + """Periodic task to send updates for a number of instances to + parent cells. + + On every run of the periodic task, we will attempt to sync + 'CONF.cells.instance_update_num_instances' number of instances. + When we get the list of instances, we shuffle them so that multiple + nova-cells services aren't attempting to sync the same instances + in lockstep. + + If CONF.cells.instance_update_at_threshold is set, only attempt + to sync instances that have been updated recently. The CONF + setting defines the maximum number of seconds old the updated_at + can be. Ie, a threshold of 3600 means to only update instances + that have modified in the last hour. + """ + + if not self.state_manager.get_parent_cells(): + # No need to sync up if we have no parents. + return + + info = {'updated_list': False} + + def _next_instance(): + try: + instance = self.instances_to_heal.next() + except StopIteration: + if info['updated_list']: + return + threshold = CONF.cells.instance_updated_at_threshold + updated_since = None + if threshold > 0: + updated_since = timeutils.utcnow() - datetime.timedelta( + seconds=threshold) + self.instances_to_heal = cells_utils.get_instances_to_sync( + ctxt, updated_since=updated_since, shuffle=True, + uuids_only=True) + info['updated_list'] = True + try: + instance = self.instances_to_heal.next() + except StopIteration: + return + return instance + + rd_context = ctxt.elevated(read_deleted='yes') + + for i in xrange(CONF.cells.instance_update_num_instances): + while True: + # Yield to other greenthreads + time.sleep(0) + instance_uuid = _next_instance() + if not instance_uuid: + return + try: + instance = self.db.instance_get_by_uuid(rd_context, + instance_uuid) + except exception.InstanceNotFound: + continue + self._sync_instance(ctxt, instance) + break + + def _sync_instance(self, ctxt, instance): + """Broadcast an instance_update or instance_destroy message up to + parent cells. + """ + if instance['deleted']: + self.instance_destroy_at_top(ctxt, instance) + else: + self.instance_update_at_top(ctxt, instance) + + def schedule_run_instance(self, ctxt, host_sched_kwargs): + """Pick a cell (possibly ourselves) to build new instance(s) + and forward the request accordingly. + """ + # Target is ourselves first. + our_cell = self.state_manager.get_my_state() + self.msg_runner.schedule_run_instance(ctxt, our_cell, + host_sched_kwargs) + + def run_compute_api_method(self, ctxt, cell_name, method_info, call): + """Call a compute API method in a specific cell.""" + response = self.msg_runner.run_compute_api_method(ctxt, + cell_name, + method_info, + call) + if call: + return response.value_or_raise() + + def instance_update_at_top(self, ctxt, instance): + """Update an instance at the top level cell.""" + self.msg_runner.instance_update_at_top(ctxt, instance) + + def instance_destroy_at_top(self, ctxt, instance): + """Destroy an instance at the top level cell.""" + self.msg_runner.instance_destroy_at_top(ctxt, instance) + + def instance_delete_everywhere(self, ctxt, instance, delete_type): + """This is used by API cell when it didn't know what cell + an instance was in, but the instance was requested to be + deleted or soft_deleted. So, we'll broadcast this everywhere. + """ + self.msg_runner.instance_delete_everywhere(ctxt, instance, + delete_type) + + def instance_fault_create_at_top(self, ctxt, instance_fault): + """Create an instance fault at the top level cell.""" + self.msg_runner.instance_fault_create_at_top(ctxt, instance_fault) + + def bw_usage_update_at_top(self, ctxt, bw_update_info): + """Update bandwidth usage at top level cell.""" + self.msg_runner.bw_usage_update_at_top(ctxt, bw_update_info) diff --git a/nova/cells/messaging.py b/nova/cells/messaging.py new file mode 100644 index 000000000..e5617e742 --- /dev/null +++ b/nova/cells/messaging.py @@ -0,0 +1,1047 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. + +""" +Cell messaging module. + +This module defines the different message types that are passed between +cells and the methods that they can call when the target cell has been +reached. + +The interface into this module is the MessageRunner class. +""" +import sys + +from eventlet import queue + +from nova.cells import state as cells_state +from nova import compute +from nova import context +from nova.db import base +from nova import exception +from nova.openstack.common import cfg +from nova.openstack.common import excutils +from nova.openstack.common import importutils +from nova.openstack.common import jsonutils +from nova.openstack.common import log as logging +from nova.openstack.common.rpc import common as rpc_common +from nova.openstack.common import uuidutils +from nova import utils + + +cell_messaging_opts = [ + cfg.IntOpt('max_hop_count', + default=10, + help='Maximum number of hops for cells routing.'), + cfg.StrOpt('scheduler', + default='nova.cells.scheduler.CellsScheduler', + help='Cells scheduler to use')] + +CONF = cfg.CONF +CONF.import_opt('name', 'nova.cells.opts', group='cells') +CONF.import_opt('call_timeout', 'nova.cells.opts', group='cells') +CONF.register_opts(cell_messaging_opts, group='cells') + +LOG = logging.getLogger(__name__) + +# Separator used between cell names for the 'full cell name' and routing +# path. +_PATH_CELL_SEP = '!' + + +def _reverse_path(path): + """Reverse a path. Used for sending responses upstream.""" + path_parts = path.split(_PATH_CELL_SEP) + path_parts.reverse() + return _PATH_CELL_SEP.join(path_parts) + + +def _response_cell_name_from_path(routing_path, neighbor_only=False): + """Reverse the routing_path. If we only want to send to our parent, + set neighbor_only to True. + """ + path = _reverse_path(routing_path) + if not neighbor_only or len(path) == 1: + return path + return _PATH_CELL_SEP.join(path.split(_PATH_CELL_SEP)[:2]) + + +# +# Message classes. +# + + +class _BaseMessage(object): + """Base message class. It defines data that is passed with every + single message through every cell. + + Messages are JSON-ified before sending and turned back into a + class instance when being received. + + Every message has a unique ID. This is used to route responses + back to callers. In the future, this might be used to detect + receiving the same message more than once. + + routing_path is updated on every hop through a cell. The current + cell name is appended to it (cells are separated by + _PATH_CELL_SEP ('!')). This is used to tell if we've reached the + target cell and also to determine the source of a message for + responses by reversing it. + + hop_count is incremented and compared against max_hop_count. The + only current usefulness of this is to break out of a routing loop + if someone has a broken config. + + fanout means to send to all nova-cells services running in a cell. + This is useful for capacity and capability broadcasting as well + as making sure responses get back to the nova-cells service that + is waiting. + """ + + # Override message_type in a subclass + message_type = None + + base_attrs_to_json = ['message_type', + 'ctxt', + 'method_name', + 'method_kwargs', + 'direction', + 'need_response', + 'fanout', + 'uuid', + 'routing_path', + 'hop_count', + 'max_hop_count'] + + def __init__(self, msg_runner, ctxt, method_name, method_kwargs, + direction, need_response=False, fanout=False, uuid=None, + routing_path=None, hop_count=0, max_hop_count=None, + **kwargs): + self.ctxt = ctxt + self.resp_queue = None + self.msg_runner = msg_runner + self.state_manager = msg_runner.state_manager + # Copy these. + self.base_attrs_to_json = self.base_attrs_to_json[:] + # Normally this would just be CONF.cells.name, but going through + # the msg_runner allows us to stub it more easily. + self.our_path_part = self.msg_runner.our_name + self.uuid = uuid + if self.uuid is None: + self.uuid = uuidutils.generate_uuid() + self.method_name = method_name + self.method_kwargs = method_kwargs + self.direction = direction + self.need_response = need_response + self.fanout = fanout + self.routing_path = routing_path + self.hop_count = hop_count + if max_hop_count is None: + max_hop_count = CONF.cells.max_hop_count + self.max_hop_count = max_hop_count + self.is_broadcast = False + self._append_hop() + # Each sub-class should set this when the message is inited + self.next_hops = [] + self.resp_queue = None + + def __repr__(self): + _dict = self._to_dict() + _dict.pop('method_kwargs') + return "<%s: %s>" % (self.__class__.__name__, _dict) + + def _append_hop(self): + """Add our hop to the routing_path.""" + routing_path = (self.routing_path and + self.routing_path + _PATH_CELL_SEP or '') + self.routing_path = routing_path + self.our_path_part + self.hop_count += 1 + + def _at_max_hop_count(self, do_raise=True): + """Check if we're at the max hop count. If we are and do_raise is + True, raise CellMaxHopCountReached. If we are at the max and + do_raise is False... return True, else False. + """ + if self.hop_count >= self.max_hop_count: + if do_raise: + raise exception.CellMaxHopCountReached( + hop_count=self.hop_count) + return True + return False + + def _process_locally(self): + """Its been determined that we should process this message in this + cell. Go through the MessageRunner to call the appropriate + method for this message. Catch the response and/or exception and + encode it within a Response instance. Return it so the caller + can potentially return it to another cell... or return it to + a caller waiting in this cell. + """ + try: + resp_value = self.msg_runner._process_message_locally(self) + failure = False + except Exception as exc: + resp_value = sys.exc_info() + failure = True + LOG.exception(_("Error processing message locally: %(exc)s"), + locals()) + return Response(self.routing_path, resp_value, failure) + + def _setup_response_queue(self): + """Shortcut to creating a response queue in the MessageRunner.""" + self.resp_queue = self.msg_runner._setup_response_queue(self) + + def _cleanup_response_queue(self): + """Shortcut to deleting a response queue in the MessageRunner.""" + if self.resp_queue: + self.msg_runner._cleanup_response_queue(self) + self.resp_queue = None + + def _wait_for_json_responses(self, num_responses=1): + """Wait for response(s) to be put into the eventlet queue. Since + each queue entry actually contains a list of JSON-ified responses, + combine them all into a single list to return. + + Destroy the eventlet queue when done. + """ + if not self.resp_queue: + # Source is not actually expecting a response + return + responses = [] + wait_time = CONF.cells.call_timeout + try: + for x in xrange(num_responses): + json_responses = self.resp_queue.get(timeout=wait_time) + responses.extend(json_responses) + except queue.Empty: + raise exception.CellTimeout() + finally: + self._cleanup_response_queue() + return responses + + def _send_json_responses(self, json_responses, neighbor_only=False, + fanout=False): + """Send list of responses to this message. Responses passed here + are JSON-ified. Targeted messages have a single response while + Broadcast messages may have multiple responses. + + If this cell was the source of the message, these responses will + be returned from self.process(). + + Otherwise, we will route the response to the source of the + request. If 'neighbor_only' is True, the response will be sent + to the neighbor cell, not the original requester. Broadcast + messages get aggregated at each hop, so neighbor_only will be + True for those messages. + """ + if not self.need_response: + return + if self.source_is_us(): + responses = [] + for json_response in json_responses: + responses.append(Response.from_json(json_response)) + return responses + direction = self.direction == 'up' and 'down' or 'up' + response_kwargs = {'orig_message': self.to_json(), + 'responses': json_responses} + target_cell = _response_cell_name_from_path(self.routing_path, + neighbor_only=neighbor_only) + response = self.msg_runner._create_response_message(self.ctxt, + direction, target_cell, self.uuid, response_kwargs, + fanout=fanout) + response.process() + + def _send_response(self, response, neighbor_only=False): + """Send a response to this message. If the source of the + request was ourselves, just return the response. It'll be + passed back to the caller of self.process(). See DocString for + _send_json_responses() as it handles most of the real work for + this method. + + 'response' is an instance of Response class. + """ + if not self.need_response: + return + if self.source_is_us(): + return response + self._send_json_responses([response.to_json()], + neighbor_only=neighbor_only) + + def _send_response_from_exception(self, exc_info): + """Take an exception as returned from sys.exc_info(), encode + it in a Response, and send it. + """ + response = Response(self.routing_path, exc_info, True) + return self._send_response(response) + + def _to_dict(self): + """Convert a message to a dictionary. Only used internally.""" + _dict = {} + for key in self.base_attrs_to_json: + _dict[key] = getattr(self, key) + return _dict + + def to_json(self): + """Convert a message into JSON for sending to a sibling cell.""" + _dict = self._to_dict() + # Convert context to dict. + _dict['ctxt'] = _dict['ctxt'].to_dict() + return jsonutils.dumps(_dict) + + def source_is_us(self): + """Did this cell create this message?""" + return self.routing_path == self.our_path_part + + def process(self): + """Process a message. Deal with it locally and/or forward it to a + sibling cell. + + Override in a subclass. + """ + raise NotImplementedError() + + +class _TargetedMessage(_BaseMessage): + """A targeted message is a message that is destined for a specific + single cell. + + 'target_cell' can be a full cell name like 'api!child-cell' or it can + be an instance of the CellState class if the target is a neighbor cell. + """ + message_type = 'targeted' + + def __init__(self, msg_runner, ctxt, method_name, method_kwargs, + direction, target_cell, **kwargs): + super(_TargetedMessage, self).__init__(msg_runner, ctxt, + method_name, method_kwargs, direction, **kwargs) + if isinstance(target_cell, cells_state.CellState): + # Neighbor cell or ourselves. Convert it to a 'full path'. + if target_cell.is_me: + target_cell = self.our_path_part + else: + target_cell = '%s%s%s' % (self.our_path_part, + _PATH_CELL_SEP, + target_cell.name) + self.target_cell = target_cell + self.base_attrs_to_json.append('target_cell') + + def _get_next_hop(self): + """Return the cell name for the next hop. If the next hop is + the current cell, return None. + """ + if self.target_cell == self.routing_path: + return self.state_manager.my_cell_state + target_cell = self.target_cell + routing_path = self.routing_path + current_hops = routing_path.count(_PATH_CELL_SEP) + next_hop_num = current_hops + 1 + dest_hops = target_cell.count(_PATH_CELL_SEP) + if dest_hops < current_hops: + reason = _("destination is %(target_cell)s but routing_path " + "is %(routing_path)s") % locals() + raise exception.CellRoutingInconsistency(reason=reason) + dest_name_parts = target_cell.split(_PATH_CELL_SEP) + if (_PATH_CELL_SEP.join(dest_name_parts[:next_hop_num]) != + routing_path): + reason = _("destination is %(target_cell)s but routing_path " + "is %(routing_path)s") % locals() + raise exception.CellRoutingInconsistency(reason=reason) + next_hop_name = dest_name_parts[next_hop_num] + if self.direction == 'up': + next_hop = self.state_manager.get_parent_cell(next_hop_name) + else: + next_hop = self.state_manager.get_child_cell(next_hop_name) + if not next_hop: + cell_type = 'parent' if self.direction == 'up' else 'child' + reason = _("Unknown %(cell_type)s when routing to " + "%(target_cell)s") % locals() + raise exception.CellRoutingInconsistency(reason=reason) + return next_hop + + def process(self): + """Process a targeted message. This is called for all cells + that touch this message. If the local cell is the one that + created this message, we reply directly with a Response instance. + If the local cell is not the target, an eventlet queue is created + and we wait for the response to show up via another thread + receiving the Response back. + + Responses to targeted messages are routed directly back to the + source. No eventlet queues are created in intermediate hops. + + All exceptions for processing the message across the whole + routing path are caught and encoded within the Response and + returned to the caller. + """ + try: + next_hop = self._get_next_hop() + except Exception as exc: + exc_info = sys.exc_info() + LOG.exception(_("Error locating next hop for message: %(exc)s"), + locals()) + return self._send_response_from_exception(exc_info) + + if next_hop.is_me: + # Final destination. + response = self._process_locally() + return self._send_response(response) + + # Need to forward via neighbor cell. + if self.need_response and self.source_is_us(): + # A response is needed and the source of the message is + # this cell. Create the eventlet queue. + self._setup_response_queue() + wait_for_response = True + else: + wait_for_response = False + + try: + # This is inside the try block, so we can encode the + # exception and return it to the caller. + if self.hop_count >= self.max_hop_count: + raise exception.CellMaxHopCountReached( + hop_count=self.hop_count) + next_hop.send_message(self) + except Exception as exc: + exc_info = sys.exc_info() + err_str = _("Failed to send message to cell: %(next_hop)s: " + "%(exc)s") + LOG.exception(err_str, locals()) + self._cleanup_response_queue() + return self._send_response_from_exception(exc_info) + + if wait_for_response: + # Targeted messages only have 1 response. + remote_response = self._wait_for_json_responses()[0] + return Response.from_json(remote_response) + + +class _BroadcastMessage(_BaseMessage): + """A broadcast message. This means to call a method in every single + cell going in a certain direction. + """ + message_type = 'broadcast' + + def __init__(self, msg_runner, ctxt, method_name, method_kwargs, + direction, run_locally=True, **kwargs): + super(_BroadcastMessage, self).__init__(msg_runner, ctxt, + method_name, method_kwargs, direction, **kwargs) + # The local cell creating this message has the option + # to be able to process the message locally or not. + self.run_locally = run_locally + self.is_broadcast = True + + def _get_next_hops(self): + """Set the next hops and return the number of hops. The next + hops may include ourself. + """ + if self.hop_count >= self.max_hop_count: + return [] + if self.direction == 'down': + return self.state_manager.get_child_cells() + else: + return self.state_manager.get_parent_cells() + + def _send_to_cells(self, target_cells): + """Send a message to multiple cells.""" + for cell in target_cells: + cell.send_message(self) + + def _send_json_responses(self, json_responses): + """Responses to broadcast messages always need to go to the + neighbor cell from which we received this message. That + cell aggregates the responses and makes sure to forward them + to the correct source. + """ + return super(_BroadcastMessage, self)._send_json_responses( + json_responses, neighbor_only=True, fanout=True) + + def process(self): + """Process a broadcast message. This is called for all cells + that touch this message. + + The message is sent to all cells in the certain direction and + the creator of this message has the option of whether or not + to process it locally as well. + + If responses from all cells are required, each hop creates an + eventlet queue and waits for responses from its immediate + neighbor cells. All responses are then aggregated into a + single list and are returned to the neighbor cell until the + source is reached. + + When the source is reached, a list of Response instances are + returned to the caller. + + All exceptions for processing the message across the whole + routing path are caught and encoded within the Response and + returned to the caller. It is possible to get a mix of + successful responses and failure responses. The caller is + responsible for dealing with this. + """ + try: + next_hops = self._get_next_hops() + except Exception as exc: + exc_info = sys.exc_info() + LOG.exception(_("Error locating next hops for message: %(exc)s"), + locals()) + return self._send_response_from_exception(exc_info) + + # Short circuit if we don't need to respond + if not self.need_response: + if self.run_locally: + self._process_locally() + self._send_to_cells(next_hops) + return + + # We'll need to aggregate all of the responses (from ourself + # and our sibling cells) into 1 response + try: + self._setup_response_queue() + self._send_to_cells(next_hops) + except Exception as exc: + # Error just trying to send to cells. Send a single response + # with the failure. + exc_info = sys.exc_info() + LOG.exception(_("Error sending message to next hops: %(exc)s"), + locals()) + self._cleanup_response_queue() + return self._send_response_from_exception(exc_info) + + if self.run_locally: + # Run locally and store the Response. + local_response = self._process_locally() + else: + local_response = None + + try: + remote_responses = self._wait_for_json_responses( + num_responses=len(next_hops)) + except Exception as exc: + # Error waiting for responses, most likely a timeout. + # Send a single response back with the failure. + exc_info = sys.exc_info() + err_str = _("Error waiting for responses from neighbor cells: " + "%(exc)s") + LOG.exception(err_str, locals()) + return self._send_response_from_exception(exc_info) + + if local_response: + remote_responses.append(local_response.to_json()) + return self._send_json_responses(remote_responses) + + +class _ResponseMessage(_TargetedMessage): + """A response message is really just a special targeted message, + saying to call 'parse_responses' when we reach the source of a 'call'. + + The 'fanout' attribute on this message may be true if we're responding + to a broadcast or if we're about to respond to the source of an + original target message. Because multiple nova-cells services may + be running within a cell, we need to make sure the response gets + back to the correct one, so we have to fanout. + """ + message_type = 'response' + + def __init__(self, msg_runner, ctxt, method_name, method_kwargs, + direction, target_cell, response_uuid, **kwargs): + super(_ResponseMessage, self).__init__(msg_runner, ctxt, + method_name, method_kwargs, direction, target_cell, **kwargs) + self.response_uuid = response_uuid + self.base_attrs_to_json.append('response_uuid') + + def process(self): + """Process a response. If the target is the local cell, process + the response here. Otherwise, forward it to where it needs to + go. + """ + next_hop = self._get_next_hop() + if next_hop.is_me: + self._process_locally() + return + if self.fanout is False: + # Really there's 1 more hop on each of these below, but + # it doesn't matter for this logic. + target_hops = self.target_cell.count(_PATH_CELL_SEP) + current_hops = self.routing_path.count(_PATH_CELL_SEP) + if current_hops + 1 == target_hops: + # Next hop is the target.. so we must fanout. See + # DocString above. + self.fanout = True + next_hop.send_message(self) + + +# +# Methods that may be called when processing messages after reaching +# a target cell. +# + + +class _BaseMessageMethods(base.Base): + """Base class for defining methods by message types.""" + def __init__(self, msg_runner): + super(_BaseMessageMethods, self).__init__() + self.msg_runner = msg_runner + self.state_manager = msg_runner.state_manager + self.compute_api = compute.API() + + +class _ResponseMessageMethods(_BaseMessageMethods): + """Methods that are called from a ResponseMessage. There's only + 1 method (parse_responses) and it is called when the message reaches + the source of a 'call'. All we do is stuff the response into the + eventlet queue to signal the caller that's waiting. + """ + def parse_responses(self, message, orig_message, responses): + self.msg_runner._put_response(message.response_uuid, + responses) + + +class _TargetedMessageMethods(_BaseMessageMethods): + """These are the methods that can be called when routing a message + to a specific cell. + """ + def __init__(self, *args, **kwargs): + super(_TargetedMessageMethods, self).__init__(*args, **kwargs) + + def schedule_run_instance(self, message, host_sched_kwargs): + """Parent cell told us to schedule new instance creation.""" + self.msg_runner.scheduler.run_instance(message, host_sched_kwargs) + + def run_compute_api_method(self, message, method_info): + """Run a method in the compute api class.""" + method = method_info['method'] + fn = getattr(self.compute_api, method, None) + if not fn: + detail = _("Unknown method '%(method)s' in compute API") + raise exception.CellServiceAPIMethodNotFound( + detail=detail % locals()) + args = list(method_info['method_args']) + # 1st arg is instance_uuid that we need to turn into the + # instance object. + instance_uuid = args[0] + try: + instance = self.db.instance_get_by_uuid(message.ctxt, + instance_uuid) + except exception.InstanceNotFound: + with excutils.save_and_reraise_exception(): + # Must be a race condition. Let's try to resolve it by + # telling the top level cells that this instance doesn't + # exist. + instance = {'uuid': instance_uuid} + self.msg_runner.instance_destroy_at_top(message.ctxt, + instance) + args[0] = instance + return fn(message.ctxt, *args, **method_info['method_kwargs']) + + def update_capabilities(self, message, cell_name, capabilities): + """A child cell told us about their capabilities.""" + LOG.debug(_("Received capabilities from child cell " + "%(cell_name)s: %(capabilities)s"), locals()) + self.state_manager.update_cell_capabilities(cell_name, + capabilities) + # Go ahead and update our parents now that a child updated us + self.msg_runner.tell_parents_our_capabilities(message.ctxt) + + def update_capacities(self, message, cell_name, capacities): + """A child cell told us about their capacity.""" + LOG.debug(_("Received capacities from child cell " + "%(cell_name)s: %(capacities)s"), locals()) + self.state_manager.update_cell_capacities(cell_name, + capacities) + # Go ahead and update our parents now that a child updated us + self.msg_runner.tell_parents_our_capacities(message.ctxt) + + def announce_capabilities(self, message): + """A parent cell has told us to send our capabilities, so let's + do so. + """ + self.msg_runner.tell_parents_our_capabilities(message.ctxt) + + def announce_capacities(self, message): + """A parent cell has told us to send our capacity, so let's + do so. + """ + self.msg_runner.tell_parents_our_capacities(message.ctxt) + + +class _BroadcastMessageMethods(_BaseMessageMethods): + """These are the methods that can be called as a part of a broadcast + message. + """ + def _at_the_top(self): + """Are we the API level?""" + return not self.state_manager.get_parent_cells() + + def instance_update_at_top(self, message, instance, **kwargs): + """Update an instance in the DB if we're a top level cell.""" + if not self._at_the_top(): + return + instance_uuid = instance['uuid'] + routing_path = message.routing_path + instance['cell_name'] = _reverse_path(routing_path) + # Remove things that we can't update in the top level cells. + # 'cell_name' is included in this list.. because we'll set it + # ourselves based on the reverse of the routing path. metadata + # is only updated in the API cell, so we don't listen to what + # the child cell tells us. + items_to_remove = ['id', 'security_groups', 'instance_type', + 'volumes', 'cell_name', 'name', 'metadata'] + for key in items_to_remove: + instance.pop(key, None) + + # Fixup info_cache. We'll have to update this separately if + # it exists. + info_cache = instance.pop('info_cache', None) + if info_cache is not None: + info_cache.pop('id', None) + info_cache.pop('instance', None) + + # Fixup system_metadata (should be a dict for update, not a list) + if ('system_metadata' in instance and + isinstance(instance['system_metadata'], list)): + sys_metadata = dict([(md['key'], md['value']) + for md in instance['system_metadata']]) + instance['system_metadata'] = sys_metadata + + LOG.debug(_("Got update for instance %(instance_uuid)s: " + "%(instance)s") % locals()) + + # It's possible due to some weird condition that the instance + # was already set as deleted... so we'll attempt to update + # it with permissions that allows us to read deleted. + with utils.temporary_mutation(message.ctxt, read_deleted="yes"): + try: + self.db.instance_update(message.ctxt, instance_uuid, + instance, update_cells=False) + except exception.NotFound: + # FIXME(comstud): Strange. Need to handle quotas here, + # if we actually want this code to remain.. + self.db.instance_create(message.ctxt, instance) + if info_cache: + self.db.instance_info_cache_update(message.ctxt, instance_uuid, + info_cache, update_cells=False) + + def instance_destroy_at_top(self, message, instance, **kwargs): + """Destroy an instance from the DB if we're a top level cell.""" + if not self._at_the_top(): + return + instance_uuid = instance['uuid'] + LOG.debug(_("Got update to delete instance %(instance_uuid)s") % + locals()) + try: + self.db.instance_destroy(message.ctxt, instance_uuid, + update_cells=False) + except exception.InstanceNotFound: + pass + + def instance_delete_everywhere(self, message, instance, delete_type, + **kwargs): + """Call compute API delete() or soft_delete() in every cell. + This is used when the API cell doesn't know what cell an instance + belongs to but the instance was requested to be deleted or + soft-deleted. So, we'll run it everywhere. + """ + LOG.debug(_("Got broadcast to %(delete_type)s delete instance"), + locals(), instance=instance) + if delete_type == 'soft': + self.compute_api.soft_delete(message.ctxt, instance) + else: + self.compute_api.delete(message.ctxt, instance) + + def instance_fault_create_at_top(self, message, instance_fault, **kwargs): + """Destroy an instance from the DB if we're a top level cell.""" + if not self._at_the_top(): + return + items_to_remove = ['id'] + for key in items_to_remove: + instance_fault.pop(key, None) + log_str = _("Got message to create instance fault: " + "%(instance_fault)s") + LOG.debug(log_str, locals()) + self.db.instance_fault_create(message.ctxt, instance_fault) + + def bw_usage_update_at_top(self, message, bw_update_info, **kwargs): + """Update Bandwidth usage in the DB if we're a top level cell.""" + if not self._at_the_top(): + return + self.db.bw_usage_update(message.ctxt, **bw_update_info) + + +_CELL_MESSAGE_TYPE_TO_MESSAGE_CLS = {'targeted': _TargetedMessage, + 'broadcast': _BroadcastMessage, + 'response': _ResponseMessage} +_CELL_MESSAGE_TYPE_TO_METHODS_CLS = {'targeted': _TargetedMessageMethods, + 'broadcast': _BroadcastMessageMethods, + 'response': _ResponseMessageMethods} + + +# +# Below are the public interfaces into this module. +# + + +class MessageRunner(object): + """This class is the main interface into creating messages and + processing them. + + Public methods in this class are typically called by the CellsManager + to create a new message and process it with the exception of + 'message_from_json' which should be used by CellsDrivers to convert + a JSONified message it has received back into the appropriate Message + class. + + Private methods are used internally when we need to keep some + 'global' state. For instance, eventlet queues used for responses are + held in this class. Also, when a Message is process()ed above and + it's determined we should take action locally, + _process_message_locally() will be called. + + When needing to add a new method to call in a Cell2Cell message, + define the new method below and also add it to the appropriate + MessageMethods class where the real work will be done. + """ + + def __init__(self, state_manager): + self.state_manager = state_manager + cells_scheduler_cls = importutils.import_class( + CONF.cells.scheduler) + self.scheduler = cells_scheduler_cls(self) + self.response_queues = {} + self.methods_by_type = {} + self.our_name = CONF.cells.name + for msg_type, cls in _CELL_MESSAGE_TYPE_TO_METHODS_CLS.iteritems(): + self.methods_by_type[msg_type] = cls(self) + + def _process_message_locally(self, message): + """Message processing will call this when its determined that + the message should be processed within this cell. Find the + method to call based on the message type, and call it. The + caller is responsible for catching exceptions and returning + results to cells, if needed. + """ + methods = self.methods_by_type[message.message_type] + fn = getattr(methods, message.method_name) + return fn(message, **message.method_kwargs) + + def _put_response(self, response_uuid, response): + """Put a response into a response queue. This is called when + a _ResponseMessage is processed in the cell that initiated a + 'call' to another cell. + """ + resp_queue = self.response_queues.get(response_uuid) + if not resp_queue: + # Response queue is gone. We must have restarted or we + # received a response after our timeout period. + return + resp_queue.put(response) + + def _setup_response_queue(self, message): + """Set up an eventlet queue to use to wait for replies. + + Replies come back from the target cell as a _ResponseMessage + being sent back to the source. + """ + resp_queue = queue.Queue() + self.response_queues[message.uuid] = resp_queue + return resp_queue + + def _cleanup_response_queue(self, message): + """Stop tracking the response queue either because we're + done receiving responses, or we've timed out. + """ + try: + del self.response_queues[message.uuid] + except KeyError: + # Ignore if queue is gone already somehow. + pass + + def _create_response_message(self, ctxt, direction, target_cell, + response_uuid, response_kwargs, **kwargs): + """Create a ResponseMessage. This is used internally within + the messaging module. + """ + return _ResponseMessage(self, ctxt, 'parse_responses', + response_kwargs, direction, target_cell, + response_uuid, **kwargs) + + def message_from_json(self, json_message): + """Turns a message in JSON format into an appropriate Message + instance. This is called when cells receive a message from + another cell. + """ + message_dict = jsonutils.loads(json_message) + message_type = message_dict.pop('message_type') + # Need to convert context back. + ctxt = message_dict['ctxt'] + message_dict['ctxt'] = context.RequestContext.from_dict(ctxt) + message_cls = _CELL_MESSAGE_TYPE_TO_MESSAGE_CLS[message_type] + return message_cls(self, **message_dict) + + def ask_children_for_capabilities(self, ctxt): + """Tell child cells to send us capabilities. This is typically + called on startup of the nova-cells service. + """ + child_cells = self.state_manager.get_child_cells() + for child_cell in child_cells: + message = _TargetedMessage(self, ctxt, + 'announce_capabilities', + dict(), 'down', child_cell) + message.process() + + def ask_children_for_capacities(self, ctxt): + """Tell child cells to send us capacities. This is typically + called on startup of the nova-cells service. + """ + child_cells = self.state_manager.get_child_cells() + for child_cell in child_cells: + message = _TargetedMessage(self, ctxt, 'announce_capacities', + dict(), 'down', child_cell) + message.process() + + def tell_parents_our_capabilities(self, ctxt): + """Send our capabilities to parent cells.""" + parent_cells = self.state_manager.get_parent_cells() + if not parent_cells: + return + my_cell_info = self.state_manager.get_my_state() + capabs = self.state_manager.get_our_capabilities() + LOG.debug(_("Updating parents with our capabilities: %(capabs)s"), + locals()) + # We have to turn the sets into lists so they can potentially + # be json encoded when the raw message is sent. + for key, values in capabs.items(): + capabs[key] = list(values) + method_kwargs = {'cell_name': my_cell_info.name, + 'capabilities': capabs} + for cell in parent_cells: + message = _TargetedMessage(self, ctxt, 'update_capabilities', + method_kwargs, 'up', cell, fanout=True) + message.process() + + def tell_parents_our_capacities(self, ctxt): + """Send our capacities to parent cells.""" + parent_cells = self.state_manager.get_parent_cells() + if not parent_cells: + return + my_cell_info = self.state_manager.get_my_state() + capacities = self.state_manager.get_our_capacities() + LOG.debug(_("Updating parents with our capacities: %(capacities)s"), + locals()) + method_kwargs = {'cell_name': my_cell_info.name, + 'capacities': capacities} + for cell in parent_cells: + message = _TargetedMessage(self, ctxt, 'update_capacities', + method_kwargs, 'up', cell, fanout=True) + message.process() + + def schedule_run_instance(self, ctxt, target_cell, host_sched_kwargs): + """Called by the scheduler to tell a child cell to schedule + a new instance for build. + """ + method_kwargs = dict(host_sched_kwargs=host_sched_kwargs) + message = _TargetedMessage(self, ctxt, 'schedule_run_instance', + method_kwargs, 'down', + target_cell) + message.process() + + def run_compute_api_method(self, ctxt, cell_name, method_info, call): + """Call a compute API method in a specific cell.""" + message = _TargetedMessage(self, ctxt, 'run_compute_api_method', + dict(method_info=method_info), 'down', + cell_name, need_response=call) + return message.process() + + def instance_update_at_top(self, ctxt, instance): + """Update an instance at the top level cell.""" + message = _BroadcastMessage(self, ctxt, 'instance_update_at_top', + dict(instance=instance), 'up', + run_locally=False) + message.process() + + def instance_destroy_at_top(self, ctxt, instance): + """Destroy an instance at the top level cell.""" + message = _BroadcastMessage(self, ctxt, 'instance_destroy_at_top', + dict(instance=instance), 'up', + run_locally=False) + message.process() + + def instance_delete_everywhere(self, ctxt, instance, delete_type): + """This is used by API cell when it didn't know what cell + an instance was in, but the instance was requested to be + deleted or soft_deleted. So, we'll broadcast this everywhere. + """ + method_kwargs = dict(instance=instance, delete_type=delete_type) + message = _BroadcastMessage(self, ctxt, + 'instance_delete_everywhere', + method_kwargs, 'down', + run_locally=False) + message.process() + + def instance_fault_create_at_top(self, ctxt, instance_fault): + """Create an instance fault at the top level cell.""" + message = _BroadcastMessage(self, ctxt, + 'instance_fault_create_at_top', + dict(instance_fault=instance_fault), + 'up', run_locally=False) + message.process() + + def bw_usage_update_at_top(self, ctxt, bw_update_info): + """Update bandwidth usage at top level cell.""" + message = _BroadcastMessage(self, ctxt, 'bw_usage_update_at_top', + dict(bw_update_info=bw_update_info), + 'up', run_locally=False) + message.process() + + @staticmethod + def get_message_types(): + return _CELL_MESSAGE_TYPE_TO_MESSAGE_CLS.keys() + + +class Response(object): + """Holds a response from a cell. If there was a failure, 'failure' + will be True and 'response' will contain an encoded Exception. + """ + def __init__(self, cell_name, value, failure): + self.failure = failure + self.cell_name = cell_name + self.value = value + + def to_json(self): + resp_value = self.value + if self.failure: + resp_value = rpc_common.serialize_remote_exception(resp_value, + log_failure=False) + _dict = {'cell_name': self.cell_name, + 'value': resp_value, + 'failure': self.failure} + return jsonutils.dumps(_dict) + + @classmethod + def from_json(cls, json_message): + _dict = jsonutils.loads(json_message) + if _dict['failure']: + resp_value = rpc_common.deserialize_remote_exception( + CONF, _dict['value']) + _dict['value'] = resp_value + return cls(**_dict) + + def value_or_raise(self): + if self.failure: + if isinstance(self.value, (tuple, list)): + raise self.value[0], self.value[1], self.value[2] + else: + raise self.value + return self.value diff --git a/nova/cells/opts.py b/nova/cells/opts.py new file mode 100644 index 000000000..45b453ebc --- /dev/null +++ b/nova/cells/opts.py @@ -0,0 +1,44 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 Rackspace Hosting +# +# 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. + +""" +Global cells config options +""" + +from nova.openstack.common import cfg + +cells_opts = [ + cfg.BoolOpt('enable', + default=False, + help='Enable cell functionality'), + cfg.StrOpt('topic', + default='cells', + help='the topic cells nodes listen on'), + cfg.StrOpt('manager', + default='nova.cells.manager.CellsManager', + help='Manager for cells'), + cfg.StrOpt('name', + default='nova', + help='name of this cell'), + cfg.ListOpt('capabilities', + default=['hypervisor=xenserver;kvm', 'os=linux;windows'], + help='Key/Multi-value list with the capabilities of the cell'), + cfg.IntOpt('call_timeout', + default=60, + help='Seconds to wait for response from a call to a cell.'), +] + +cfg.CONF.register_opts(cells_opts, group='cells') diff --git a/nova/cells/rpc_driver.py b/nova/cells/rpc_driver.py new file mode 100644 index 000000000..5e420aa8e --- /dev/null +++ b/nova/cells/rpc_driver.py @@ -0,0 +1,165 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. + +""" +Cells RPC Communication Driver +""" +from nova.cells import driver +from nova.openstack.common import cfg +from nova.openstack.common import rpc +from nova.openstack.common.rpc import dispatcher as rpc_dispatcher +from nova.openstack.common.rpc import proxy as rpc_proxy + +cell_rpc_driver_opts = [ + cfg.StrOpt('rpc_driver_queue_base', + default='cells.intercell', + help="Base queue name to use when communicating between " + "cells. Various topics by message type will be " + "appended to this.")] + +CONF = cfg.CONF +CONF.register_opts(cell_rpc_driver_opts, group='cells') +CONF.import_opt('call_timeout', 'nova.cells.opts', group='cells') + +_CELL_TO_CELL_RPC_API_VERSION = '1.0' + + +class CellsRPCDriver(driver.BaseCellsDriver): + """Driver for cell<->cell communication via RPC. This is used to + setup the RPC consumers as well as to send a message to another cell. + + One instance of this class will be created for every neighbor cell + that we find in the DB and it will be associated with the cell in + its CellState. + + One instance is also created by the cells manager for setting up + the consumers. + """ + BASE_RPC_API_VERSION = _CELL_TO_CELL_RPC_API_VERSION + + def __init__(self, *args, **kwargs): + super(CellsRPCDriver, self).__init__(*args, **kwargs) + self.rpc_connections = [] + self.intercell_rpcapi = InterCellRPCAPI( + self.BASE_RPC_API_VERSION) + + def _start_consumer(self, dispatcher, topic): + """Start an RPC consumer.""" + conn = rpc.create_connection(new=True) + conn.create_consumer(topic, dispatcher, fanout=False) + conn.create_consumer(topic, dispatcher, fanout=True) + self.rpc_connections.append(conn) + conn.consume_in_thread() + return conn + + def start_consumers(self, msg_runner): + """Start RPC consumers. + + Start up 2 separate consumers for handling inter-cell + communication via RPC. Both handle the same types of + messages, but requests/replies are separated to solve + potential deadlocks. (If we used the same queue for both, + it's possible to exhaust the RPC thread pool while we wait + for replies.. such that we'd never consume a reply.) + """ + topic_base = CONF.cells.rpc_driver_queue_base + proxy_manager = InterCellRPCDispatcher(msg_runner) + dispatcher = rpc_dispatcher.RpcDispatcher([proxy_manager]) + for msg_type in msg_runner.get_message_types(): + topic = '%s.%s' % (topic_base, msg_type) + self._start_consumer(dispatcher, topic) + + def stop_consumers(self): + """Stop RPC consumers. + + NOTE: Currently there's no hooks when stopping services + to have managers cleanup, so this is not currently called. + """ + for conn in self.rpc_connections: + conn.close() + + def send_message_to_cell(self, cell_state, message): + """Use the IntercellRPCAPI to send a message to a cell.""" + self.intercell_rpcapi.send_message_to_cell(cell_state, message) + + +class InterCellRPCAPI(rpc_proxy.RpcProxy): + """Client side of the Cell<->Cell RPC API. + + The CellsRPCDriver uses this to make calls to another cell. + + API version history: + 1.0 - Initial version. + """ + def __init__(self, default_version): + super(InterCellRPCAPI, self).__init__(None, default_version) + + @staticmethod + def _get_server_params_for_cell(next_hop): + """Turn the DB information for a cell into the parameters + needed for the RPC call. + """ + param_map = {'username': 'username', + 'password': 'password', + 'rpc_host': 'hostname', + 'rpc_port': 'port', + 'rpc_virtual_host': 'virtual_host'} + server_params = {} + for source, target in param_map.items(): + if next_hop.db_info[source]: + server_params[target] = next_hop.db_info[source] + return server_params + + def send_message_to_cell(self, cell_state, message): + """Send a message to another cell by JSON-ifying the message and + making an RPC cast to 'process_message'. If the message says to + fanout, do it. The topic that is used will be + 'CONF.rpc_driver_queue_base.<message_type>'. + """ + ctxt = message.ctxt + json_message = message.to_json() + rpc_message = self.make_msg('process_message', message=json_message) + topic_base = CONF.cells.rpc_driver_queue_base + topic = '%s.%s' % (topic_base, message.message_type) + server_params = self._get_server_params_for_cell(cell_state) + if message.fanout: + self.fanout_cast_to_server(ctxt, server_params, + rpc_message, topic=topic) + else: + self.cast_to_server(ctxt, server_params, + rpc_message, topic=topic) + + +class InterCellRPCDispatcher(object): + """RPC Dispatcher to handle messages received from other cells. + + All messages received here have come from a sibling cell. Depending + on the ultimate target and type of message, we may process the message + in this cell, relay the message to another sibling cell, or both. This + logic is defined by the message class in the messaging module. + """ + BASE_RPC_API_VERSION = _CELL_TO_CELL_RPC_API_VERSION + + def __init__(self, msg_runner): + """Init the Intercell RPC Dispatcher.""" + self.msg_runner = msg_runner + + def process_message(self, _ctxt, message): + """We received a message from another cell. Use the MessageRunner + to turn this from JSON back into an instance of the correct + Message class. Then process it! + """ + message = self.msg_runner.message_from_json(message) + message.process() diff --git a/nova/cells/rpcapi.py b/nova/cells/rpcapi.py new file mode 100644 index 000000000..8ce298829 --- /dev/null +++ b/nova/cells/rpcapi.py @@ -0,0 +1,138 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. + +""" +Client side of nova-cells RPC API (for talking to the nova-cells service +within a cell). + +This is different than communication between child and parent nova-cells +services. That communication is handled by the cells driver via the +messging module. +""" + +from nova.openstack.common import cfg +from nova.openstack.common import jsonutils +from nova.openstack.common import log as logging +from nova.openstack.common.rpc import proxy as rpc_proxy + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF +CONF.import_opt('enable', 'nova.cells.opts', group='cells') +CONF.import_opt('topic', 'nova.cells.opts', group='cells') + + +class CellsAPI(rpc_proxy.RpcProxy): + '''Cells client-side RPC API + + API version history: + + 1.0 - Initial version. + ''' + BASE_RPC_API_VERSION = '1.0' + + def __init__(self): + super(CellsAPI, self).__init__(topic=CONF.cells.topic, + default_version=self.BASE_RPC_API_VERSION) + + def cast_compute_api_method(self, ctxt, cell_name, method, + *args, **kwargs): + """Make a cast to a compute API method in a certain cell.""" + method_info = {'method': method, + 'method_args': args, + 'method_kwargs': kwargs} + self.cast(ctxt, self.make_msg('run_compute_api_method', + cell_name=cell_name, + method_info=method_info, + call=False)) + + def call_compute_api_method(self, ctxt, cell_name, method, + *args, **kwargs): + """Make a call to a compute API method in a certain cell.""" + method_info = {'method': method, + 'method_args': args, + 'method_kwargs': kwargs} + return self.call(ctxt, self.make_msg('run_compute_api_method', + cell_name=cell_name, + method_info=method_info, + call=True)) + + def schedule_run_instance(self, ctxt, **kwargs): + """Schedule a new instance for creation.""" + self.cast(ctxt, self.make_msg('schedule_run_instance', + host_sched_kwargs=kwargs)) + + def instance_update_at_top(self, ctxt, instance): + """Update instance at API level.""" + if not CONF.cells.enable: + return + # Make sure we have a dict, not a SQLAlchemy model + instance_p = jsonutils.to_primitive(instance) + self.cast(ctxt, self.make_msg('instance_update_at_top', + instance=instance_p)) + + def instance_destroy_at_top(self, ctxt, instance): + """Destroy instance at API level.""" + if not CONF.cells.enable: + return + instance_p = jsonutils.to_primitive(instance) + self.cast(ctxt, self.make_msg('instance_destroy_at_top', + instance=instance_p)) + + def instance_delete_everywhere(self, ctxt, instance, delete_type): + """Delete instance everywhere. delete_type may be 'soft' + or 'hard'. This is generally only used to resolve races + when API cell doesn't know to what cell an instance belongs. + """ + if not CONF.cells.enable: + return + instance_p = jsonutils.to_primitive(instance) + self.cast(ctxt, self.make_msg('instance_delete_everywhere', + instance=instance_p, + delete_type=delete_type)) + + def instance_fault_create_at_top(self, ctxt, instance_fault): + """Create an instance fault at the top.""" + if not CONF.cells.enable: + return + instance_fault_p = jsonutils.to_primitive(instance_fault) + self.cast(ctxt, self.make_msg('instance_fault_create_at_top', + instance_fault=instance_fault_p)) + + def bw_usage_update_at_top(self, ctxt, uuid, mac, start_period, + bw_in, bw_out, last_ctr_in, last_ctr_out, last_refreshed=None): + """Broadcast upwards that bw_usage was updated.""" + if not CONF.cells.enable: + return + bw_update_info = {'uuid': uuid, + 'mac': mac, + 'start_period': start_period, + 'bw_in': bw_in, + 'bw_out': bw_out, + 'last_ctr_in': last_ctr_in, + 'last_ctr_out': last_ctr_out, + 'last_refreshed': last_refreshed} + self.cast(ctxt, self.make_msg('bw_usage_update_at_top', + bw_update_info=bw_update_info)) + + def instance_info_cache_update_at_top(self, ctxt, instance_info_cache): + """Broadcast up that an instance's info_cache has changed.""" + if not CONF.cells.enable: + return + iicache = jsonutils.to_primitive(instance_info_cache) + instance = {'uuid': iicache['instance_uuid'], + 'info_cache': iicache} + self.cast(ctxt, self.make_msg('instance_update_at_top', + instance=instance)) diff --git a/nova/cells/scheduler.py b/nova/cells/scheduler.py new file mode 100644 index 000000000..0b730290a --- /dev/null +++ b/nova/cells/scheduler.py @@ -0,0 +1,136 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. + +""" +Cells Scheduler +""" +import random +import time + +from nova import compute +from nova.compute import vm_states +from nova.db import base +from nova import exception +from nova.openstack.common import cfg +from nova.openstack.common import log as logging +from nova.scheduler import rpcapi as scheduler_rpcapi + +cell_scheduler_opts = [ + cfg.IntOpt('scheduler_retries', + default=10, + help='How many retries when no cells are available.'), + cfg.IntOpt('scheduler_retry_delay', + default=2, + help='How often to retry in seconds when no cells are ' + 'available.') +] + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF +CONF.register_opts(cell_scheduler_opts, group='cells') + + +class CellsScheduler(base.Base): + """The cells scheduler.""" + + def __init__(self, msg_runner): + super(CellsScheduler, self).__init__() + self.msg_runner = msg_runner + self.state_manager = msg_runner.state_manager + self.compute_api = compute.API() + self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI() + + def _create_instances_here(self, ctxt, request_spec): + instance_values = request_spec['instance_properties'] + for instance_uuid in request_spec['instance_uuids']: + instance_values['uuid'] = instance_uuid + instance = self.compute_api.create_db_entry_for_new_instance( + ctxt, + request_spec['instance_type'], + request_spec['image'], + instance_values, + request_spec['security_group'], + request_spec['block_device_mapping']) + self.msg_runner.instance_update_at_top(ctxt, instance) + + def _get_possible_cells(self): + cells = set(self.state_manager.get_child_cells()) + our_cell = self.state_manager.get_my_state() + # Include our cell in the list, if we have any capacity info + if not cells or our_cell.capacities: + cells.add(our_cell) + return cells + + def _run_instance(self, message, host_sched_kwargs): + """Attempt to schedule instance(s). If we have no cells + to try, raise exception.NoCellsAvailable + """ + ctxt = message.ctxt + request_spec = host_sched_kwargs['request_spec'] + + # The message we might forward to a child cell + cells = self._get_possible_cells() + if not cells: + raise exception.NoCellsAvailable() + cells = list(cells) + + # Random selection for now + random.shuffle(cells) + target_cell = cells[0] + + LOG.debug(_("Scheduling with routing_path=%(routing_path)s"), + locals()) + + if target_cell.is_me: + # Need to create instance DB entries as the host scheduler + # expects that the instance(s) already exists. + self._create_instances_here(ctxt, request_spec) + self.scheduler_rpcapi.run_instance(ctxt, + **host_sched_kwargs) + return + self.msg_runner.schedule_run_instance(ctxt, target_cell, + host_sched_kwargs) + + def run_instance(self, message, host_sched_kwargs): + """Pick a cell where we should create a new instance.""" + try: + for i in xrange(max(0, CONF.cells.scheduler_retries) + 1): + try: + return self._run_instance(message, host_sched_kwargs) + except exception.NoCellsAvailable: + if i == max(0, CONF.cells.scheduler_retries): + raise + sleep_time = max(1, CONF.cells.scheduler_retry_delay) + LOG.info(_("No cells available when scheduling. Will " + "retry in %(sleep_time)s second(s)"), locals()) + time.sleep(sleep_time) + continue + except Exception: + request_spec = host_sched_kwargs['request_spec'] + instance_uuids = request_spec['instance_uuids'] + LOG.exception(_("Error scheduling instances %(instance_uuids)s"), + locals()) + ctxt = message.ctxt + for instance_uuid in instance_uuids: + self.msg_runner.instance_update_at_top(ctxt, + {'uuid': instance_uuid, + 'vm_state': vm_states.ERROR}) + try: + self.db.instance_update(ctxt, + instance_uuid, + {'vm_state': vm_states.ERROR}) + except Exception: + pass diff --git a/nova/cells/state.py b/nova/cells/state.py new file mode 100644 index 000000000..c6f8f3220 --- /dev/null +++ b/nova/cells/state.py @@ -0,0 +1,346 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. + +""" +CellState Manager +""" +import copy +import datetime +import functools + +from nova.cells import rpc_driver +from nova import context +from nova.db import base +from nova.openstack.common import cfg +from nova.openstack.common import lockutils +from nova.openstack.common import log as logging +from nova.openstack.common import timeutils + +cell_state_manager_opts = [ + cfg.IntOpt('db_check_interval', + default=60, + help='Seconds between getting fresh cell info from db.'), +] + + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF +CONF.import_opt('host', 'nova.config') +CONF.import_opt('name', 'nova.cells.opts', group='cells') +#CONF.import_opt('capabilities', 'nova.cells.opts', group='cells') +CONF.register_opts(cell_state_manager_opts, group='cells') + + +class CellState(object): + """Holds information for a particular cell.""" + def __init__(self, cell_name, is_me=False): + self.name = cell_name + self.is_me = is_me + self.last_seen = datetime.datetime.min + self.capabilities = {} + self.capacities = {} + self.db_info = {} + # TODO(comstud): The DB will specify the driver to use to talk + # to this cell, but there's no column for this yet. The only + # available driver is the rpc driver. + self.driver = rpc_driver.CellsRPCDriver() + + def update_db_info(self, cell_db_info): + """Update cell credentials from db""" + self.db_info = dict( + [(k, v) for k, v in cell_db_info.iteritems() + if k != 'name']) + + def update_capabilities(self, cell_metadata): + """Update cell capabilities for a cell.""" + self.last_seen = timeutils.utcnow() + self.capabilities = cell_metadata + + def update_capacities(self, capacities): + """Update capacity information for a cell.""" + self.last_seen = timeutils.utcnow() + self.capacities = capacities + + def get_cell_info(self): + """Return subset of cell information for OS API use.""" + db_fields_to_return = ['id', 'is_parent', 'weight_scale', + 'weight_offset', 'username', 'rpc_host', 'rpc_port'] + cell_info = dict(name=self.name, capabilities=self.capabilities) + if self.db_info: + for field in db_fields_to_return: + cell_info[field] = self.db_info[field] + return cell_info + + def send_message(self, message): + """Send a message to a cell. Just forward this to the driver, + passing ourselves and the message as arguments. + """ + self.driver.send_message_to_cell(self, message) + + def __repr__(self): + me = "me" if self.is_me else "not_me" + return "Cell '%s' (%s)" % (self.name, me) + + +def sync_from_db(f): + """Use as a decorator to wrap methods that use cell information to + make sure they sync the latest information from the DB periodically. + """ + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if self._time_to_sync(): + self._cell_db_sync() + return f(self, *args, **kwargs) + return wrapper + + +class CellStateManager(base.Base): + def __init__(self, cell_state_cls=None): + super(CellStateManager, self).__init__() + if not cell_state_cls: + cell_state_cls = CellState + self.cell_state_cls = cell_state_cls + self.my_cell_state = cell_state_cls(CONF.cells.name, is_me=True) + self.parent_cells = {} + self.child_cells = {} + self.last_cell_db_check = datetime.datetime.min + self._cell_db_sync() + my_cell_capabs = {} + for cap in CONF.cells.capabilities: + name, value = cap.split('=', 1) + if ';' in value: + values = set(value.split(';')) + else: + values = set([value]) + my_cell_capabs[name] = values + self.my_cell_state.update_capabilities(my_cell_capabs) + + def _refresh_cells_from_db(self, ctxt): + """Make our cell info map match the db.""" + # Add/update existing cells ... + db_cells = self.db.cell_get_all(ctxt) + db_cells_dict = dict([(cell['name'], cell) for cell in db_cells]) + + # Update current cells. Delete ones that disappeared + for cells_dict in (self.parent_cells, self.child_cells): + for cell_name, cell_info in cells_dict.items(): + is_parent = cell_info.db_info['is_parent'] + db_dict = db_cells_dict.get(cell_name) + if db_dict and is_parent == db_dict['is_parent']: + cell_info.update_db_info(db_dict) + else: + del cells_dict[cell_name] + + # Add new cells + for cell_name, db_info in db_cells_dict.items(): + if db_info['is_parent']: + cells_dict = self.parent_cells + else: + cells_dict = self.child_cells + if cell_name not in cells_dict: + cells_dict[cell_name] = self.cell_state_cls(cell_name) + cells_dict[cell_name].update_db_info(db_info) + + def _time_to_sync(self): + """Is it time to sync the DB against our memory cache?""" + diff = timeutils.utcnow() - self.last_cell_db_check + return diff.seconds >= CONF.cells.db_check_interval + + def _update_our_capacity(self, context): + """Update our capacity in the self.my_cell_state CellState. + + This will add/update 2 entries in our CellState.capacities, + 'ram_free' and 'disk_free'. + + The values of these are both dictionaries with the following + format: + + {'total_mb': <total_memory_free_in_the_cell>, + 'units_by_mb: <units_dictionary>} + + <units_dictionary> contains the number of units that we can + build for every instance_type that we have. This number is + computed by looking at room available on every compute_node. + + Take the following instance_types as an example: + + [{'memory_mb': 1024, 'root_gb': 10, 'ephemeral_gb': 100}, + {'memory_mb': 2048, 'root_gb': 20, 'ephemeral_gb': 200}] + + capacities['ram_free']['units_by_mb'] would contain the following: + + {'1024': <number_of_instances_that_will_fit>, + '2048': <number_of_instances_that_will_fit>} + + capacities['disk_free']['units_by_mb'] would contain the following: + + {'122880': <number_of_instances_that_will_fit>, + '225280': <number_of_instances_that_will_fit>} + + Units are in MB, so 122880 = (10 + 100) * 1024. + + NOTE(comstud): Perhaps we should only report a single number + available per instance_type. + """ + + compute_hosts = {} + + def _get_compute_hosts(): + compute_nodes = self.db.compute_node_get_all(context) + for compute in compute_nodes: + service = compute['service'] + if not service or service['disabled']: + continue + host = service['host'] + compute_hosts[host] = { + 'free_ram_mb': compute['free_ram_mb'], + 'free_disk_mb': compute['free_disk_gb'] * 1024} + + _get_compute_hosts() + if not compute_hosts: + self.my_cell_state.update_capacities({}) + return + + ram_mb_free_units = {} + disk_mb_free_units = {} + total_ram_mb_free = 0 + total_disk_mb_free = 0 + + def _free_units(tot, per_inst): + if per_inst: + return max(0, int(tot / per_inst)) + else: + return 0 + + def _update_from_values(values, instance_type): + memory_mb = instance_type['memory_mb'] + disk_mb = (instance_type['root_gb'] + + instance_type['ephemeral_gb']) * 1024 + ram_mb_free_units.setdefault(str(memory_mb), 0) + disk_mb_free_units.setdefault(str(disk_mb), 0) + ram_free_units = _free_units(compute_values['free_ram_mb'], + memory_mb) + disk_free_units = _free_units(compute_values['free_disk_mb'], + disk_mb) + ram_mb_free_units[str(memory_mb)] += ram_free_units + disk_mb_free_units[str(disk_mb)] += disk_free_units + + instance_types = self.db.instance_type_get_all(context) + + for compute_values in compute_hosts.values(): + total_ram_mb_free += compute_values['free_ram_mb'] + total_disk_mb_free += compute_values['free_disk_mb'] + for instance_type in instance_types: + _update_from_values(compute_values, instance_type) + + capacities = {'ram_free': {'total_mb': total_ram_mb_free, + 'units_by_mb': ram_mb_free_units}, + 'disk_free': {'total_mb': total_disk_mb_free, + 'units_by_mb': disk_mb_free_units}} + self.my_cell_state.update_capacities(capacities) + + @lockutils.synchronized('cell-db-sync', 'nova-') + def _cell_db_sync(self): + """Update status for all cells if it's time. Most calls to + this are from the check_for_update() decorator that checks + the time, but it checks outside of a lock. The duplicate + check here is to prevent multiple threads from pulling the + information simultaneously. + """ + if self._time_to_sync(): + LOG.debug(_("Updating cell cache from db.")) + self.last_cell_db_check = timeutils.utcnow() + ctxt = context.get_admin_context() + self._refresh_cells_from_db(ctxt) + self._update_our_capacity(ctxt) + + @sync_from_db + def get_my_state(self): + """Return information for my (this) cell.""" + return self.my_cell_state + + @sync_from_db + def get_child_cells(self): + """Return list of child cell_infos.""" + return self.child_cells.values() + + @sync_from_db + def get_parent_cells(self): + """Return list of parent cell_infos.""" + return self.parent_cells.values() + + @sync_from_db + def get_parent_cell(self, cell_name): + return self.parent_cells.get(cell_name) + + @sync_from_db + def get_child_cell(self, cell_name): + return self.child_cells.get(cell_name) + + @sync_from_db + def update_cell_capabilities(self, cell_name, capabilities): + """Update capabilities for a cell.""" + cell = self.child_cells.get(cell_name) + if not cell: + cell = self.parent_cells.get(cell_name) + if not cell: + LOG.error(_("Unknown cell '%(cell_name)s' when trying to " + "update capabilities"), locals()) + return + # Make sure capabilities are sets. + for capab_name, values in capabilities.items(): + capabilities[capab_name] = set(values) + cell.update_capabilities(capabilities) + + @sync_from_db + def update_cell_capacities(self, cell_name, capacities): + """Update capacities for a cell.""" + cell = self.child_cells.get(cell_name) + if not cell: + cell = self.parent_cells.get(cell_name) + if not cell: + LOG.error(_("Unknown cell '%(cell_name)s' when trying to " + "update capacities"), locals()) + return + cell.update_capacities(capacities) + + @sync_from_db + def get_our_capabilities(self, include_children=True): + capabs = copy.deepcopy(self.my_cell_state.capabilities) + if include_children: + for cell in self.child_cells.values(): + for capab_name, values in cell.capabilities.items(): + if capab_name not in capabs: + capabs[capab_name] = set([]) + capabs[capab_name] |= values + return capabs + + def _add_to_dict(self, target, src): + for key, value in src.items(): + if isinstance(value, dict): + target.setdefault(key, {}) + self._add_to_dict(target[key], value) + continue + target.setdefault(key, 0) + target[key] += value + + @sync_from_db + def get_our_capacities(self, include_children=True): + capacities = copy.deepcopy(self.my_cell_state.capacities) + if include_children: + for cell in self.child_cells.values(): + self._add_to_dict(capacities, cell.capacities) + return capacities diff --git a/nova/cells/utils.py b/nova/cells/utils.py new file mode 100644 index 000000000..d25f98fab --- /dev/null +++ b/nova/cells/utils.py @@ -0,0 +1,48 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. + +""" +Cells Utility Methods +""" +import random + +from nova import db + + +def get_instances_to_sync(context, updated_since=None, project_id=None, + deleted=True, shuffle=False, uuids_only=False): + """Return a generator that will return a list of active and + deleted instances to sync with parent cells. The list may + optionally be shuffled for periodic updates so that multiple + cells services aren't self-healing the same instances in nearly + lockstep. + """ + filters = {} + if updated_since is not None: + filters['changes-since'] = updated_since + if project_id is not None: + filters['project_id'] = project_id + if not deleted: + filters['deleted'] = False + # Active instances first. + instances = db.instance_get_all_by_filters( + context, filters, 'deleted', 'asc') + if shuffle: + random.shuffle(instances) + for instance in instances: + if uuids_only: + yield instance['uuid'] + else: + yield instance diff --git a/nova/cert/manager.py b/nova/cert/manager.py index 2d17a675a..3a00c47a6 100644 --- a/nova/cert/manager.py +++ b/nova/cert/manager.py @@ -34,7 +34,7 @@ LOG = logging.getLogger(__name__) class CertManager(manager.Manager): - RPC_API_VERSION = '1.0' + RPC_API_VERSION = '1.1' def init_host(self): crypto.ensure_ca_filesystem() @@ -66,3 +66,6 @@ class CertManager(manager.Manager): def decrypt_text(self, context, project_id, text): """Decrypt base64 encoded text using the projects private key.""" return crypto.decrypt_text(project_id, base64.b64decode(text)) + + def get_backdoor_port(self, context): + return self.backdoor_port diff --git a/nova/cert/rpcapi.py b/nova/cert/rpcapi.py index 35d02b643..79b136571 100644 --- a/nova/cert/rpcapi.py +++ b/nova/cert/rpcapi.py @@ -31,6 +31,7 @@ class CertAPI(nova.openstack.common.rpc.proxy.RpcProxy): API version history: 1.0 - Initial version. + 1.1 - Added get_backdoor_port() ''' # @@ -78,3 +79,7 @@ class CertAPI(nova.openstack.common.rpc.proxy.RpcProxy): return self.call(ctxt, self.make_msg('decrypt_text', project_id=project_id, text=text)) + + def get_backdoor_port(self, context, host): + return self.call(context, self.make_msg('get_backdoor_port'), + version='1.1') diff --git a/nova/compute/api.py b/nova/compute/api.py index 757f78f2d..abbc0bd92 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -1954,6 +1954,14 @@ class API(base.Base): return {'url': connect_info['access_url']} + def get_vnc_connect_info(self, context, instance, console_type): + """Used in a child cell to get console info.""" + if not instance['host']: + raise exception.InstanceNotReady(instance_id=instance['uuid']) + connect_info = self.compute_rpcapi.get_vnc_console(context, + instance=instance, console_type=console_type) + return connect_info + @wrap_check_policy def get_console_output(self, context, instance, tail_length=None): """Get console output for an instance.""" diff --git a/nova/compute/cells_api.py b/nova/compute/cells_api.py new file mode 100644 index 000000000..cdbccebb1 --- /dev/null +++ b/nova/compute/cells_api.py @@ -0,0 +1,471 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. + +"""Compute API that proxies via Cells Service""" + +from nova import block_device +from nova.cells import rpcapi as cells_rpcapi +from nova.compute import api as compute_api +from nova.compute import task_states +from nova.compute import vm_states +from nova import exception +from nova.openstack.common import excutils +from nova.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +check_instance_state = compute_api.check_instance_state +wrap_check_policy = compute_api.wrap_check_policy +check_policy = compute_api.check_policy +check_instance_lock = compute_api.check_instance_lock + + +def validate_cell(fn): + def _wrapped(self, context, instance, *args, **kwargs): + self._validate_cell(instance, fn.__name__) + return fn(self, context, instance, *args, **kwargs) + _wrapped.__name__ = fn.__name__ + return _wrapped + + +class ComputeRPCAPINoOp(object): + def __getattr__(self, key): + def _noop_rpc_wrapper(*args, **kwargs): + return None + return _noop_rpc_wrapper + + +class SchedulerRPCAPIRedirect(object): + def __init__(self, cells_rpcapi_obj): + self.cells_rpcapi = cells_rpcapi_obj + + def __getattr__(self, key): + def _noop_rpc_wrapper(*args, **kwargs): + return None + return _noop_rpc_wrapper + + def run_instance(self, context, **kwargs): + self.cells_rpcapi.schedule_run_instance(context, **kwargs) + + +class ComputeCellsAPI(compute_api.API): + def __init__(self, *args, **kwargs): + super(ComputeCellsAPI, self).__init__(*args, **kwargs) + self.cells_rpcapi = cells_rpcapi.CellsAPI() + # Avoid casts/calls directly to compute + self.compute_rpcapi = ComputeRPCAPINoOp() + # Redirect scheduler run_instance to cells. + self.scheduler_rpcapi = SchedulerRPCAPIRedirect(self.cells_rpcapi) + + def _cell_read_only(self, cell_name): + """Is the target cell in a read-only mode?""" + # FIXME(comstud): Add support for this. + return False + + def _validate_cell(self, instance, method): + cell_name = instance['cell_name'] + if not cell_name: + raise exception.InstanceUnknownCell( + instance_uuid=instance['uuid']) + if self._cell_read_only(cell_name): + raise exception.InstanceInvalidState( + attr="vm_state", + instance_uuid=instance['uuid'], + state="temporary_readonly", + method=method) + + def _cast_to_cells(self, context, instance, method, *args, **kwargs): + instance_uuid = instance['uuid'] + cell_name = instance['cell_name'] + if not cell_name: + raise exception.InstanceUnknownCell(instance_uuid=instance_uuid) + self.cells_rpcapi.cast_compute_api_method(context, cell_name, + method, instance_uuid, *args, **kwargs) + + def _call_to_cells(self, context, instance, method, *args, **kwargs): + instance_uuid = instance['uuid'] + cell_name = instance['cell_name'] + if not cell_name: + raise exception.InstanceUnknownCell(instance_uuid=instance_uuid) + return self.cells_rpcapi.call_compute_api_method(context, cell_name, + method, instance_uuid, *args, **kwargs) + + def _check_requested_networks(self, context, requested_networks): + """Override compute API's checking of this. It'll happen in + child cell + """ + return + + def _validate_image_href(self, context, image_href): + """Override compute API's checking of this. It'll happen in + child cell + """ + return + + def _create_image(self, context, instance, name, image_type, + backup_type=None, rotation=None, extra_properties=None): + if backup_type: + return self._call_to_cells(context, instance, 'backup', + name, backup_type, rotation, + extra_properties=extra_properties) + else: + return self._call_to_cells(context, instance, 'snapshot', + name, extra_properties=extra_properties) + + def create(self, *args, **kwargs): + """We can use the base functionality, but I left this here just + for completeness. + """ + return super(ComputeCellsAPI, self).create(*args, **kwargs) + + @validate_cell + def update(self, context, instance, **kwargs): + """Update an instance.""" + rv = super(ComputeCellsAPI, self).update(context, + instance, **kwargs) + # We need to skip vm_state/task_state updates... those will + # happen when via a a _cast_to_cells for running a different + # compute api method + kwargs_copy = kwargs.copy() + kwargs_copy.pop('vm_state', None) + kwargs_copy.pop('task_state', None) + if kwargs_copy: + try: + self._cast_to_cells(context, instance, 'update', + **kwargs_copy) + except exception.InstanceUnknownCell: + pass + return rv + + def _local_delete(self, context, instance, bdms): + # This will get called for every delete in the API cell + # because _delete() in compute/api.py will not find a + # service when checking if it's up. + # We need to only take action if there's no cell_name. Our + # overrides of delete() and soft_delete() will take care of + # the rest. + cell_name = instance['cell_name'] + if not cell_name: + return super(ComputeCellsAPI, self)._local_delete(context, + instance, bdms) + + def soft_delete(self, context, instance): + self._handle_cell_delete(context, instance, + super(ComputeCellsAPI, self).soft_delete, 'soft_delete') + + def delete(self, context, instance): + self._handle_cell_delete(context, instance, + super(ComputeCellsAPI, self).delete, 'delete') + + def _handle_cell_delete(self, context, instance, method, method_name): + """Terminate an instance.""" + # We can't use the decorator because we have special logic in the + # case we don't know the cell_name... + cell_name = instance['cell_name'] + if cell_name and self._cell_read_only(cell_name): + raise exception.InstanceInvalidState( + attr="vm_state", + instance_uuid=instance['uuid'], + state="temporary_readonly", + method=method_name) + method(context, instance) + try: + self._cast_to_cells(context, instance, method_name) + except exception.InstanceUnknownCell: + # If there's no cell, there's also no host... which means + # the instance was destroyed from the DB here. Let's just + # broadcast a message down to all cells and hope this ends + # up resolving itself... Worse case.. the instance will + # show back up again here. + delete_type = method == 'soft_delete' and 'soft' or 'hard' + self.cells_rpcapi.instance_delete_everywhere(context, + instance['uuid'], delete_type) + + @validate_cell + def restore(self, context, instance): + """Restore a previously deleted (but not reclaimed) instance.""" + super(ComputeCellsAPI, self).restore(context, instance) + self._cast_to_cells(context, instance, 'restore') + + @validate_cell + def force_delete(self, context, instance): + """Force delete a previously deleted (but not reclaimed) instance.""" + super(ComputeCellsAPI, self).force_delete(context, instance) + self._cast_to_cells(context, instance, 'force_delete') + + @validate_cell + def stop(self, context, instance, do_cast=True): + """Stop an instance.""" + super(ComputeCellsAPI, self).stop(context, instance) + if do_cast: + self._cast_to_cells(context, instance, 'stop', do_cast=True) + else: + return self._call_to_cells(context, instance, 'stop', + do_cast=False) + + @validate_cell + def start(self, context, instance): + """Start an instance.""" + super(ComputeCellsAPI, self).start(context, instance) + self._cast_to_cells(context, instance, 'start') + + @validate_cell + def reboot(self, context, instance, *args, **kwargs): + """Reboot the given instance.""" + super(ComputeCellsAPI, self).reboot(context, instance, + *args, **kwargs) + self._cast_to_cells(context, instance, 'reboot', *args, + **kwargs) + + @validate_cell + def rebuild(self, context, instance, *args, **kwargs): + """Rebuild the given instance with the provided attributes.""" + super(ComputeCellsAPI, self).rebuild(context, instance, *args, + **kwargs) + self._cast_to_cells(context, instance, 'rebuild', *args, **kwargs) + + @check_instance_state(vm_state=[vm_states.RESIZED]) + @validate_cell + def revert_resize(self, context, instance): + """Reverts a resize, deleting the 'new' instance in the process.""" + # NOTE(markwash): regular api manipulates the migration here, but we + # don't have access to it. So to preserve the interface just update the + # vm and task state. + self.update(context, instance, + task_state=task_states.RESIZE_REVERTING) + self._cast_to_cells(context, instance, 'revert_resize') + + @check_instance_state(vm_state=[vm_states.RESIZED]) + @validate_cell + def confirm_resize(self, context, instance): + """Confirms a migration/resize and deletes the 'old' instance.""" + # NOTE(markwash): regular api manipulates migration here, but we don't + # have the migration in the api database. So to preserve the interface + # just update the vm and task state without calling super() + self.update(context, instance, task_state=None, + vm_state=vm_states.ACTIVE) + self._cast_to_cells(context, instance, 'confirm_resize') + + @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED], + task_state=[None]) + @validate_cell + def resize(self, context, instance, *args, **kwargs): + """Resize (ie, migrate) a running instance. + + If flavor_id is None, the process is considered a migration, keeping + the original flavor_id. If flavor_id is not None, the instance should + be migrated to a new host and resized to the new flavor_id. + """ + super(ComputeCellsAPI, self).resize(context, instance, *args, + **kwargs) + # FIXME(comstud): pass new instance_type object down to a method + # that'll unfold it + self._cast_to_cells(context, instance, 'resize', *args, **kwargs) + + @validate_cell + def add_fixed_ip(self, context, instance, *args, **kwargs): + """Add fixed_ip from specified network to given instance.""" + super(ComputeCellsAPI, self).add_fixed_ip(context, instance, + *args, **kwargs) + self._cast_to_cells(context, instance, 'add_fixed_ip', + *args, **kwargs) + + @validate_cell + def remove_fixed_ip(self, context, instance, *args, **kwargs): + """Remove fixed_ip from specified network to given instance.""" + super(ComputeCellsAPI, self).remove_fixed_ip(context, instance, + *args, **kwargs) + self._cast_to_cells(context, instance, 'remove_fixed_ip', + *args, **kwargs) + + @validate_cell + def pause(self, context, instance): + """Pause the given instance.""" + super(ComputeCellsAPI, self).pause(context, instance) + self._cast_to_cells(context, instance, 'pause') + + @validate_cell + def unpause(self, context, instance): + """Unpause the given instance.""" + super(ComputeCellsAPI, self).unpause(context, instance) + self._cast_to_cells(context, instance, 'unpause') + + def set_host_enabled(self, context, host, enabled): + """Sets the specified host's ability to accept new instances.""" + # FIXME(comstud): Since there's no instance here, we have no + # idea which cell should be the target. + pass + + def host_power_action(self, context, host, action): + """Reboots, shuts down or powers up the host.""" + # FIXME(comstud): Since there's no instance here, we have no + # idea which cell should be the target. + pass + + def get_diagnostics(self, context, instance): + """Retrieve diagnostics for the given instance.""" + # FIXME(comstud): Cache this? + # Also: only calling super() to get state/policy checking + super(ComputeCellsAPI, self).get_diagnostics(context, instance) + return self._call_to_cells(context, instance, 'get_diagnostics') + + @validate_cell + def suspend(self, context, instance): + """Suspend the given instance.""" + super(ComputeCellsAPI, self).suspend(context, instance) + self._cast_to_cells(context, instance, 'suspend') + + @validate_cell + def resume(self, context, instance): + """Resume the given instance.""" + super(ComputeCellsAPI, self).resume(context, instance) + self._cast_to_cells(context, instance, 'resume') + + @validate_cell + def rescue(self, context, instance, rescue_password=None): + """Rescue the given instance.""" + super(ComputeCellsAPI, self).rescue(context, instance, + rescue_password=rescue_password) + self._cast_to_cells(context, instance, 'rescue', + rescue_password=rescue_password) + + @validate_cell + def unrescue(self, context, instance): + """Unrescue the given instance.""" + super(ComputeCellsAPI, self).unrescue(context, instance) + self._cast_to_cells(context, instance, 'unrescue') + + @validate_cell + def set_admin_password(self, context, instance, password=None): + """Set the root/admin password for the given instance.""" + super(ComputeCellsAPI, self).set_admin_password(context, instance, + password=password) + self._cast_to_cells(context, instance, 'set_admin_password', + password=password) + + @validate_cell + def inject_file(self, context, instance, *args, **kwargs): + """Write a file to the given instance.""" + super(ComputeCellsAPI, self).inject_file(context, instance, *args, + **kwargs) + self._cast_to_cells(context, instance, 'inject_file', *args, **kwargs) + + @wrap_check_policy + @validate_cell + def get_vnc_console(self, context, instance, console_type): + """Get a url to a VNC Console.""" + if not instance['host']: + raise exception.InstanceNotReady(instance_id=instance['uuid']) + + connect_info = self._call_to_cells(context, instance, + 'get_vnc_connect_info', console_type) + + self.consoleauth_rpcapi.authorize_console(context, + connect_info['token'], console_type, connect_info['host'], + connect_info['port'], connect_info['internal_access_path']) + return {'url': connect_info['access_url']} + + @validate_cell + def get_console_output(self, context, instance, *args, **kwargs): + """Get console output for an an instance.""" + # NOTE(comstud): Calling super() just to get policy check + super(ComputeCellsAPI, self).get_console_output(context, instance, + *args, **kwargs) + return self._call_to_cells(context, instance, 'get_console_output', + *args, **kwargs) + + def lock(self, context, instance): + """Lock the given instance.""" + super(ComputeCellsAPI, self).lock(context, instance) + self._cast_to_cells(context, instance, 'lock') + + def unlock(self, context, instance): + """Unlock the given instance.""" + super(ComputeCellsAPI, self).lock(context, instance) + self._cast_to_cells(context, instance, 'unlock') + + @validate_cell + def reset_network(self, context, instance): + """Reset networking on the instance.""" + super(ComputeCellsAPI, self).reset_network(context, instance) + self._cast_to_cells(context, instance, 'reset_network') + + @validate_cell + def inject_network_info(self, context, instance): + """Inject network info for the instance.""" + super(ComputeCellsAPI, self).inject_network_info(context, instance) + self._cast_to_cells(context, instance, 'inject_network_info') + + @wrap_check_policy + @validate_cell + def attach_volume(self, context, instance, volume_id, device=None): + """Attach an existing volume to an existing instance.""" + if device and not block_device.match_device(device): + raise exception.InvalidDevicePath(path=device) + device = self.compute_rpcapi.reserve_block_device_name( + context, device=device, instance=instance, volume_id=volume_id) + try: + volume = self.volume_api.get(context, volume_id) + self.volume_api.check_attach(context, volume) + except Exception: + with excutils.save_and_reraise_exception(): + self.db.block_device_mapping_destroy_by_instance_and_device( + context, instance['uuid'], device) + self._cast_to_cells(context, instance, 'attach_volume', + volume_id, device) + + @check_instance_lock + @validate_cell + def _detach_volume(self, context, instance, volume_id): + """Detach a volume from an instance.""" + check_policy(context, 'detach_volume', instance) + + volume = self.volume_api.get(context, volume_id) + self.volume_api.check_detach(context, volume) + self._cast_to_cells(context, instance, 'detach_volume', + volume_id) + + @wrap_check_policy + @validate_cell + def associate_floating_ip(self, context, instance, address): + """Makes calls to network_api to associate_floating_ip. + + :param address: is a string floating ip address + """ + self._cast_to_cells(context, instance, 'associate_floating_ip', + address) + + @validate_cell + def delete_instance_metadata(self, context, instance, key): + """Delete the given metadata item from an instance.""" + super(ComputeCellsAPI, self).delete_instance_metadata(context, + instance, key) + self._cast_to_cells(context, instance, 'delete_instance_metadata', + key) + + @wrap_check_policy + @validate_cell + def update_instance_metadata(self, context, instance, + metadata, delete=False): + rv = super(ComputeCellsAPI, self).update_instance_metadata(context, + instance, metadata, delete=delete) + try: + self._cast_to_cells(context, instance, + 'update_instance_metadata', + metadata, delete=delete) + except exception.InstanceUnknownCell: + pass + return rv diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 6bf171635..7ac6b1518 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -116,9 +116,9 @@ interval_opts = [ default=120, help='Interval in seconds for querying the host status'), cfg.IntOpt("image_cache_manager_interval", - default=40, - help="Number of periodic scheduler ticks to wait between " - "runs of the image cache manager."), + default=2400, + help='Number of seconds to wait between runs of the image ' + 'cache manager'), cfg.IntOpt('reclaim_instance_interval', default=0, help='Interval in seconds for reclaiming deleted instances'), @@ -155,9 +155,9 @@ running_deleted_opts = [ "Valid options are 'noop', 'log' and 'reap'. " "Set to 'noop' to disable."), cfg.IntOpt("running_deleted_instance_poll_interval", - default=30, - help="Number of periodic scheduler ticks to wait between " - "runs of the cleanup task."), + default=1800, + help="Number of seconds to wait between runs of the cleanup " + "task."), cfg.IntOpt("running_deleted_instance_timeout", default=0, help="Number of seconds after being deleted when a running " @@ -417,6 +417,9 @@ class ComputeManager(manager.SchedulerDependentManager): """Initialization for a standalone compute service.""" self.driver.init_host(host=self.host) context = nova.context.get_admin_context() + + # NOTE(danms): this requires some care since conductor + # may not be up and fielding requests by the time compute is instances = self.db.instance_get_all_by_host(context, self.host) if CONF.defer_iptables_apply: @@ -551,7 +554,7 @@ class ComputeManager(manager.SchedulerDependentManager): if volume['status'] != 'creating': break greenthread.sleep(1) - self.db.block_device_mapping_update( + self.conductor_api.block_device_mapping_update( context, bdm['id'], {'volume_id': vol['id']}) bdm['volume_id'] = vol['id'] @@ -562,7 +565,7 @@ class ComputeManager(manager.SchedulerDependentManager): instance, volume, bdm['device_name']) - self.db.block_device_mapping_update( + self.conductor_api.block_device_mapping_update( context, bdm['id'], {'connection_info': jsonutils.dumps(cinfo)}) bdmap = {'connection_info': cinfo, @@ -602,8 +605,8 @@ class ComputeManager(manager.SchedulerDependentManager): context, instance, "create.start", extra_usage_info=extra_usage_info) network_info = None - bdms = self.db.block_device_mapping_get_all_by_instance( - context, instance['uuid']) + bdms = self.conductor_api.block_device_mapping_get_all_by_instance( + context, instance) rt = self._get_resource_tracker(node) try: limits = filter_properties.get('limits', {}) @@ -732,7 +735,8 @@ class ComputeManager(manager.SchedulerDependentManager): return filters = {'vm_state': vm_states.BUILDING} - building_insts = self.db.instance_get_all_by_filters(context, filters) + building_insts = self.conductor_api.instance_get_all_by_filters( + context, filters) for instance in building_insts: if timeutils.is_older_than(instance['created_at'], timeout): @@ -917,13 +921,13 @@ class ComputeManager(manager.SchedulerDependentManager): return [bdm for bdm in bdms if bdm['volume_id']] # NOTE(danms): Legacy interface for digging up volumes in the database - def _get_instance_volume_bdms(self, context, instance_uuid): + def _get_instance_volume_bdms(self, context, instance): return self._get_volume_bdms( - self.db.block_device_mapping_get_all_by_instance(context, - instance_uuid)) + self.conductor_api.block_device_mapping_get_all_by_instance( + context, instance)) - def _get_instance_volume_bdm(self, context, instance_uuid, volume_id): - bdms = self._get_instance_volume_bdms(context, instance_uuid) + def _get_instance_volume_bdm(self, context, instance, volume_id): + bdms = self._get_instance_volume_bdms(context, instance) for bdm in bdms: # NOTE(vish): Comparing as strings because the os_api doesn't # convert to integer and we may wish to support uuids @@ -933,10 +937,10 @@ class ComputeManager(manager.SchedulerDependentManager): # NOTE(danms): This is a transitional interface until all the callers # can provide their own bdms - def _get_instance_volume_block_device_info(self, context, instance_uuid, + def _get_instance_volume_block_device_info(self, context, instance, bdms=None): if bdms is None: - bdms = self._get_instance_volume_bdms(context, instance_uuid) + bdms = self._get_instance_volume_bdms(context, instance) return self._get_volume_block_device_info(bdms) def _get_volume_block_device_info(self, bdms): @@ -998,7 +1002,7 @@ class ComputeManager(manager.SchedulerDependentManager): # NOTE(vish) get bdms before destroying the instance vol_bdms = self._get_volume_bdms(bdms) block_device_info = self._get_instance_volume_block_device_info( - context, instance['uuid'], bdms=bdms) + context, instance, bdms=bdms) self.driver.destroy(instance, self._legacy_nw_info(network_info), block_device_info) for bdm in vol_bdms: @@ -1033,7 +1037,7 @@ class ComputeManager(manager.SchedulerDependentManager): def _delete_instance(self, context, instance, bdms): """Delete an instance on this host.""" instance_uuid = instance['uuid'] - self.db.instance_info_cache_delete(context, instance_uuid) + self.conductor_api.instance_info_cache_delete(context, instance) self._notify_about_instance_usage(context, instance, "delete.start") self._shutdown_instance(context, instance, bdms) # NOTE(vish): We have already deleted the instance, so we have @@ -1056,13 +1060,12 @@ class ComputeManager(manager.SchedulerDependentManager): vm_state=vm_states.DELETED, task_state=None, terminated_at=timeutils.utcnow()) - self.db.instance_destroy(context, instance_uuid) - system_meta = self.db.instance_system_metadata_get(context, - instance_uuid) + system_meta = compute_utils.metadata_to_dict( + instance['system_metadata']) + self.conductor_api.instance_destroy(context, instance) # ensure block device mappings are not leaked - for bdm in bdms: - self.db.block_device_mapping_destroy(context, bdm['id']) + self.conductor_api.block_device_mapping_destroy(context, bdms) self._notify_about_instance_usage(context, instance, "delete.end", system_metadata=system_meta) @@ -1080,7 +1083,7 @@ class ComputeManager(manager.SchedulerDependentManager): elevated = context.elevated() # NOTE(danms): remove this compatibility in the future if not bdms: - bdms = self._get_instance_volume_bdms(context, instance["uuid"]) + bdms = self._get_instance_volume_bdms(context, instance) @lockutils.synchronized(instance['uuid'], 'nova-') def do_terminate_instance(instance, bdms): @@ -1263,8 +1266,9 @@ class ComputeManager(manager.SchedulerDependentManager): network_info = self.network_api.get_instance_nw_info(context, instance) if bdms is None: - bdms = self.db.block_device_mapping_get_all_by_instance( - context, instance['uuid']) + capi = self.conductor_api + bdms = capi.block_device_mapping_get_all_by_instance( + context, instance) device_info = self._setup_block_device_mapping(context, instance, bdms) @@ -1313,7 +1317,7 @@ class ComputeManager(manager.SchedulerDependentManager): # is no longer needed if block_device_info is None: block_device_info = self._get_instance_volume_block_device_info( - context, instance['uuid']) + context, instance) # NOTE(danms): remove this when RPC API < 2.5 compatibility # is no longer needed if network_info is None: @@ -1391,16 +1395,21 @@ class ComputeManager(manager.SchedulerDependentManager): self._notify_about_instance_usage( context, instance, "snapshot.start") - self.driver.snapshot(context, instance, image_id) - if image_type == 'snapshot': expected_task_state = task_states.IMAGE_SNAPSHOT elif image_type == 'backup': expected_task_state = task_states.IMAGE_BACKUP + def update_task_state(task_state, expected_state=expected_task_state): + self._instance_update(context, instance['uuid'], + task_state=task_state, + expected_task_state=expected_state) + + self.driver.snapshot(context, instance, image_id, update_task_state) + self._instance_update(context, instance['uuid'], task_state=None, - expected_task_state=expected_task_state) + expected_task_state=task_states.IMAGE_UPLOADING) if image_type == 'snapshot' and rotation: raise exception.ImageRotationNotAllowed() @@ -1545,8 +1554,8 @@ class ComputeManager(manager.SchedulerDependentManager): def _get_rescue_image_ref(self, context, instance): """Determine what image should be used to boot the rescue VM. """ - system_meta = self.db.instance_system_metadata_get( - context, instance['uuid']) + system_meta = compute_utils.metadata_to_dict( + instance['system_metadata']) rescue_image_ref = system_meta.get('image_base_image_ref') @@ -1692,7 +1701,7 @@ class ComputeManager(manager.SchedulerDependentManager): network_info = self._get_instance_nw_info(context, instance) block_device_info = self._get_instance_volume_block_device_info( - context, instance['uuid']) + context, instance) self.driver.destroy(instance, self._legacy_nw_info(network_info), block_device_info) @@ -1742,9 +1751,9 @@ class ComputeManager(manager.SchedulerDependentManager): self.network_api.setup_networks_on_host(context, instance, migration['source_compute']) - bdms = self._get_instance_volume_bdms(context, instance['uuid']) + bdms = self._get_instance_volume_bdms(context, instance) block_device_info = self._get_instance_volume_block_device_info( - context, instance['uuid']) + context, instance) if bdms: connector = self.driver.get_volume_connector(instance) for bdm in bdms: @@ -1906,7 +1915,7 @@ class ComputeManager(manager.SchedulerDependentManager): with self._error_out_instance_on_exception(context, instance['uuid'], reservations): if not instance_type: - instance_type = self.db.instance_type_get(context, + instance_type = self.conductor_api.instance_type_get(context, migration['new_instance_type_id']) network_info = self._get_instance_nw_info(context, instance) @@ -1922,7 +1931,7 @@ class ComputeManager(manager.SchedulerDependentManager): context, instance, "resize.start", network_info=network_info) block_device_info = self._get_instance_volume_block_device_info( - context, instance['uuid']) + context, instance) disk_info = self.driver.migrate_disk_and_power_off( context, instance, migration['dest_host'], @@ -1952,7 +1961,7 @@ class ComputeManager(manager.SchedulerDependentManager): network_info=network_info) def _terminate_volume_connections(self, context, instance): - bdms = self._get_instance_volume_bdms(context, instance['uuid']) + bdms = self._get_instance_volume_bdms(context, instance) if bdms: connector = self.driver.get_volume_connector(instance) for bdm in bdms: @@ -1995,9 +2004,9 @@ class ComputeManager(manager.SchedulerDependentManager): context, instance, "finish_resize.start", network_info=network_info) - bdms = self._get_instance_volume_bdms(context, instance['uuid']) + bdms = self._get_instance_volume_bdms(context, instance) block_device_info = self._get_instance_volume_block_device_info( - context, instance['uuid'], bdms=bdms) + context, instance, bdms=bdms) if bdms: connector = self.driver.get_volume_connector(instance) @@ -2012,6 +2021,9 @@ class ComputeManager(manager.SchedulerDependentManager): image, resize_instance, block_device_info) + migration = self.conductor_api.migration_update(context, + migration, 'finished') + instance = self._instance_update(context, instance['uuid'], vm_state=vm_states.RESIZED, @@ -2020,9 +2032,6 @@ class ComputeManager(manager.SchedulerDependentManager): expected_task_state=task_states. RESIZE_FINISH) - migration = self.conductor_api.migration_update(context, - migration, 'finished') - self._notify_about_instance_usage( context, instance, "finish_resize.end", network_info=network_info) @@ -2194,7 +2203,7 @@ class ComputeManager(manager.SchedulerDependentManager): network_info = self._get_instance_nw_info(context, instance) block_device_info = self._get_instance_volume_block_device_info( - context, instance['uuid']) + context, instance) self.driver.resume(instance, self._legacy_nw_info(network_info), block_device_info) @@ -2314,7 +2323,7 @@ class ComputeManager(manager.SchedulerDependentManager): values = {'instance_uuid': instance['uuid'], 'volume_id': volume_id or 'reserved', 'device_name': result} - self.db.block_device_mapping_create(context, values) + self.conductor_api.block_device_mapping_create(context, values) return result return do_reserve() @@ -2328,8 +2337,9 @@ class ComputeManager(manager.SchedulerDependentManager): mountpoint, instance) except Exception: with excutils.save_and_reraise_exception(): - self.db.block_device_mapping_destroy_by_instance_and_device( - context, instance.get('uuid'), mountpoint) + capi = self.conductor_api + capi.block_device_mapping_destroy_by_instance_and_device( + context, instance, mountpoint) def _attach_volume(self, context, volume_id, mountpoint, instance): volume = self.volume_api.get(context, volume_id) @@ -2380,7 +2390,8 @@ class ComputeManager(manager.SchedulerDependentManager): 'volume_id': volume_id, 'volume_size': None, 'no_device': None} - self.db.block_device_mapping_update_or_create(context, values) + self.conductor_api.block_device_mapping_update_or_create(context, + values) def _detach_volume(self, context, instance, bdm): """Do the actual driver detach using block device mapping.""" @@ -2415,8 +2426,7 @@ class ComputeManager(manager.SchedulerDependentManager): @wrap_instance_fault def detach_volume(self, context, volume_id, instance): """Detach a volume from an instance.""" - bdm = self._get_instance_volume_bdm(context, instance['uuid'], - volume_id) + bdm = self._get_instance_volume_bdm(context, instance, volume_id) if CONF.volume_usage_poll_interval > 0: vol_stats = [] mp = bdm['device_name'] @@ -2431,17 +2441,19 @@ class ComputeManager(manager.SchedulerDependentManager): if vol_stats: LOG.debug(_("Updating volume usage cache with totals")) rd_req, rd_bytes, wr_req, wr_bytes, flush_ops = vol_stats - self.db.vol_usage_update(context, volume_id, rd_req, rd_bytes, - wr_req, wr_bytes, instance['id'], - update_totals=True) + self.conductor_api.vol_usage_update(context, volume_id, + rd_req, rd_bytes, + wr_req, wr_bytes, + instance, + update_totals=True) self._detach_volume(context, instance, bdm) volume = self.volume_api.get(context, volume_id) connector = self.driver.get_volume_connector(instance) self.volume_api.terminate_connection(context, volume, connector) self.volume_api.detach(context.elevated(), volume) - self.db.block_device_mapping_destroy_by_instance_and_volume( - context, instance['uuid'], volume_id) + self.conductor_api.block_device_mapping_destroy_by_instance_and_volume( + context, instance, volume_id) @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def remove_volume_connection(self, context, volume_id, instance): @@ -2450,9 +2462,7 @@ class ComputeManager(manager.SchedulerDependentManager): # detached, or delete the bdm, just remove the # connection from this host. try: - bdm = self._get_instance_volume_bdm(context, - instance['uuid'], - volume_id) + bdm = self._get_instance_volume_bdm(context, instance, volume_id) self._detach_volume(context, instance, bdm) volume = self.volume_api.get(context, volume_id) connector = self.driver.get_volume_connector(instance) @@ -2461,8 +2471,8 @@ class ComputeManager(manager.SchedulerDependentManager): pass def _get_compute_info(self, context, host): - compute_node_ref = self.db.service_get_all_compute_by_host(context, - host) + compute_node_ref = self.conductor_api.service_get_all_compute_by_host( + context, host) try: return compute_node_ref[0]['compute_node'][0] except IndexError: @@ -2537,14 +2547,14 @@ class ComputeManager(manager.SchedulerDependentManager): """ # If any volume is mounted, prepare here. block_device_info = self._get_instance_volume_block_device_info( - context, instance['uuid']) + context, instance) if not block_device_info['block_device_mapping']: LOG.info(_('Instance has no volume.'), instance=instance) # assign the volume to host system # needed by the lefthand volume driver and maybe others connector = self.driver.get_volume_connector(instance) - for bdm in self._get_instance_volume_bdms(context, instance['uuid']): + for bdm in self._get_instance_volume_bdms(context, instance): volume = self.volume_api.get(context, bdm['volume_id']) self.volume_api.initialize_connection(context, volume, connector) @@ -2633,7 +2643,7 @@ class ComputeManager(manager.SchedulerDependentManager): # Detaching volumes. connector = self.driver.get_volume_connector(instance_ref) - for bdm in self._get_instance_volume_bdms(ctxt, instance_ref['uuid']): + for bdm in self._get_instance_volume_bdms(ctxt, instance_ref): # NOTE(vish): We don't want to actually mark the volume # detached, or delete the bdm, just remove the # connection from this host. @@ -2753,8 +2763,7 @@ class ComputeManager(manager.SchedulerDependentManager): self.network_api.setup_networks_on_host(context, instance_ref, self.host) - for bdm in self._get_instance_volume_bdms(context, - instance_ref['uuid']): + for bdm in self._get_instance_volume_bdms(context, instance_ref): volume_id = bdm['volume_id'] volume = self.volume_api.get(context, volume_id) self.compute_rpcapi.remove_volume_connection(context, instance_ref, @@ -2790,7 +2799,7 @@ class ComputeManager(manager.SchedulerDependentManager): # NOTE(vish): The mapping is passed in so the driver can disconnect # from remote volumes if necessary block_device_info = self._get_instance_volume_block_device_info( - context, instance['uuid']) + context, instance) self.driver.destroy(instance, self._legacy_nw_info(network_info), block_device_info) @@ -2827,7 +2836,7 @@ class ComputeManager(manager.SchedulerDependentManager): continue else: # No more in our copy of uuids. Pull from the DB. - db_instances = self.db.instance_get_all_by_host( + db_instances = self.conductor_api.instance_get_all_by_host( context, self.host) if not db_instances: # None.. just return. @@ -2850,7 +2859,7 @@ class ComputeManager(manager.SchedulerDependentManager): @manager.periodic_task def _poll_rebooting_instances(self, context): if CONF.reboot_timeout > 0: - instances = self.db.instance_get_all_hung_in_rebooting( + instances = self.conductor_api.instance_get_all_hung_in_rebooting( context, CONF.reboot_timeout) self.driver.poll_rebooting_instances(CONF.reboot_timeout, instances) @@ -2878,7 +2887,8 @@ class ComputeManager(manager.SchedulerDependentManager): @manager.periodic_task def _poll_unconfirmed_resizes(self, context): if CONF.resize_confirm_window > 0: - migrations = self.db.migration_get_unconfirmed_by_dest_compute( + capi = self.conductor_api + migrations = capi.migration_get_unconfirmed_by_dest_compute( context, CONF.resize_confirm_window, self.host) migrations_info = dict(migration_count=len(migrations), @@ -2935,11 +2945,9 @@ class ComputeManager(manager.SchedulerDependentManager): if CONF.instance_usage_audit: if not compute_utils.has_audit_been_run(context, self.host): begin, end = utils.last_completed_audit_period() - instances = self.db.instance_get_active_by_window_joined( - context, - begin, - end, - host=self.host) + capi = self.conductor_api + instances = capi.instance_get_active_by_window_joined( + context, begin, end, host=self.host) num_instances = len(instances) errors = 0 successes = 0 @@ -2986,7 +2994,8 @@ class ComputeManager(manager.SchedulerDependentManager): self._last_bw_usage_poll = curr_time LOG.info(_("Updating bandwidth usage cache")) - instances = self.db.instance_get_all_by_host(context, self.host) + instances = self.conductor_api.instance_get_all_by_host(context, + self.host) try: bw_counters = self.driver.get_all_bw_counters(instances) except NotImplementedError: @@ -3047,10 +3056,10 @@ class ComputeManager(manager.SchedulerDependentManager): def _get_host_volume_bdms(self, context, host): """Return all block device mappings on a compute host""" compute_host_bdms = [] - instances = self.db.instance_get_all_by_host(context, self.host) + instances = self.conductor_api.instance_get_all_by_host(context, + self.host) for instance in instances: - instance_bdms = self._get_instance_volume_bdms(context, - instance['uuid']) + instance_bdms = self._get_instance_volume_bdms(context, instance) compute_host_bdms.append(dict(instance=instance, instance_bdms=instance_bdms)) @@ -3061,10 +3070,13 @@ class ComputeManager(manager.SchedulerDependentManager): for usage in vol_usages: # Allow switching of greenthreads between queries. greenthread.sleep(0) - self.db.vol_usage_update(context, usage['volume'], usage['rd_req'], - usage['rd_bytes'], usage['wr_req'], - usage['wr_bytes'], usage['instance_id'], - last_refreshed=refreshed) + self.conductor_api.vol_usage_update(context, usage['volume'], + usage['rd_req'], + usage['rd_bytes'], + usage['wr_req'], + usage['wr_bytes'], + usage['instance'], + last_refreshed=refreshed) def _send_volume_usage_notifications(self, context, start_time): """Queries vol usage cache table and sends a vol usage notification""" @@ -3072,7 +3084,8 @@ class ComputeManager(manager.SchedulerDependentManager): # the last run of get_all_volume_usage and this one # but detach stats will be recorded in db and returned from # vol_get_usage_by_time - vol_usages = self.db.vol_get_usage_by_time(context, start_time) + vol_usages = self.conductor_api.vol_get_usage_by_time(context, + start_time) for vol_usage in vol_usages: notifier.notify(context, 'volume.%s' % self.host, 'volume.usage', notifier.INFO, @@ -3124,7 +3137,7 @@ class ComputeManager(manager.SchedulerDependentManager): capability['host_ip'] = CONF.my_ip self.update_service_capabilities(capabilities) - @manager.periodic_task(ticks_between_runs=10) + @manager.periodic_task(spacing=600.0) def _sync_power_states(self, context): """Align power states between the database and the hypervisor. @@ -3137,7 +3150,8 @@ class ComputeManager(manager.SchedulerDependentManager): If the instance is not found on the hypervisor, but is in the database, then a stop() API will be called on the instance. """ - db_instances = self.db.instance_get_all_by_host(context, self.host) + db_instances = self.conductor_api.instance_get_all_by_host(context, + self.host) num_vm_instances = self.driver.get_num_instances() num_db_instances = len(db_instances) @@ -3267,7 +3281,8 @@ class ComputeManager(manager.SchedulerDependentManager): LOG.debug(_("CONF.reclaim_instance_interval <= 0, skipping...")) return - instances = self.db.instance_get_all_by_host(context, self.host) + instances = self.conductor_api.instance_get_all_by_host(context, + self.host) for instance in instances: old_enough = (not instance['deleted_at'] or timeutils.is_older_than(instance['deleted_at'], @@ -3275,8 +3290,9 @@ class ComputeManager(manager.SchedulerDependentManager): soft_deleted = instance['vm_state'] == vm_states.SOFT_DELETED if soft_deleted and old_enough: - bdms = self.db.block_device_mapping_get_all_by_instance( - context, instance['uuid']) + capi = self.conductor_api + bdms = capi.block_device_mapping_get_all_by_instance( + context, instance) LOG.info(_('Reclaiming deleted instance'), instance=instance) self._delete_instance(context, instance, bdms) @@ -3297,8 +3313,7 @@ class ComputeManager(manager.SchedulerDependentManager): new_resource_tracker_dict[nodename] = rt self._resource_tracker_dict = new_resource_tracker_dict - @manager.periodic_task( - ticks_between_runs=CONF.running_deleted_instance_poll_interval) + @manager.periodic_task(spacing=CONF.running_deleted_instance_poll_interval) def _cleanup_running_deleted_instances(self, context): """Cleanup any instances which are erroneously still running after having been deleted. @@ -3327,8 +3342,9 @@ class ComputeManager(manager.SchedulerDependentManager): # NOTE(sirp): admin contexts don't ordinarily return deleted records with utils.temporary_mutation(context, read_deleted="yes"): for instance in self._running_deleted_instances(context): - bdms = self.db.block_device_mapping_get_all_by_instance( - context, instance['uuid']) + capi = self.conductor_api + bdms = capi.block_device_mapping_get_all_by_instance( + context, instance) if action == "log": name = instance['name'] @@ -3367,7 +3383,8 @@ class ComputeManager(manager.SchedulerDependentManager): return True return False present_name_labels = set(self.driver.list_instances()) - instances = self.db.instance_get_all_by_host(context, self.host) + instances = self.conductor_api.instance_get_all_by_host(context, + self.host) return [i for i in instances if deleted_instance(i)] @contextlib.contextmanager @@ -3418,8 +3435,8 @@ class ComputeManager(manager.SchedulerDependentManager): aggregate, host, isinstance(e, exception.AggregateError)) - @manager.periodic_task( - ticks_between_runs=CONF.image_cache_manager_interval) + @manager.periodic_task(spacing=CONF.image_cache_manager_interval, + external_process_ok=True) def _run_image_cache_manager_pass(self, context): """Run a single pass of the image cache manager.""" @@ -3428,7 +3445,7 @@ class ComputeManager(manager.SchedulerDependentManager): if CONF.image_cache_manager_interval == 0: return - all_instances = self.db.instance_get_all(context) + all_instances = self.conductor_api.instance_get_all(context) # Determine what other nodes use this storage storage_users.register_storage_use(CONF.instances_path, CONF.host) diff --git a/nova/compute/resource_tracker.py b/nova/compute/resource_tracker.py index 1c653b8ca..c784fd83d 100644 --- a/nova/compute/resource_tracker.py +++ b/nova/compute/resource_tracker.py @@ -503,7 +503,10 @@ class ResourceTracker(object): resources['running_vms'] = 0 for instance in instances: - self._update_usage_from_instance(resources, instance) + if instance['vm_state'] == vm_states.DELETED: + continue + else: + self._update_usage_from_instance(resources, instance) def _find_orphaned_instances(self): """Given the set of instances and migrations already account for diff --git a/nova/compute/task_states.py b/nova/compute/task_states.py index c2966d554..8e2b8344a 100644 --- a/nova/compute/task_states.py +++ b/nova/compute/task_states.py @@ -33,6 +33,8 @@ SPAWNING = 'spawning' # possible task states during snapshot() IMAGE_SNAPSHOT = 'image_snapshot' +IMAGE_PENDING_UPLOAD = 'image_pending_upload' +IMAGE_UPLOADING = 'image_uploading' # possible task states during backup() IMAGE_BACKUP = 'image_backup' diff --git a/nova/compute/utils.py b/nova/compute/utils.py index a0dfbea8d..8852cb820 100644 --- a/nova/compute/utils.py +++ b/nova/compute/utils.py @@ -37,6 +37,13 @@ CONF.import_opt('host', 'nova.config') LOG = log.getLogger(__name__) +def metadata_to_dict(metadata): + result = {} + for item in metadata: + result[item['key']] = item['value'] + return result + + def add_instance_fault_from_exc(context, instance_uuid, fault, exc_info=None): """Adds the specified fault to the database.""" @@ -153,11 +160,7 @@ def notify_usage_exists(context, instance_ref, current_period=False, ignore_missing_network_data) if system_metadata is None: - try: - system_metadata = db.instance_system_metadata_get( - context, instance_ref['uuid']) - except exception.NotFound: - system_metadata = {} + system_metadata = metadata_to_dict(instance_ref['system_metadata']) # add image metadata to the notification: image_meta = notifications.image_meta(system_metadata) diff --git a/nova/conductor/api.py b/nova/conductor/api.py index d811265ac..66badb756 100644 --- a/nova/conductor/api.py +++ b/nova/conductor/api.py @@ -68,6 +68,9 @@ class LocalAPI(object): # other/future users of this sort of functionality. self._manager = ExceptionHelper(manager.ConductorManager()) + def ping(self, context, arg, timeout=None): + return self._manager.ping(context, arg) + def instance_update(self, context, instance_uuid, **updates): """Perform an instance update in the database""" return self._manager.instance_update(context, instance_uuid, updates) @@ -75,12 +78,47 @@ class LocalAPI(object): def instance_get_by_uuid(self, context, instance_uuid): return self._manager.instance_get_by_uuid(context, instance_uuid) + def instance_destroy(self, context, instance): + return self._manager.instance_destroy(context, instance) + + def instance_get_all(self, context): + return self.instance_get_all_by_filters(context, {}) + def instance_get_all_by_host(self, context, host): - return self._manager.instance_get_all_by_host(context, host) + return self.instance_get_all_by_filters(context, {'host': host}) + + def instance_get_all_by_filters(self, context, filters, + sort_key='created_at', + sort_dir='desc'): + return self._manager.instance_get_all_by_filters(context, + filters, + sort_key, + sort_dir) + + def instance_get_all_hung_in_rebooting(self, context, timeout): + return self._manager.instance_get_all_hung_in_rebooting(context, + timeout) + + def instance_get_active_by_window(self, context, begin, end=None, + project_id=None, host=None): + return self._manager.instance_get_active_by_window( + context, begin, end, project_id, host) + + def instance_info_cache_delete(self, context, instance): + return self._manager.instance_info_cache_delete(context, instance) + + def instance_type_get(self, context, instance_type_id): + return self._manager.instance_type_get(context, instance_type_id) def migration_get(self, context, migration_id): return self._manager.migration_get(context, migration_id) + def migration_get_unconfirmed_by_dest_compute(self, context, + confirm_window, + dest_compute): + return self._manager.migration_get_unconfirmed_by_dest_compute( + context, confirm_window, dest_compute) + def migration_update(self, context, migration, status): return self._manager.migration_update(context, migration, status) @@ -135,6 +173,67 @@ class LocalAPI(object): return self._manager.agent_build_get_by_triple(context, hypervisor, os, architecture) + def block_device_mapping_create(self, context, values): + return self._manager.block_device_mapping_update_or_create(context, + values, + create=True) + + def block_device_mapping_update(self, context, bdm_id, values): + values = dict(values) + values['id'] = bdm_id + return self._manager.block_device_mapping_update_or_create( + context, values, create=False) + + def block_device_mapping_update_or_create(self, context, values): + return self._manager.block_device_mapping_update_or_create(context, + values) + + def block_device_mapping_get_all_by_instance(self, context, instance): + return self._manager.block_device_mapping_get_all_by_instance( + context, instance) + + def block_device_mapping_destroy(self, context, bdms): + return self._manager.block_device_mapping_destroy(context, bdms=bdms) + + def block_device_mapping_destroy_by_instance_and_device(self, context, + instance, + device_name): + return self._manager.block_device_mapping_destroy( + context, instance=instance, device_name=device_name) + + def block_device_mapping_destroy_by_instance_and_volume(self, context, + instance, + volume_id): + return self._manager.block_device_mapping_destroy( + context, instance=instance, volume_id=volume_id) + + def vol_get_usage_by_time(self, context, start_time): + return self._manager.vol_get_usage_by_time(context, start_time) + + def vol_usage_update(self, context, vol_id, rd_req, rd_bytes, wr_req, + wr_bytes, instance, last_refreshed=None, + update_totals=False): + return self._manager.vol_usage_update(context, vol_id, + rd_req, rd_bytes, + wr_req, wr_bytes, + instance, last_refreshed, + update_totals) + + def service_get_all(self, context): + return self._manager.service_get_all_by(context) + + def service_get_all_by_topic(self, context, topic): + return self._manager.service_get_all_by(context, topic=topic) + + def service_get_all_by_host(self, context, host): + return self._manager.service_get_all_by(context, host=host) + + def service_get_by_host_and_topic(self, context, host, topic): + return self._manager.service_get_all_by(context, topic, host) + + def service_get_all_compute_by_host(self, context, host): + return self._manager.service_get_all_by(context, 'compute', host) + class API(object): """Conductor API that does updates via RPC to the ConductorManager""" @@ -142,21 +241,62 @@ class API(object): def __init__(self): self.conductor_rpcapi = rpcapi.ConductorAPI() + def ping(self, context, arg, timeout=None): + return self.conductor_rpcapi.ping(context, arg, timeout) + def instance_update(self, context, instance_uuid, **updates): """Perform an instance update in the database""" return self.conductor_rpcapi.instance_update(context, instance_uuid, updates) + def instance_destroy(self, context, instance): + return self.conductor_rpcapi.instance_destroy(context, instance) + def instance_get_by_uuid(self, context, instance_uuid): return self.conductor_rpcapi.instance_get_by_uuid(context, instance_uuid) + def instance_get_all(self, context): + return self.instance_get_all_by_filters(context, {}) + def instance_get_all_by_host(self, context, host): - return self.conductor_rpcapi.instance_get_all_by_host(context, host) + return self.instance_get_all_by_filters(context, {'host': host}) + + def instance_get_all_by_filters(self, context, filters, + sort_key='created_at', + sort_dir='desc'): + return self.conductor_rpcapi.instance_get_all_by_filters(context, + filters, + sort_key, + sort_dir) + + def instance_get_all_hung_in_rebooting(self, context, timeout): + return self.conductor_rpcapi.instance_get_all_hung_in_rebooting( + context, timeout) + + def instance_get_active_by_window(self, context, begin, end=None, + project_id=None, host=None): + return self.conductor_rpcapi.instance_get_active_by_window( + context, begin, end, project_id, host) + + def instance_info_cache_delete(self, context, instance): + return self.conductor_rpcapi.instance_info_cache_delete(context, + instance) + + def instance_type_get(self, context, instance_type_id): + return self.conductor_rpcapi.instance_type_get(context, + instance_type_id) def migration_get(self, context, migration_id): return self.conductor_rpcapi.migration_get(context, migration_id) + def migration_get_unconfirmed_by_dest_compute(self, context, + confirm_window, + dest_compute): + crpcapi = self.conductor_rpcapi + return crpcapi.migration_get_unconfirmed_by_dest_compute( + context, confirm_window, dest_compute) + def migration_update(self, context, migration, status): return self.conductor_rpcapi.migration_update(context, migration, status) @@ -220,3 +360,65 @@ class API(object): hypervisor, os, architecture) + + def block_device_mapping_create(self, context, values): + return self.conductor_rpcapi.block_device_mapping_update_or_create( + context, values, create=True) + + def block_device_mapping_update(self, context, bdm_id, values): + values = dict(values) + values['id'] = bdm_id + return self.conductor_rpcapi.block_device_mapping_update_or_create( + context, values, create=False) + + def block_device_mapping_update_or_create(self, context, values): + return self.conductor_rpcapi.block_device_mapping_update_or_create( + context, values) + + def block_device_mapping_get_all_by_instance(self, context, instance): + return self.conductor_rpcapi.block_device_mapping_get_all_by_instance( + context, instance) + + def block_device_mapping_destroy(self, context, bdms): + return self.conductor_rpcapi.block_device_mapping_destroy(context, + bdms=bdms) + + def block_device_mapping_destroy_by_instance_and_device(self, context, + instance, + device_name): + return self.conductor_rpcapi.block_device_mapping_destroy( + context, instance=instance, device_name=device_name) + + def block_device_mapping_destroy_by_instance_and_volume(self, context, + instance, + volume_id): + return self.conductor_rpcapi.block_device_mapping_destroy( + context, instance=instance, volume_id=volume_id) + + def vol_get_usage_by_time(self, context, start_time): + return self.conductor_rpcapi.vol_get_usage_by_time(context, start_time) + + def vol_usage_update(self, context, vol_id, rd_req, rd_bytes, wr_req, + wr_bytes, instance, last_refreshed=None, + update_totals=False): + return self.conductor_rpcapi.vol_usage_update(context, vol_id, + rd_req, rd_bytes, + wr_req, wr_bytes, + instance, last_refreshed, + update_totals) + + def service_get_all(self, context): + return self.conductor_rpcapi.service_get_all_by(context) + + def service_get_all_by_topic(self, context, topic): + return self.conductor_rpcapi.service_get_all_by(context, topic=topic) + + def service_get_all_by_host(self, context, host): + return self.conductor_rpcapi.service_get_all_by(context, host=host) + + def service_get_by_host_and_topic(self, context, host, topic): + return self.conductor_rpcapi.service_get_all_by(context, topic, host) + + def service_get_all_compute_by_host(self, context, host): + return self.conductor_rpcapi.service_get_all_by(context, 'compute', + host) diff --git a/nova/conductor/manager.py b/nova/conductor/manager.py index 92770cc84..123e7e13f 100644 --- a/nova/conductor/manager.py +++ b/nova/conductor/manager.py @@ -43,12 +43,15 @@ datetime_fields = ['launched_at', 'terminated_at'] class ConductorManager(manager.SchedulerDependentManager): """Mission: TBD""" - RPC_API_VERSION = '1.11' + RPC_API_VERSION = '1.22' def __init__(self, *args, **kwargs): super(ConductorManager, self).__init__(service_name='conductor', *args, **kwargs) + def ping(self, context, arg): + return jsonutils.to_primitive({'service': 'conductor', 'arg': arg}) + @rpc_common.client_exceptions(KeyError, ValueError, exception.InvalidUUID, exception.InstanceNotFound, @@ -72,6 +75,7 @@ class ConductorManager(manager.SchedulerDependentManager): return jsonutils.to_primitive( self.db.instance_get_by_uuid(context, instance_uuid)) + # NOTE(danms): This should go away in RPC version 2 def instance_get_all_by_host(self, context, host): return jsonutils.to_primitive( self.db.instance_get_all_by_host(context.elevated(), host)) @@ -82,6 +86,13 @@ class ConductorManager(manager.SchedulerDependentManager): migration_id) return jsonutils.to_primitive(migration_ref) + def migration_get_unconfirmed_by_dest_compute(self, context, + confirm_window, + dest_compute): + migrations = self.db.migration_get_unconfirmed_by_dest_compute( + context, confirm_window, dest_compute) + return jsonutils.to_primitive(migrations) + @rpc_common.client_exceptions(exception.MigrationNotFound) def migration_update(self, context, migration, status): migration_ref = self.db.migration_update(context.elevated(), @@ -155,3 +166,90 @@ class ConductorManager(manager.SchedulerDependentManager): info = self.db.agent_build_get_by_triple(context, hypervisor, os, architecture) return jsonutils.to_primitive(info) + + def block_device_mapping_update_or_create(self, context, values, + create=None): + if create is None: + self.db.block_device_mapping_update_or_create(context, values) + elif create is True: + self.db.block_device_mapping_create(context, values) + else: + self.db.block_device_mapping_update(context, values['id'], values) + + def block_device_mapping_get_all_by_instance(self, context, instance): + bdms = self.db.block_device_mapping_get_all_by_instance( + context, instance['uuid']) + return jsonutils.to_primitive(bdms) + + def block_device_mapping_destroy(self, context, bdms=None, + instance=None, volume_id=None, + device_name=None): + if bdms is not None: + for bdm in bdms: + self.db.block_device_mapping_destroy(context, bdm['id']) + elif instance is not None and volume_id is not None: + self.db.block_device_mapping_destroy_by_instance_and_volume( + context, instance['uuid'], volume_id) + elif instance is not None and device_name is not None: + self.db.block_device_mapping_destroy_by_instance_and_device( + context, instance['uuid'], device_name) + else: + # NOTE(danms): This shouldn't happen + raise exception.Invalid(_("Invalid block_device_mapping_destroy" + " invocation")) + + def instance_get_all_by_filters(self, context, filters, sort_key, + sort_dir): + result = self.db.instance_get_all_by_filters(context, filters, + sort_key, sort_dir) + return jsonutils.to_primitive(result) + + def instance_get_all_hung_in_rebooting(self, context, timeout): + result = self.db.instance_get_all_hung_in_rebooting(context, timeout) + return jsonutils.to_primitive(result) + + def instance_get_active_by_window(self, context, begin, end=None, + project_id=None, host=None): + result = self.db.instance_get_active_by_window_joined(context, + begin, end, + project_id, + host) + return jsonutils.to_primitive(result) + + def instance_destroy(self, context, instance): + self.db.instance_destroy(context, instance['uuid']) + + def instance_info_cache_delete(self, context, instance): + self.db.instance_info_cache_delete(context, instance['uuid']) + + def instance_type_get(self, context, instance_type_id): + result = self.db.instance_type_get(context, instance_type_id) + return jsonutils.to_primitive(result) + + def vol_get_usage_by_time(self, context, start_time): + result = self.db.vol_get_usage_by_time(context, start_time) + return jsonutils.to_primitive(result) + + def vol_usage_update(self, context, vol_id, rd_req, rd_bytes, wr_req, + wr_bytes, instance, last_refreshed=None, + update_totals=False): + self.db.vol_usage_update(context, vol_id, rd_req, rd_bytes, wr_req, + wr_bytes, instance['uuid'], last_refreshed, + update_totals) + + def service_get_all_by(self, context, topic=None, host=None): + if not any((topic, host)): + result = self.db.service_get_all(context) + elif all((topic, host)): + if topic == 'compute': + result = self.db.service_get_all_compute_by_host(context, + host) + else: + result = self.db.service_get_by_host_and_topic(context, + host, topic) + elif topic: + result = self.db.service_get_all_by_topic(context, topic) + elif host: + result = self.db.service_get_all_by_host(context, host) + + return jsonutils.to_primitive(result) diff --git a/nova/conductor/rpcapi.py b/nova/conductor/rpcapi.py index e7484d91f..0f2fe1f0c 100644 --- a/nova/conductor/rpcapi.py +++ b/nova/conductor/rpcapi.py @@ -40,6 +40,20 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): 1.9 - Added provider_fw_rule_get_all 1.10 - Added agent_build_get_by_triple 1.11 - Added aggregate_get + 1.12 - Added block_device_mapping_update_or_create + 1.13 - Added block_device_mapping_get_all_by_instance + 1.14 - Added block_device_mapping_destroy + 1.15 - Added instance_get_all_by_filters and + instance_get_all_hung_in_rebooting and + instance_get_active_by_window + Deprecated instance_get_all_by_host + 1.16 - Added instance_destroy + 1.17 - Added instance_info_cache_delete + 1.18 - Added instance_type_get + 1.19 - Added vol_get_usage_by_time and vol_usage_update + 1.20 - Added migration_get_unconfirmed_by_dest_compute + 1.21 - Added service_get_all_by + 1.22 - Added ping """ BASE_RPC_API_VERSION = '1.0' @@ -49,6 +63,11 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): topic=CONF.conductor.topic, default_version=self.BASE_RPC_API_VERSION) + def ping(self, context, arg, timeout=None): + arg_p = jsonutils.to_primitive(arg) + msg = self.make_msg('ping', arg=arg_p) + return self.call(context, msg, version='1.22', timeout=timeout) + def instance_update(self, context, instance_uuid, updates): updates_p = jsonutils.to_primitive(updates) return self.call(context, @@ -61,14 +80,18 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): instance_uuid=instance_uuid) return self.call(context, msg, version='1.2') - def instance_get_all_by_host(self, context, host): - msg = self.make_msg('instance_get_all_by_host', host=host) - return self.call(context, msg, version='1.2') - def migration_get(self, context, migration_id): msg = self.make_msg('migration_get', migration_id=migration_id) return self.call(context, msg, version='1.4') + def migration_get_unconfirmed_by_dest_compute(self, context, + confirm_window, + dest_compute): + msg = self.make_msg('migration_get_unconfirmed_by_dest_compute', + confirm_window=confirm_window, + dest_compute=dest_compute) + return self.call(context, msg, version='1.20') + def migration_update(self, context, migration, status): migration_p = jsonutils.to_primitive(migration) msg = self.make_msg('migration_update', migration=migration_p, @@ -145,3 +168,80 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): hypervisor=hypervisor, os=os, architecture=architecture) return self.call(context, msg, version='1.10') + + def block_device_mapping_update_or_create(self, context, values, + create=None): + msg = self.make_msg('block_device_mapping_update_or_create', + values=values, create=create) + return self.call(context, msg, version='1.12') + + def block_device_mapping_get_all_by_instance(self, context, instance): + instance_p = jsonutils.to_primitive(instance) + msg = self.make_msg('block_device_mapping_get_all_by_instance', + instance=instance_p) + return self.call(context, msg, version='1.13') + + def block_device_mapping_destroy(self, context, bdms=None, + instance=None, volume_id=None, + device_name=None): + bdms_p = jsonutils.to_primitive(bdms) + instance_p = jsonutils.to_primitive(instance) + msg = self.make_msg('block_device_mapping_destroy', + bdms=bdms_p, + instance=instance_p, volume_id=volume_id, + device_name=device_name) + return self.call(context, msg, version='1.14') + + def instance_get_all_by_filters(self, context, filters, sort_key, + sort_dir): + msg = self.make_msg('instance_get_all_by_filters', + filters=filters, sort_key=sort_key, + sort_dir=sort_dir) + return self.call(context, msg, version='1.15') + + def instance_get_all_hung_in_rebooting(self, context, timeout): + msg = self.make_msg('instance_get_all_hung_in_rebooting', + timeout=timeout) + return self.call(context, msg, version='1.15') + + def instance_get_active_by_window(self, context, begin, end=None, + project_id=None, host=None): + msg = self.make_msg('instance_get_active_by_window', + begin=begin, end=end, project_id=project_id, + host=host) + return self.call(context, msg, version='1.15') + + def instance_destroy(self, context, instance): + instance_p = jsonutils.to_primitive(instance) + msg = self.make_msg('instance_destroy', instance=instance_p) + self.call(context, msg, version='1.16') + + def instance_info_cache_delete(self, context, instance): + instance_p = jsonutils.to_primitive(instance) + msg = self.make_msg('instance_info_cache_delete', instance=instance_p) + self.call(context, msg, version='1.17') + + def instance_type_get(self, context, instance_type_id): + msg = self.make_msg('instance_type_get', + instance_type_id=instance_type_id) + return self.call(context, msg, version='1.18') + + def vol_get_usage_by_time(self, context, start_time): + start_time_p = jsonutils.to_primitive(start_time) + msg = self.make_msg('vol_get_usage_by_time', start_time=start_time_p) + return self.call(context, msg, version='1.19') + + def vol_usage_update(self, context, vol_id, rd_req, rd_bytes, wr_req, + wr_bytes, instance, last_refreshed=None, + update_totals=False): + instance_p = jsonutils.to_primitive(instance) + msg = self.make_msg('vol_usage_update', vol_id=vol_id, rd_req=rd_req, + rd_bytes=rd_bytes, wr_req=wr_req, + wr_bytes=wr_bytes, + instance=instance_p, last_refreshed=last_refreshed, + update_totals=update_totals) + return self.call(context, msg, version='1.19') + + def service_get_all_by(self, context, topic=None, host=None): + msg = self.make_msg('service_get_all_by', topic=topic, host=host) + return self.call(context, msg, version='1.21') diff --git a/nova/config.py b/nova/config.py index 4f4fbe822..2cc153203 100644 --- a/nova/config.py +++ b/nova/config.py @@ -21,6 +21,7 @@ import os import socket from nova.openstack.common import cfg +from nova.openstack.common import rpc def _get_my_ip(): @@ -55,14 +56,7 @@ core_opts = [ help="Top-level directory for maintaining nova's state"), ] -debug_opts = [ - cfg.BoolOpt('fake_network', - default=False, - help='If passed, use fake network devices and addresses'), -] - cfg.CONF.register_cli_opts(core_opts) -cfg.CONF.register_cli_opts(debug_opts) global_opts = [ cfg.StrOpt('my_ip', @@ -176,15 +170,13 @@ global_opts = [ cfg.StrOpt('volume_api_class', default='nova.volume.cinder.API', help='The full class name of the volume API class to use'), - cfg.StrOpt('control_exchange', - default='nova', - help='AMQP exchange to connect to if using RabbitMQ or Qpid'), ] cfg.CONF.register_opts(global_opts) def parse_args(argv, default_config_files=None): + rpc.set_defaults(control_exchange='nova') cfg.CONF(argv[1:], project='nova', default_config_files=default_config_files) diff --git a/nova/console/api.py b/nova/console/api.py index 7eb7a31e2..cad1999be 100644 --- a/nova/console/api.py +++ b/nova/console/api.py @@ -68,3 +68,8 @@ class API(base.Base): else: instance = self.db.instance_get(context, instance_uuid) return instance + + def get_backdoor_port(self, context, host): + topic = self._get_console_topic(context, host) + rpcapi = console_rpcapi.ConsoleAPI(topic=topic) + return rpcapi.get_backdoor_port(context, host) diff --git a/nova/console/manager.py b/nova/console/manager.py index 6bae3b45c..243c028d9 100644 --- a/nova/console/manager.py +++ b/nova/console/manager.py @@ -52,7 +52,7 @@ class ConsoleProxyManager(manager.Manager): """ - RPC_API_VERSION = '1.0' + RPC_API_VERSION = '1.1' def __init__(self, console_driver=None, *args, **kwargs): if not console_driver: @@ -132,3 +132,6 @@ class ConsoleProxyManager(manager.Manager): pool_info['compute_host'] = instance_host pool = self.db.console_pool_create(context, pool_info) return pool + + def get_backdoor_port(self, context): + return self.backdoor_port diff --git a/nova/console/rpcapi.py b/nova/console/rpcapi.py index 8c31bb97f..15a3b46ec 100644 --- a/nova/console/rpcapi.py +++ b/nova/console/rpcapi.py @@ -31,6 +31,7 @@ class ConsoleAPI(nova.openstack.common.rpc.proxy.RpcProxy): API version history: 1.0 - Initial version. + 1.1 - Added get_backdoor_port() ''' # @@ -54,3 +55,7 @@ class ConsoleAPI(nova.openstack.common.rpc.proxy.RpcProxy): def remove_console(self, ctxt, console_id): self.cast(ctxt, self.make_msg('remove_console', console_id=console_id)) + + def get_backdoor_port(self, ctxt, host): + return self.call(ctxt, self.make_msg('get_backdoor_port'), + version='1.1') diff --git a/nova/consoleauth/manager.py b/nova/consoleauth/manager.py index 23d0a06f6..8d2171de7 100644 --- a/nova/consoleauth/manager.py +++ b/nova/consoleauth/manager.py @@ -45,7 +45,7 @@ CONF.import_opt('memcached_servers', 'nova.config') class ConsoleAuthManager(manager.Manager): """Manages token based authentication.""" - RPC_API_VERSION = '1.0' + RPC_API_VERSION = '1.1' def __init__(self, scheduler_driver=None, *args, **kwargs): super(ConsoleAuthManager, self).__init__(*args, **kwargs) @@ -75,3 +75,6 @@ class ConsoleAuthManager(manager.Manager): LOG.audit(_("Checking Token: %(token)s, %(token_valid)s)"), locals()) if token_valid: return jsonutils.loads(token_str) + + def get_backdoor_port(self, context): + return self.backdoor_port diff --git a/nova/consoleauth/rpcapi.py b/nova/consoleauth/rpcapi.py index f4b4390ac..64b915ec3 100644 --- a/nova/consoleauth/rpcapi.py +++ b/nova/consoleauth/rpcapi.py @@ -30,6 +30,7 @@ class ConsoleAuthAPI(nova.openstack.common.rpc.proxy.RpcProxy): API version history: 1.0 - Initial version. + 1.1 - Added get_backdoor_port() ''' # @@ -59,3 +60,7 @@ class ConsoleAuthAPI(nova.openstack.common.rpc.proxy.RpcProxy): def check_token(self, ctxt, token): return self.call(ctxt, self.make_msg('check_token', token=token)) + + def get_backdoor_port(self, ctxt, host): + return self.call(ctxt, self.make_msg('get_backdoor_port'), + version='1.1') diff --git a/nova/db/api.py b/nova/db/api.py index 4acff8a99..1322c29e9 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -43,8 +43,10 @@ these objects be simple dictionaries. """ +from nova.cells import rpcapi as cells_rpcapi from nova import exception from nova.openstack.common import cfg +from nova.openstack.common import log as logging from nova import utils @@ -68,6 +70,7 @@ CONF.register_opts(db_opts) IMPL = utils.LazyPluggable('db_backend', sqlalchemy='nova.db.sqlalchemy.api') +LOG = logging.getLogger(__name__) class NoMoreNetworks(exception.NovaException): @@ -290,7 +293,8 @@ def floating_ip_destroy(context, address): def floating_ip_disassociate(context, address): """Disassociate a floating ip from a fixed ip by address. - :returns: the address of the existing fixed ip. + :returns: the address of the previous fixed ip or None + if the ip was not associated to an ip. """ return IMPL.floating_ip_disassociate(context, address) @@ -298,7 +302,12 @@ def floating_ip_disassociate(context, address): def floating_ip_fixed_ip_associate(context, floating_address, fixed_address, host): - """Associate a floating ip to a fixed_ip by address.""" + """Associate a floating ip to a fixed_ip by address. + + :returns: the address of the new fixed ip (fixed_address) or None + if the ip was already associated to the fixed ip. + """ + return IMPL.floating_ip_fixed_ip_associate(context, floating_address, fixed_address, @@ -560,9 +569,16 @@ def instance_data_get_for_project(context, project_id, session=None): session=session) -def instance_destroy(context, instance_uuid, constraint=None): +def instance_destroy(context, instance_uuid, constraint=None, + update_cells=True): """Destroy the instance or raise if it does not exist.""" - return IMPL.instance_destroy(context, instance_uuid, constraint) + rv = IMPL.instance_destroy(context, instance_uuid, constraint) + if update_cells: + try: + cells_rpcapi.CellsAPI().instance_destroy_at_top(context, rv) + except Exception: + LOG.exception(_("Failed to notify cells of instance destroy")) + return rv def instance_get_by_uuid(context, uuid): @@ -659,13 +675,19 @@ def instance_test_and_set(context, instance_uuid, attr, ok_states, ok_states, new_state) -def instance_update(context, instance_uuid, values): +def instance_update(context, instance_uuid, values, update_cells=True): """Set the given properties on an instance and update it. Raises NotFound if instance does not exist. """ - return IMPL.instance_update(context, instance_uuid, values) + rv = IMPL.instance_update(context, instance_uuid, values) + if update_cells: + try: + cells_rpcapi.CellsAPI().instance_update_at_top(context, rv) + except Exception: + LOG.exception(_("Failed to notify cells of instance update")) + return rv def instance_update_and_get_original(context, instance_uuid, values): @@ -681,8 +703,12 @@ def instance_update_and_get_original(context, instance_uuid, values): Raises NotFound if instance does not exist. """ - return IMPL.instance_update_and_get_original(context, instance_uuid, - values) + rv = IMPL.instance_update_and_get_original(context, instance_uuid, values) + try: + cells_rpcapi.CellsAPI().instance_update_at_top(context, rv[1]) + except Exception: + LOG.exception(_("Failed to notify cells of instance update")) + return rv def instance_add_security_group(context, instance_id, security_group_id): @@ -708,13 +734,21 @@ def instance_info_cache_get(context, instance_uuid): return IMPL.instance_info_cache_get(context, instance_uuid) -def instance_info_cache_update(context, instance_uuid, values): +def instance_info_cache_update(context, instance_uuid, values, + update_cells=True): """Update an instance info cache record in the table. :param instance_uuid: = uuid of info cache's instance :param values: = dict containing column values to update """ - return IMPL.instance_info_cache_update(context, instance_uuid, values) + rv = IMPL.instance_info_cache_update(context, instance_uuid, values) + try: + cells_rpcapi.CellsAPI().instance_info_cache_update_at_top(context, + rv) + except Exception: + LOG.exception(_("Failed to notify cells of instance info cache " + "update")) + return rv def instance_info_cache_delete(context, instance_uuid): @@ -1348,7 +1382,7 @@ def instance_metadata_delete(context, instance_uuid, key): def instance_metadata_update(context, instance_uuid, metadata, delete): """Update metadata if it exists, otherwise create it.""" return IMPL.instance_metadata_update(context, instance_uuid, - metadata, delete) + metadata, delete) #################### @@ -1408,12 +1442,21 @@ def bw_usage_get_by_uuids(context, uuids, start_period): def bw_usage_update(context, uuid, mac, start_period, bw_in, bw_out, - last_ctr_in, last_ctr_out, last_refreshed=None): + last_ctr_in, last_ctr_out, last_refreshed=None, + update_cells=True): """Update cached bandwidth usage for an instance's network based on mac address. Creates new record if needed. """ - return IMPL.bw_usage_update(context, uuid, mac, start_period, bw_in, + rv = IMPL.bw_usage_update(context, uuid, mac, start_period, bw_in, bw_out, last_ctr_in, last_ctr_out, last_refreshed=last_refreshed) + if update_cells: + try: + cells_rpcapi.CellsAPI().bw_usage_update_at_top(context, + uuid, mac, start_period, bw_in, bw_out, + last_ctr_in, last_ctr_out, last_refreshed) + except Exception: + LOG.exception(_("Failed to notify cells of bw_usage update")) + return rv #################### @@ -1549,9 +1592,15 @@ def aggregate_host_delete(context, aggregate_id, host): #################### -def instance_fault_create(context, values): +def instance_fault_create(context, values, update_cells=True): """Create a new Instance Fault.""" - return IMPL.instance_fault_create(context, values) + rv = IMPL.instance_fault_create(context, values) + if update_cells: + try: + cells_rpcapi.CellsAPI().instance_fault_create_at_top(context, rv) + except Exception: + LOG.exception(_("Failed to notify cells of instance fault")) + return rv def instance_fault_get_by_instance_uuids(context, instance_uuids): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 095f76126..dce92ba54 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -32,7 +32,6 @@ from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload_all from sqlalchemy.sql.expression import asc from sqlalchemy.sql.expression import desc -from sqlalchemy.sql.expression import literal_column from sqlalchemy.sql import func from nova import block_device @@ -762,9 +761,12 @@ def floating_ip_fixed_ip_associate(context, floating_address, fixed_ip_ref = fixed_ip_get_by_address(context, fixed_address, session=session) + if floating_ip_ref.fixed_ip_id == fixed_ip_ref["id"]: + return None floating_ip_ref.fixed_ip_id = fixed_ip_ref["id"] floating_ip_ref.host = host floating_ip_ref.save(session=session) + return fixed_address @require_context @@ -3247,10 +3249,12 @@ def security_group_rule_create(context, values): def security_group_rule_destroy(context, security_group_rule_id): session = get_session() with session.begin(): - security_group_rule = security_group_rule_get(context, - security_group_rule_id, - session=session) - security_group_rule.delete(session=session) + count = _security_group_rule_get_query(context, session=session).\ + filter_by(id=security_group_rule_id).\ + soft_delete() + if count == 0: + raise exception.SecurityGroupNotFoundForRule( + rule_id=security_group_rule_id) @require_context @@ -3796,43 +3800,42 @@ def instance_metadata_get_item(context, instance_uuid, key, session=None): @require_context def instance_metadata_update(context, instance_uuid, metadata, delete, session=None): + all_keys = metadata.keys() + synchronize_session = "fetch" if session is None: session = get_session() - # Set existing metadata to deleted if delete argument is True - if delete: - original_metadata = instance_metadata_get(context, instance_uuid, - session=session) - for meta_key, meta_value in original_metadata.iteritems(): - if meta_key not in metadata: - meta_ref = instance_metadata_get_item(context, instance_uuid, - meta_key, session) - meta_ref.update({'deleted': True}) - meta_ref.save(session=session) - - meta_ref = None - - # Now update all existing items with new values, or create new meta objects - for meta_key, meta_value in metadata.iteritems(): + synchronize_session = False + with session.begin(subtransactions=True): + if delete: + _instance_metadata_get_query(context, instance_uuid, + session=session).\ + filter(~models.InstanceMetadata.key.in_(all_keys)).\ + soft_delete(synchronize_session=synchronize_session) + + already_existing_keys = [] + meta_refs = _instance_metadata_get_query(context, instance_uuid, + session=session).\ + filter(models.InstanceMetadata.key.in_(all_keys)).\ + all() - # update the value whether it exists or not - item = {"value": meta_value} + for meta_ref in meta_refs: + already_existing_keys.append(meta_ref.key) + meta_ref.update({"value": metadata[meta_ref.key]}) - try: - meta_ref = instance_metadata_get_item(context, instance_uuid, - meta_key, session) - except exception.InstanceMetadataNotFound: + new_keys = set(all_keys) - set(already_existing_keys) + for key in new_keys: meta_ref = models.InstanceMetadata() - item.update({"key": meta_key, "instance_uuid": instance_uuid}) - - meta_ref.update(item) - meta_ref.save(session=session) + meta_ref.update({"key": key, "value": metadata[key], + "instance_uuid": instance_uuid}) + session.add(meta_ref) - return metadata + return metadata ####################### # System-owned metadata + def _instance_system_metadata_get_query(context, instance_uuid, session=None): return model_query(context, models.InstanceSystemMetadata, session=session).\ @@ -3868,39 +3871,36 @@ def _instance_system_metadata_get_item(context, instance_uuid, key, @require_context def instance_system_metadata_update(context, instance_uuid, metadata, delete, session=None): + all_keys = metadata.keys() + synchronize_session = "fetch" if session is None: session = get_session() + synchronize_session = False + with session.begin(subtransactions=True): + if delete: + _instance_system_metadata_get_query(context, instance_uuid, + session=session).\ + filter(~models.InstanceSystemMetadata.key.in_(all_keys)).\ + soft_delete(synchronize_session=synchronize_session) + + already_existing_keys = [] + meta_refs = _instance_system_metadata_get_query(context, instance_uuid, + session=session).\ + filter(models.InstanceSystemMetadata.key.in_(all_keys)).\ + all() - # Set existing metadata to deleted if delete argument is True - if delete: - original_metadata = instance_system_metadata_get( - context, instance_uuid, session=session) - for meta_key, meta_value in original_metadata.iteritems(): - if meta_key not in metadata: - meta_ref = _instance_system_metadata_get_item( - context, instance_uuid, meta_key, session) - meta_ref.update({'deleted': True}) - meta_ref.save(session=session) - - meta_ref = None - - # Now update all existing items with new values, or create new meta objects - for meta_key, meta_value in metadata.iteritems(): - - # update the value whether it exists or not - item = {"value": meta_value} + for meta_ref in meta_refs: + already_existing_keys.append(meta_ref.key) + meta_ref.update({"value": metadata[meta_ref.key]}) - try: - meta_ref = _instance_system_metadata_get_item( - context, instance_uuid, meta_key, session) - except exception.InstanceSystemMetadataNotFound: + new_keys = set(all_keys) - set(already_existing_keys) + for key in new_keys: meta_ref = models.InstanceSystemMetadata() - item.update({"key": meta_key, "instance_uuid": instance_uuid}) + meta_ref.update({"key": key, "value": metadata[key], + "instance_uuid": instance_uuid}) + session.add(meta_ref) - meta_ref.update(item) - meta_ref.save(session=session) - - return metadata + return metadata #################### @@ -4343,6 +4343,16 @@ def aggregate_get_all(context): @require_admin_context +def aggregate_metadata_get_query(context, aggregate_id, session=None, + read_deleted="yes"): + return model_query(context, + models.AggregateMetadata, + read_deleted=read_deleted, + session=session).\ + filter_by(aggregate_id=aggregate_id) + + +@require_admin_context @require_aggregate_exists def aggregate_metadata_get(context, aggregate_id): rows = model_query(context, @@ -4387,33 +4397,31 @@ def aggregate_metadata_get_item(context, aggregate_id, key, session=None): @require_aggregate_exists def aggregate_metadata_add(context, aggregate_id, metadata, set_delete=False): session = get_session() + all_keys = metadata.keys() + with session.begin(): + query = aggregate_metadata_get_query(context, aggregate_id, + session=session) + if set_delete: + query.filter(~models.AggregateMetadata.key.in_(all_keys)).\ + soft_delete(synchronize_session=False) - if set_delete: - original_metadata = aggregate_metadata_get(context, aggregate_id) - for meta_key, meta_value in original_metadata.iteritems(): - if meta_key not in metadata: - meta_ref = aggregate_metadata_get_item(context, aggregate_id, - meta_key, session) - meta_ref.update({'deleted': True}) - meta_ref.save(session=session) - - meta_ref = None + query = query.filter(models.AggregateMetadata.key.in_(all_keys)) + already_existing_keys = [] + for meta_ref in query.all(): + key = meta_ref.key + meta_ref.update({"value": metadata[key], + "deleted": False, + "deleted_at": None}) + already_existing_keys.append(key) - for meta_key, meta_value in metadata.iteritems(): - item = {"value": meta_value} - try: - meta_ref = aggregate_metadata_get_item(context, aggregate_id, - meta_key, session) - if meta_ref.deleted: - item.update({'deleted': False, 'deleted_at': None}) - except exception.AggregateMetadataNotFound: + for key in set(all_keys) - set(already_existing_keys): meta_ref = models.AggregateMetadata() - item.update({"key": meta_key, "aggregate_id": aggregate_id}) + meta_ref.update({"key": key, + "value": metadata[key], + "aggregate_id": aggregate_id}) + session.add(meta_ref) - meta_ref.update(item) - meta_ref.save(session=session) - - return metadata + return metadata @require_admin_context diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py index b128ccc23..ae8fec32d 100644 --- a/nova/db/sqlalchemy/session.py +++ b/nova/db/sqlalchemy/session.py @@ -306,7 +306,6 @@ def get_session(autocommit=True, expire_on_commit=False): _MAKER = get_maker(engine, autocommit, expire_on_commit) session = _MAKER() - session = wrap_session(session) return session @@ -388,13 +387,6 @@ def wrap_db_error(f): return _wrap -def wrap_session(session): - """Return a session whose exceptions are wrapped.""" - session.query = wrap_db_error(session.query) - session.flush = wrap_db_error(session.flush) - return session - - def get_engine(): """Return a SQLAlchemy engine.""" global _ENGINE @@ -548,9 +540,21 @@ class Query(sqlalchemy.orm.query.Query): synchronize_session=synchronize_session) +class Session(sqlalchemy.orm.session.Session): + """Custom Session class to avoid SqlAlchemy Session monkey patching""" + @wrap_db_error + def query(self, *args, **kwargs): + return super(Session, self).query(*args, **kwargs) + + @wrap_db_error + def flush(self, *args, **kwargs): + return super(Session, self).flush(*args, **kwargs) + + def get_maker(engine, autocommit=True, expire_on_commit=False): """Return a SQLAlchemy sessionmaker using the given engine.""" return sqlalchemy.orm.sessionmaker(bind=engine, + class_=Session, autocommit=autocommit, expire_on_commit=expire_on_commit, query_cls=Query) diff --git a/nova/exception.py b/nova/exception.py index ee0a88a95..9507a0088 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -410,6 +410,10 @@ class InvalidUUID(Invalid): message = _("Expected a uuid but received %(uuid)s.") +class InvalidPeriodicTaskArg(Invalid): + message = _("Unexpected argument for periodic task creation: %(arg)s.") + + class ConstraintNotMet(NovaException): message = _("Constraint not met.") code = 412 @@ -769,6 +773,34 @@ class CellNotFound(NotFound): message = _("Cell %(cell_id)s could not be found.") +class CellRoutingInconsistency(NovaException): + message = _("Inconsistency in cell routing: %(reason)s") + + +class CellServiceAPIMethodNotFound(NotFound): + message = _("Service API method not found: %(detail)s") + + +class CellTimeout(NotFound): + message = _("Timeout waiting for response from cell") + + +class CellMaxHopCountReached(NovaException): + message = _("Cell message has reached maximum hop count: %(hop_count)s") + + +class NoCellsAvailable(NovaException): + message = _("No cells available matching scheduling criteria.") + + +class CellError(NovaException): + message = _("Exception received during cell processing: %(exc_name)s.") + + +class InstanceUnknownCell(NotFound): + message = _("Cell is not known for instance %(instance_uuid)s") + + class SchedulerHostFilterNotFound(NotFound): message = _("Scheduler Host Filter %(filter_name)s could not be found.") diff --git a/nova/locale/nova.pot b/nova/locale/nova.pot index 7ed43bf34..3fb397298 100644 --- a/nova/locale/nova.pot +++ b/nova/locale/nova.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: nova 2013.1\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2013-01-01 00:02+0000\n" +"POT-Creation-Date: 2013-01-06 00:03+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -145,8 +145,8 @@ msgstr "" msgid "Volume %(volume_id)s is not attached to anything" msgstr "" -#: nova/exception.py:233 nova/api/ec2/cloud.py:436 nova/api/ec2/cloud.py:461 -#: nova/api/openstack/compute/contrib/keypairs.py:98 nova/compute/api.py:2313 +#: nova/exception.py:233 nova/api/ec2/cloud.py:447 nova/api/ec2/cloud.py:472 +#: nova/api/openstack/compute/contrib/keypairs.py:98 nova/compute/api.py:2321 msgid "Keypair data is invalid" msgstr "" @@ -162,7 +162,7 @@ msgstr "" msgid "Invalid volume" msgstr "" -#: nova/exception.py:249 nova/api/openstack/compute/servers.py:1309 +#: nova/exception.py:249 nova/api/openstack/compute/servers.py:1307 #: nova/api/openstack/compute/contrib/admin_actions.py:242 msgid "Invalid metadata" msgstr "" @@ -176,7 +176,7 @@ msgstr "" msgid "Invalid port range %(from_port)s:%(to_port)s. %(msg)s" msgstr "" -#: nova/exception.py:261 nova/api/ec2/cloud.py:618 +#: nova/exception.py:261 nova/api/ec2/cloud.py:629 #, python-format msgid "Invalid IP protocol %(protocol)s." msgstr "" @@ -774,320 +774,353 @@ msgstr "" #: nova/exception.py:773 #, python-format -msgid "Scheduler Host Filter %(filter_name)s could not be found." +msgid "Inconsistency in cell routing: %(reason)s" msgstr "" #: nova/exception.py:777 #, python-format +msgid "Service API method not found: %(detail)s" +msgstr "" + +#: nova/exception.py:781 +msgid "Timeout waiting for response from cell" +msgstr "" + +#: nova/exception.py:785 +#, python-format +msgid "Cell message has reached maximum hop count: %(hop_count)s" +msgstr "" + +#: nova/exception.py:789 +msgid "No cells available matching scheduling criteria." +msgstr "" + +#: nova/exception.py:793 +#, python-format +msgid "Exception received during cell processing: %(exc_name)s." +msgstr "" + +#: nova/exception.py:797 +#, python-format +msgid "Cell is not known for instance %(instance_uuid)s" +msgstr "" + +#: nova/exception.py:801 +#, python-format +msgid "Scheduler Host Filter %(filter_name)s could not be found." +msgstr "" + +#: nova/exception.py:805 +#, python-format msgid "Scheduler cost function %(cost_fn_str)s could not be found." msgstr "" -#: nova/exception.py:782 +#: nova/exception.py:810 #, python-format msgid "Scheduler weight flag not found: %(flag_name)s" msgstr "" -#: nova/exception.py:786 +#: nova/exception.py:814 #, python-format msgid "Instance %(instance_uuid)s has no metadata with key %(metadata_key)s." msgstr "" -#: nova/exception.py:791 +#: nova/exception.py:819 #, python-format msgid "" "Instance %(instance_uuid)s has no system metadata with key " "%(metadata_key)s." msgstr "" -#: nova/exception.py:796 +#: nova/exception.py:824 #, python-format msgid "" "Instance Type %(instance_type_id)s has no extra specs with key " "%(extra_specs_key)s." msgstr "" -#: nova/exception.py:801 +#: nova/exception.py:829 #, python-format msgid "File %(file_path)s could not be found." msgstr "" -#: nova/exception.py:805 +#: nova/exception.py:833 msgid "Zero files could be found." msgstr "" -#: nova/exception.py:809 +#: nova/exception.py:837 #, python-format msgid "Virtual switch associated with the network adapter %(adapter)s not found." msgstr "" -#: nova/exception.py:814 +#: nova/exception.py:842 #, python-format msgid "Network adapter %(adapter)s could not be found." msgstr "" -#: nova/exception.py:818 +#: nova/exception.py:846 #, python-format msgid "Class %(class_name)s could not be found: %(exception)s" msgstr "" -#: nova/exception.py:822 +#: nova/exception.py:850 msgid "Action not allowed." msgstr "" -#: nova/exception.py:826 +#: nova/exception.py:854 msgid "Rotation is not allowed for snapshots" msgstr "" -#: nova/exception.py:830 +#: nova/exception.py:858 msgid "Rotation param is required for backup image_type" msgstr "" -#: nova/exception.py:834 +#: nova/exception.py:862 #, python-format msgid "Key pair %(key_name)s already exists." msgstr "" -#: nova/exception.py:838 +#: nova/exception.py:866 #, python-format msgid "Instance %(name)s already exists." msgstr "" -#: nova/exception.py:842 +#: nova/exception.py:870 #, python-format msgid "Instance Type with name %(name)s already exists." msgstr "" -#: nova/exception.py:846 +#: nova/exception.py:874 #, python-format msgid "Instance Type with ID %(flavor_id)s already exists." msgstr "" -#: nova/exception.py:850 +#: nova/exception.py:878 #, python-format msgid "" "Flavor access alreay exists for flavor %(flavor_id)s and project " "%(project_id)s combination." msgstr "" -#: nova/exception.py:855 +#: nova/exception.py:883 #, python-format msgid "%(path)s is not on shared storage: %(reason)s" msgstr "" -#: nova/exception.py:859 +#: nova/exception.py:887 #, python-format msgid "%(path)s is not on local storage: %(reason)s" msgstr "" -#: nova/exception.py:863 +#: nova/exception.py:891 msgid "Migration error" msgstr "" -#: nova/exception.py:867 +#: nova/exception.py:895 #, python-format msgid "Malformed message body: %(reason)s" msgstr "" -#: nova/exception.py:873 +#: nova/exception.py:901 #, python-format msgid "Could not find config at %(path)s" msgstr "" -#: nova/exception.py:877 +#: nova/exception.py:905 #, python-format msgid "Could not load paste app '%(name)s' from %(path)s" msgstr "" -#: nova/exception.py:881 +#: nova/exception.py:909 msgid "When resizing, instances must change flavor!" msgstr "" -#: nova/exception.py:885 +#: nova/exception.py:913 #, python-format msgid "Resize error: %(reason)s" msgstr "" -#: nova/exception.py:889 +#: nova/exception.py:917 msgid "Image is larger than instance type allows" msgstr "" -#: nova/exception.py:893 +#: nova/exception.py:921 msgid "Instance type's memory is too small for requested image." msgstr "" -#: nova/exception.py:897 +#: nova/exception.py:925 msgid "Instance type's disk is too small for requested image." msgstr "" -#: nova/exception.py:901 +#: nova/exception.py:929 #, python-format msgid "Insufficient free memory on compute node to start %(uuid)s." msgstr "" -#: nova/exception.py:905 +#: nova/exception.py:933 msgid "Could not fetch bandwidth/cpu/disk metrics for this host." msgstr "" -#: nova/exception.py:909 +#: nova/exception.py:937 #, python-format msgid "No valid host was found. %(reason)s" msgstr "" -#: nova/exception.py:913 +#: nova/exception.py:941 msgid "Quota exceeded" msgstr "" -#: nova/exception.py:920 +#: nova/exception.py:948 #, python-format msgid "" "Quota exceeded for %(overs)s: Requested %(req)s, but already used " "%(used)d of %(allowed)d %(resource)s" msgstr "" -#: nova/exception.py:925 +#: nova/exception.py:953 msgid "Maximum number of floating ips exceeded" msgstr "" -#: nova/exception.py:929 +#: nova/exception.py:957 #, python-format msgid "Maximum number of metadata items exceeds %(allowed)d" msgstr "" -#: nova/exception.py:933 +#: nova/exception.py:961 msgid "Personality file limit exceeded" msgstr "" -#: nova/exception.py:937 +#: nova/exception.py:965 msgid "Personality file path too long" msgstr "" -#: nova/exception.py:941 +#: nova/exception.py:969 msgid "Personality file content too long" msgstr "" -#: nova/exception.py:945 +#: nova/exception.py:973 msgid "Maximum number of key pairs exceeded" msgstr "" -#: nova/exception.py:949 +#: nova/exception.py:977 msgid "Maximum number of security groups or rules exceeded" msgstr "" -#: nova/exception.py:953 +#: nova/exception.py:981 #, python-format msgid "" "Aggregate %(aggregate_id)s: action '%(action)s' caused an error: " "%(reason)s." msgstr "" -#: nova/exception.py:958 +#: nova/exception.py:986 #, python-format msgid "Aggregate %(aggregate_id)s could not be found." msgstr "" -#: nova/exception.py:962 +#: nova/exception.py:990 #, python-format msgid "Aggregate %(aggregate_name)s already exists." msgstr "" -#: nova/exception.py:966 +#: nova/exception.py:994 #, python-format msgid "Aggregate %(aggregate_id)s has no host %(host)s." msgstr "" -#: nova/exception.py:970 +#: nova/exception.py:998 #, python-format msgid "Aggregate %(aggregate_id)s has no metadata with key %(metadata_key)s." msgstr "" -#: nova/exception.py:975 +#: nova/exception.py:1003 #, python-format msgid "Aggregate %(aggregate_id)s already has host %(host)s." msgstr "" -#: nova/exception.py:979 +#: nova/exception.py:1007 msgid "Unable to create instance type" msgstr "" -#: nova/exception.py:983 +#: nova/exception.py:1011 #, python-format msgid "Failed to set admin password on %(instance)s because %(reason)s" msgstr "" -#: nova/exception.py:989 +#: nova/exception.py:1017 #, python-format msgid "Detected existing vlan with id %(vlan)d" msgstr "" -#: nova/exception.py:993 +#: nova/exception.py:1021 #, python-format msgid "Instance %(instance_id)s could not be found." msgstr "" -#: nova/exception.py:997 +#: nova/exception.py:1025 #, python-format msgid "Marker %(marker)s could not be found." msgstr "" -#: nova/exception.py:1001 +#: nova/exception.py:1029 #, python-format msgid "Invalid id: %(val)s (expecting \"i-...\")." msgstr "" -#: nova/exception.py:1005 +#: nova/exception.py:1033 #, python-format msgid "Could not fetch image %(image_id)s" msgstr "" -#: nova/exception.py:1009 +#: nova/exception.py:1037 #, python-format msgid "Task %(task_name)s is already running on host %(host)s" msgstr "" -#: nova/exception.py:1013 +#: nova/exception.py:1041 #, python-format msgid "Task %(task_name)s is not running on host %(host)s" msgstr "" -#: nova/exception.py:1017 +#: nova/exception.py:1045 #, python-format msgid "Instance %(instance_uuid)s is locked" msgstr "" -#: nova/exception.py:1021 +#: nova/exception.py:1049 #, python-format msgid "Could not mount vfat config drive. %(operation)s failed. Error: %(error)s" msgstr "" -#: nova/exception.py:1026 +#: nova/exception.py:1054 #, python-format msgid "Unknown config drive format %(format)s. Select one of iso9660 or vfat." msgstr "" -#: nova/exception.py:1031 +#: nova/exception.py:1059 #, python-format msgid "" "User data too large. User data must be no larger than %(maxsize)s bytes " "once base64 encoded. Your data is %(length)d bytes" msgstr "" -#: nova/exception.py:1037 +#: nova/exception.py:1065 msgid "User data needs to be valid base 64." msgstr "" -#: nova/exception.py:1041 +#: nova/exception.py:1069 #, python-format msgid "" "unexpected task state: expecting %(expected)s but the actual state is " "%(actual)s" msgstr "" -#: nova/exception.py:1046 +#: nova/exception.py:1074 #, python-format msgid "The CA file for %(project)s could not be found" msgstr "" -#: nova/exception.py:1050 +#: nova/exception.py:1078 #, python-format msgid "The CRL file for %(project)s could not be found" msgstr "" @@ -1318,7 +1351,7 @@ msgstr "" msgid "Reloading cached file %s" msgstr "" -#: nova/utils.py:1113 nova/virt/configdrive.py:165 +#: nova/utils.py:1113 nova/virt/configdrive.py:177 #, python-format msgid "Could not remove tmpdir: %s" msgstr "" @@ -1353,7 +1386,7 @@ msgstr "" msgid "Sourcing roles from deprecated X-Role HTTP header" msgstr "" -#: nova/api/sizelimit.py:50 nova/api/metadata/password.py:37 +#: nova/api/sizelimit.py:50 nova/api/metadata/password.py:64 msgid "Request is too large." msgstr "" @@ -1499,241 +1532,241 @@ msgstr "" msgid "Unsupported API request: controller = %(controller)s, action = %(action)s" msgstr "" -#: nova/api/ec2/cloud.py:384 +#: nova/api/ec2/cloud.py:395 #, python-format msgid "Create snapshot of volume %s" msgstr "" -#: nova/api/ec2/cloud.py:410 +#: nova/api/ec2/cloud.py:421 #, python-format msgid "Could not find key pair(s): %s" msgstr "" -#: nova/api/ec2/cloud.py:426 +#: nova/api/ec2/cloud.py:437 #, python-format msgid "Create key pair %s" msgstr "" -#: nova/api/ec2/cloud.py:433 nova/api/ec2/cloud.py:458 +#: nova/api/ec2/cloud.py:444 nova/api/ec2/cloud.py:469 #: nova/api/openstack/compute/contrib/keypairs.py:93 msgid "Quota exceeded, too many key pairs." msgstr "" -#: nova/api/ec2/cloud.py:439 nova/api/ec2/cloud.py:464 +#: nova/api/ec2/cloud.py:450 nova/api/ec2/cloud.py:475 #: nova/api/openstack/compute/contrib/keypairs.py:101 #, python-format msgid "Key pair '%s' already exists." msgstr "" -#: nova/api/ec2/cloud.py:448 +#: nova/api/ec2/cloud.py:459 #, python-format msgid "Import key %s" msgstr "" -#: nova/api/ec2/cloud.py:471 +#: nova/api/ec2/cloud.py:482 #, python-format msgid "Delete key pair %s" msgstr "" -#: nova/api/ec2/cloud.py:605 nova/api/ec2/cloud.py:726 +#: nova/api/ec2/cloud.py:616 nova/api/ec2/cloud.py:737 msgid "Not enough parameters, need group_name or group_id" msgstr "" -#: nova/api/ec2/cloud.py:610 +#: nova/api/ec2/cloud.py:621 #, python-format msgid "%s Not enough parameters to build a valid rule" msgstr "" -#: nova/api/ec2/cloud.py:648 nova/api/ec2/cloud.py:680 +#: nova/api/ec2/cloud.py:659 nova/api/ec2/cloud.py:691 msgid "No rule for the specified parameters." msgstr "" -#: nova/api/ec2/cloud.py:671 +#: nova/api/ec2/cloud.py:682 #, python-format msgid "%s - This rule already exists in group" msgstr "" -#: nova/api/ec2/cloud.py:737 +#: nova/api/ec2/cloud.py:748 #, python-format msgid "Get console output for instance %s" msgstr "" -#: nova/api/ec2/cloud.py:813 +#: nova/api/ec2/cloud.py:824 #, python-format msgid "Create volume from snapshot %s" msgstr "" -#: nova/api/ec2/cloud.py:817 nova/api/openstack/compute/contrib/volumes.py:240 +#: nova/api/ec2/cloud.py:828 nova/api/openstack/compute/contrib/volumes.py:241 #, python-format msgid "Create volume of %s GB" msgstr "" -#: nova/api/ec2/cloud.py:845 +#: nova/api/ec2/cloud.py:856 msgid "Delete Failed" msgstr "" -#: nova/api/ec2/cloud.py:858 +#: nova/api/ec2/cloud.py:869 #, python-format msgid "Attach volume %(volume_id)s to instance %(instance_id)s at %(device)s" msgstr "" -#: nova/api/ec2/cloud.py:866 +#: nova/api/ec2/cloud.py:877 msgid "Attach Failed." msgstr "" -#: nova/api/ec2/cloud.py:879 nova/api/openstack/compute/contrib/volumes.py:419 +#: nova/api/ec2/cloud.py:890 nova/api/openstack/compute/contrib/volumes.py:428 #, python-format msgid "Detach volume %s" msgstr "" -#: nova/api/ec2/cloud.py:885 +#: nova/api/ec2/cloud.py:896 msgid "Detach Volume Failed." msgstr "" -#: nova/api/ec2/cloud.py:911 nova/api/ec2/cloud.py:968 -#: nova/api/ec2/cloud.py:1504 nova/api/ec2/cloud.py:1519 +#: nova/api/ec2/cloud.py:922 nova/api/ec2/cloud.py:979 +#: nova/api/ec2/cloud.py:1528 nova/api/ec2/cloud.py:1543 #, python-format msgid "attribute not supported: %s" msgstr "" -#: nova/api/ec2/cloud.py:1033 +#: nova/api/ec2/cloud.py:1049 #, python-format msgid "vol = %s\n" msgstr "" -#: nova/api/ec2/cloud.py:1184 +#: nova/api/ec2/cloud.py:1208 msgid "Allocate address" msgstr "" -#: nova/api/ec2/cloud.py:1188 +#: nova/api/ec2/cloud.py:1212 msgid "No more floating IPs available" msgstr "" -#: nova/api/ec2/cloud.py:1192 +#: nova/api/ec2/cloud.py:1216 #, python-format msgid "Release address %s" msgstr "" -#: nova/api/ec2/cloud.py:1197 +#: nova/api/ec2/cloud.py:1221 msgid "Unable to release IP Address." msgstr "" -#: nova/api/ec2/cloud.py:1200 +#: nova/api/ec2/cloud.py:1224 #, python-format msgid "Associate address %(public_ip)s to instance %(instance_id)s" msgstr "" -#: nova/api/ec2/cloud.py:1208 +#: nova/api/ec2/cloud.py:1232 msgid "Unable to associate IP Address, no fixed_ips." msgstr "" -#: nova/api/ec2/cloud.py:1216 +#: nova/api/ec2/cloud.py:1240 #: nova/api/openstack/compute/contrib/floating_ips.py:257 #, python-format msgid "multiple fixed_ips exist, using the first: %s" msgstr "" -#: nova/api/ec2/cloud.py:1225 +#: nova/api/ec2/cloud.py:1249 msgid "Floating ip is already associated." msgstr "" -#: nova/api/ec2/cloud.py:1228 +#: nova/api/ec2/cloud.py:1252 msgid "l3driver call to add floating ip failed." msgstr "" -#: nova/api/ec2/cloud.py:1231 +#: nova/api/ec2/cloud.py:1255 msgid "Error, unable to associate floating ip." msgstr "" -#: nova/api/ec2/cloud.py:1239 +#: nova/api/ec2/cloud.py:1263 #, python-format msgid "Disassociate address %s" msgstr "" -#: nova/api/ec2/cloud.py:1244 +#: nova/api/ec2/cloud.py:1268 msgid "Floating ip is not associated." msgstr "" -#: nova/api/ec2/cloud.py:1247 +#: nova/api/ec2/cloud.py:1271 #: nova/api/openstack/compute/contrib/floating_ips.py:100 msgid "Cannot disassociate auto assigned floating ip" msgstr "" -#: nova/api/ec2/cloud.py:1274 +#: nova/api/ec2/cloud.py:1298 msgid "Image must be available" msgstr "" -#: nova/api/ec2/cloud.py:1306 +#: nova/api/ec2/cloud.py:1330 msgid "Going to start terminating instances" msgstr "" -#: nova/api/ec2/cloud.py:1316 +#: nova/api/ec2/cloud.py:1340 #, python-format msgid "Reboot instance %r" msgstr "" -#: nova/api/ec2/cloud.py:1325 +#: nova/api/ec2/cloud.py:1349 msgid "Going to stop instances" msgstr "" -#: nova/api/ec2/cloud.py:1334 +#: nova/api/ec2/cloud.py:1358 msgid "Going to start instances" msgstr "" -#: nova/api/ec2/cloud.py:1425 +#: nova/api/ec2/cloud.py:1449 #, python-format msgid "De-registering image %s" msgstr "" -#: nova/api/ec2/cloud.py:1441 +#: nova/api/ec2/cloud.py:1465 msgid "imageLocation is required" msgstr "" -#: nova/api/ec2/cloud.py:1460 +#: nova/api/ec2/cloud.py:1484 #, python-format msgid "Registered image %(image_location)s with id %(image_id)s" msgstr "" -#: nova/api/ec2/cloud.py:1522 +#: nova/api/ec2/cloud.py:1546 msgid "user or group not specified" msgstr "" -#: nova/api/ec2/cloud.py:1524 +#: nova/api/ec2/cloud.py:1548 msgid "only group \"all\" is supported" msgstr "" -#: nova/api/ec2/cloud.py:1526 +#: nova/api/ec2/cloud.py:1550 msgid "operation_type must be add or remove" msgstr "" -#: nova/api/ec2/cloud.py:1528 +#: nova/api/ec2/cloud.py:1552 #, python-format msgid "Updating image %s publicity" msgstr "" -#: nova/api/ec2/cloud.py:1541 +#: nova/api/ec2/cloud.py:1565 #, python-format msgid "Not allowed to modify attributes for image %s" msgstr "" -#: nova/api/ec2/cloud.py:1570 +#: nova/api/ec2/cloud.py:1594 #, python-format msgid "" "Invalid value '%(ec2_instance_id)s' for instanceId. Instance does not " "have a volume attached at root (%(root)s)" msgstr "" -#: nova/api/ec2/cloud.py:1600 +#: nova/api/ec2/cloud.py:1624 #, python-format msgid "Couldn't stop instance with in %d sec" msgstr "" -#: nova/api/ec2/cloud.py:1618 +#: nova/api/ec2/cloud.py:1642 #, python-format msgid "image of %(instance)s at %(now)s" msgstr "" -#: nova/api/ec2/cloud.py:1651 +#: nova/api/ec2/cloud.py:1675 msgid "Invalid CIDR" msgstr "" @@ -1836,7 +1869,7 @@ msgstr "" msgid "offset param must be positive" msgstr "" -#: nova/api/openstack/common.py:230 nova/api/openstack/compute/servers.py:542 +#: nova/api/openstack/common.py:230 nova/api/openstack/compute/servers.py:536 #, python-format msgid "marker [%s] not found" msgstr "" @@ -2103,229 +2136,231 @@ msgstr "" msgid "Metadata item was not found" msgstr "" -#: nova/api/openstack/compute/servers.py:456 -#: nova/api/openstack/compute/servers.py:468 -#: nova/api/openstack/compute/servers.py:561 -#: nova/api/openstack/compute/servers.py:729 -#: nova/api/openstack/compute/servers.py:989 -#: nova/api/openstack/compute/servers.py:1092 -#: nova/api/openstack/compute/servers.py:1260 -msgid "Instance could not be found" -msgstr "" - -#: nova/api/openstack/compute/servers.py:507 +#: nova/api/openstack/compute/servers.py:501 msgid "Invalid changes-since value" msgstr "" -#: nova/api/openstack/compute/servers.py:526 +#: nova/api/openstack/compute/servers.py:520 msgid "Only administrators may list deleted instances" msgstr "" -#: nova/api/openstack/compute/servers.py:545 +#: nova/api/openstack/compute/servers.py:539 msgid "Flavor could not be found" msgstr "" -#: nova/api/openstack/compute/servers.py:568 +#: nova/api/openstack/compute/servers.py:555 +#: nova/api/openstack/compute/servers.py:723 +#: nova/api/openstack/compute/servers.py:987 +#: nova/api/openstack/compute/servers.py:1090 +#: nova/api/openstack/compute/servers.py:1258 +msgid "Instance could not be found" +msgstr "" + +#: nova/api/openstack/compute/servers.py:562 msgid "Server name is not a string or unicode" msgstr "" -#: nova/api/openstack/compute/servers.py:572 +#: nova/api/openstack/compute/servers.py:566 msgid "Server name is an empty string" msgstr "" -#: nova/api/openstack/compute/servers.py:576 +#: nova/api/openstack/compute/servers.py:570 msgid "Server name must be less than 256 characters." msgstr "" -#: nova/api/openstack/compute/servers.py:593 +#: nova/api/openstack/compute/servers.py:587 #, python-format msgid "Bad personality format: missing %s" msgstr "" -#: nova/api/openstack/compute/servers.py:596 +#: nova/api/openstack/compute/servers.py:590 msgid "Bad personality format" msgstr "" -#: nova/api/openstack/compute/servers.py:600 +#: nova/api/openstack/compute/servers.py:594 #, python-format msgid "Personality content for %s cannot be decoded" msgstr "" -#: nova/api/openstack/compute/servers.py:631 +#: nova/api/openstack/compute/servers.py:625 msgid "Unknown argment : port" msgstr "" -#: nova/api/openstack/compute/servers.py:634 +#: nova/api/openstack/compute/servers.py:628 #, python-format msgid "Bad port format: port uuid is not in proper format (%s)" msgstr "" -#: nova/api/openstack/compute/servers.py:644 +#: nova/api/openstack/compute/servers.py:638 #, python-format msgid "Bad networks format: network uuid is not in proper format (%s)" msgstr "" -#: nova/api/openstack/compute/servers.py:654 +#: nova/api/openstack/compute/servers.py:648 #, python-format msgid "Invalid fixed IP address (%s)" msgstr "" -#: nova/api/openstack/compute/servers.py:667 +#: nova/api/openstack/compute/servers.py:661 #, python-format msgid "Duplicate networks (%s) are not allowed" msgstr "" -#: nova/api/openstack/compute/servers.py:673 +#: nova/api/openstack/compute/servers.py:667 #, python-format msgid "Bad network format: missing %s" msgstr "" -#: nova/api/openstack/compute/servers.py:676 +#: nova/api/openstack/compute/servers.py:670 msgid "Bad networks format" msgstr "" -#: nova/api/openstack/compute/servers.py:702 +#: nova/api/openstack/compute/servers.py:696 msgid "Userdata content cannot be decoded" msgstr "" -#: nova/api/openstack/compute/servers.py:709 +#: nova/api/openstack/compute/servers.py:703 msgid "accessIPv4 is not proper IPv4 format" msgstr "" -#: nova/api/openstack/compute/servers.py:716 +#: nova/api/openstack/compute/servers.py:710 msgid "accessIPv6 is not proper IPv6 format" msgstr "" -#: nova/api/openstack/compute/servers.py:745 +#: nova/api/openstack/compute/servers.py:739 msgid "Server name is not defined" msgstr "" -#: nova/api/openstack/compute/servers.py:793 -#: nova/api/openstack/compute/servers.py:899 +#: nova/api/openstack/compute/servers.py:787 +#: nova/api/openstack/compute/servers.py:893 msgid "Invalid flavorRef provided." msgstr "" -#: nova/api/openstack/compute/servers.py:833 +#: nova/api/openstack/compute/servers.py:827 msgid "min_count must be an integer value" msgstr "" -#: nova/api/openstack/compute/servers.py:836 +#: nova/api/openstack/compute/servers.py:830 msgid "min_count must be > 0" msgstr "" -#: nova/api/openstack/compute/servers.py:841 +#: nova/api/openstack/compute/servers.py:835 msgid "max_count must be an integer value" msgstr "" -#: nova/api/openstack/compute/servers.py:844 +#: nova/api/openstack/compute/servers.py:838 msgid "max_count must be > 0" msgstr "" -#: nova/api/openstack/compute/servers.py:847 +#: nova/api/openstack/compute/servers.py:841 msgid "min_count must be <= max_count" msgstr "" -#: nova/api/openstack/compute/servers.py:896 +#: nova/api/openstack/compute/servers.py:890 msgid "Can not find requested image" msgstr "" -#: nova/api/openstack/compute/servers.py:902 +#: nova/api/openstack/compute/servers.py:896 msgid "Invalid key_name provided." msgstr "" -#: nova/api/openstack/compute/servers.py:981 +#: nova/api/openstack/compute/servers.py:975 msgid "HostId cannot be updated." msgstr "" -#: nova/api/openstack/compute/servers.py:1007 -#: nova/api/openstack/compute/servers.py:1027 +#: nova/api/openstack/compute/servers.py:979 +msgid "Personality cannot be updated." +msgstr "" + +#: nova/api/openstack/compute/servers.py:1005 +#: nova/api/openstack/compute/servers.py:1025 msgid "Instance has not been resized." msgstr "" -#: nova/api/openstack/compute/servers.py:1013 +#: nova/api/openstack/compute/servers.py:1011 #, python-format msgid "Error in confirm-resize %s" msgstr "" -#: nova/api/openstack/compute/servers.py:1033 +#: nova/api/openstack/compute/servers.py:1031 #, python-format msgid "Error in revert-resize %s" msgstr "" -#: nova/api/openstack/compute/servers.py:1046 +#: nova/api/openstack/compute/servers.py:1044 msgid "Argument 'type' for reboot is not HARD or SOFT" msgstr "" -#: nova/api/openstack/compute/servers.py:1050 +#: nova/api/openstack/compute/servers.py:1048 msgid "Missing argument 'type' for reboot" msgstr "" -#: nova/api/openstack/compute/servers.py:1063 +#: nova/api/openstack/compute/servers.py:1061 #, python-format msgid "Error in reboot %s" msgstr "" -#: nova/api/openstack/compute/servers.py:1075 +#: nova/api/openstack/compute/servers.py:1073 msgid "Unable to locate requested flavor." msgstr "" -#: nova/api/openstack/compute/servers.py:1078 +#: nova/api/openstack/compute/servers.py:1076 msgid "Resize requires a flavor change." msgstr "" -#: nova/api/openstack/compute/servers.py:1102 +#: nova/api/openstack/compute/servers.py:1100 msgid "Missing imageRef attribute" msgstr "" -#: nova/api/openstack/compute/servers.py:1111 +#: nova/api/openstack/compute/servers.py:1109 msgid "Invalid imageRef provided." msgstr "" -#: nova/api/openstack/compute/servers.py:1138 +#: nova/api/openstack/compute/servers.py:1136 msgid "Missing flavorRef attribute" msgstr "" -#: nova/api/openstack/compute/servers.py:1151 +#: nova/api/openstack/compute/servers.py:1149 msgid "No adminPass was specified" msgstr "" -#: nova/api/openstack/compute/servers.py:1155 -#: nova/api/openstack/compute/servers.py:1357 +#: nova/api/openstack/compute/servers.py:1153 +#: nova/api/openstack/compute/servers.py:1355 msgid "Invalid adminPass" msgstr "" -#: nova/api/openstack/compute/servers.py:1166 +#: nova/api/openstack/compute/servers.py:1164 msgid "Unable to parse metadata key/value pairs." msgstr "" -#: nova/api/openstack/compute/servers.py:1179 +#: nova/api/openstack/compute/servers.py:1177 msgid "Resize request has invalid 'flavorRef' attribute." msgstr "" -#: nova/api/openstack/compute/servers.py:1182 +#: nova/api/openstack/compute/servers.py:1180 msgid "Resize requests require 'flavorRef' attribute." msgstr "" -#: nova/api/openstack/compute/servers.py:1200 +#: nova/api/openstack/compute/servers.py:1198 #: nova/api/openstack/compute/contrib/aggregates.py:143 -#: nova/api/openstack/compute/contrib/coverage_ext.py:223 +#: nova/api/openstack/compute/contrib/coverage_ext.py:246 #: nova/api/openstack/compute/contrib/keypairs.py:78 msgid "Invalid request body" msgstr "" -#: nova/api/openstack/compute/servers.py:1205 +#: nova/api/openstack/compute/servers.py:1203 msgid "Could not parse imageRef from request." msgstr "" -#: nova/api/openstack/compute/servers.py:1267 +#: nova/api/openstack/compute/servers.py:1265 msgid "Cannot find image for rebuild" msgstr "" -#: nova/api/openstack/compute/servers.py:1300 +#: nova/api/openstack/compute/servers.py:1298 msgid "createImage entity requires name attribute" msgstr "" -#: nova/api/openstack/compute/servers.py:1384 +#: nova/api/openstack/compute/servers.py:1382 #, python-format msgid "Removing options '%(unk_opt_str)s' from query" msgstr "" @@ -2468,7 +2503,7 @@ msgstr "" msgid "Only root certificate can be retrieved." msgstr "" -#: nova/api/openstack/compute/contrib/cloudpipe.py:149 +#: nova/api/openstack/compute/contrib/cloudpipe.py:151 msgid "" "Unable to claim IP for VPN instances, ensure it isn't running, and try " "again in a few minutes" @@ -2491,17 +2526,37 @@ msgstr "" msgid "Unable to get console" msgstr "" -#: nova/api/openstack/compute/contrib/coverage_ext.py:87 +#: nova/api/openstack/compute/contrib/coverage_ext.py:101 #, python-format msgid "Can't connect to service: %s, no portspecified\n" msgstr "" -#: nova/api/openstack/compute/contrib/coverage_ext.py:90 +#: nova/api/openstack/compute/contrib/coverage_ext.py:104 #, python-format msgid "No backdoor API command for service: %s\n" msgstr "" -#: nova/api/openstack/compute/contrib/coverage_ext.py:221 +#: nova/api/openstack/compute/contrib/coverage_ext.py:123 +msgid "Coverage begin" +msgstr "" + +#: nova/api/openstack/compute/contrib/coverage_ext.py:157 +msgid "Coverage not running" +msgstr "" + +#: nova/api/openstack/compute/contrib/coverage_ext.py:186 +msgid "Invalid path" +msgstr "" + +#: nova/api/openstack/compute/contrib/coverage_ext.py:190 +msgid "No path given for report file" +msgstr "" + +#: nova/api/openstack/compute/contrib/coverage_ext.py:197 +msgid "You can't use html reports without combining" +msgstr "" + +#: nova/api/openstack/compute/contrib/coverage_ext.py:244 #, python-format msgid "Coverage doesn't have %s action" msgstr "" @@ -2784,38 +2839,43 @@ msgstr "" msgid "stop instance" msgstr "" -#: nova/api/openstack/compute/contrib/volumes.py:72 +#: nova/api/openstack/compute/contrib/volumes.py:73 #, python-format msgid "vol=%s" msgstr "" -#: nova/api/openstack/compute/contrib/volumes.py:184 +#: nova/api/openstack/compute/contrib/volumes.py:185 #, python-format msgid "Delete volume with id: %s" msgstr "" -#: nova/api/openstack/compute/contrib/volumes.py:349 -#: nova/api/openstack/compute/contrib/volumes.py:429 +#: nova/api/openstack/compute/contrib/volumes.py:350 +#: nova/api/openstack/compute/contrib/volumes.py:438 #, python-format msgid "Instance %s is not attached." msgstr "" -#: nova/api/openstack/compute/contrib/volumes.py:380 +#: nova/api/openstack/compute/contrib/volumes.py:371 +#, python-format +msgid "Bad volumeId format: volumeId is not in proper format (%s)" +msgstr "" + +#: nova/api/openstack/compute/contrib/volumes.py:389 #, python-format msgid "Attach volume %(volume_id)s to instance %(server_id)s at %(device)s" msgstr "" -#: nova/api/openstack/compute/contrib/volumes.py:543 +#: nova/api/openstack/compute/contrib/volumes.py:552 #, python-format msgid "Delete snapshot with id: %s" msgstr "" -#: nova/api/openstack/compute/contrib/volumes.py:586 +#: nova/api/openstack/compute/contrib/volumes.py:595 #, python-format msgid "Create snapshot from volume %s" msgstr "" -#: nova/api/openstack/compute/contrib/volumes.py:590 +#: nova/api/openstack/compute/contrib/volumes.py:599 #, python-format msgid "Invalid value '%s' for force. " msgstr "" @@ -2824,6 +2884,122 @@ msgstr "" msgid "Instance has had its instance_type removed from the DB" msgstr "" +#: nova/cells/messaging.py:198 +#, python-format +msgid "Error processing message locally: %(exc)s" +msgstr "" + +#: nova/cells/messaging.py:352 nova/cells/messaging.py:358 +#, python-format +msgid "destination is %(target_cell)s but routing_path is %(routing_path)s" +msgstr "" + +#: nova/cells/messaging.py:368 +#, python-format +msgid "Unknown %(cell_type)s when routing to %(target_cell)s" +msgstr "" + +#: nova/cells/messaging.py:392 +#, python-format +msgid "Error locating next hop for message: %(exc)s" +msgstr "" + +#: nova/cells/messaging.py:419 +#, python-format +msgid "Failed to send message to cell: %(next_hop)s: %(exc)s" +msgstr "" + +#: nova/cells/messaging.py:498 +#, python-format +msgid "Error locating next hops for message: %(exc)s" +msgstr "" + +#: nova/cells/messaging.py:518 +#, python-format +msgid "Error sending message to next hops: %(exc)s" +msgstr "" + +#: nova/cells/messaging.py:536 +#, python-format +msgid "Error waiting for responses from neighbor cells: %(exc)s" +msgstr "" + +#: nova/cells/messaging.py:628 +#, python-format +msgid "Unknown method '%(method)s' in compute API" +msgstr "" + +#: nova/cells/messaging.py:651 +#, python-format +msgid "Received capabilities from child cell %(cell_name)s: %(capabilities)s" +msgstr "" + +#: nova/cells/messaging.py:660 +#, python-format +msgid "Received capacities from child cell %(cell_name)s: %(capacities)s" +msgstr "" + +#: nova/cells/messaging.py:719 +#, python-format +msgid "Got update for instance %(instance_uuid)s: %(instance)s" +msgstr "" + +#: nova/cells/messaging.py:742 +#, python-format +msgid "Got update to delete instance %(instance_uuid)s" +msgstr "" + +#: nova/cells/messaging.py:757 +#, python-format +msgid "Got broadcast to %(delete_type)s delete instance" +msgstr "" + +#: nova/cells/messaging.py:771 +#, python-format +msgid "Got message to create instance fault: %(instance_fault)s" +msgstr "" + +#: nova/cells/messaging.py:921 +#, python-format +msgid "Updating parents with our capabilities: %(capabs)s" +msgstr "" + +#: nova/cells/messaging.py:941 +#, python-format +msgid "Updating parents with our capacities: %(capacities)s" +msgstr "" + +#: nova/cells/scheduler.py:94 +#, python-format +msgid "Scheduling with routing_path=%(routing_path)s" +msgstr "" + +#: nova/cells/scheduler.py:117 +#, python-format +msgid "" +"No cells available when scheduling. Will retry in %(sleep_time)s " +"second(s)" +msgstr "" + +#: nova/cells/scheduler.py:124 +#, python-format +msgid "Error scheduling instances %(instance_uuids)s" +msgstr "" + +#: nova/cells/state.py:264 +msgid "Updating cell cache from db." +msgstr "" + +#: nova/cells/state.py:300 +#, python-format +msgid "Unknown cell '%(cell_name)s' when trying to update capabilities" +msgstr "" + +#: nova/cells/state.py:315 +#, python-format +msgid "Unknown cell '%(cell_name)s' when trying to update capacities" +msgstr "" + #: nova/cloudpipe/pipelib.py:43 msgid "Instance type for vpn instances" msgstr "" @@ -2958,95 +3134,95 @@ msgstr "" msgid "%(overs)s quota exceeded for %(pid)s, tried to resize instance." msgstr "" -#: nova/compute/api.py:1968 +#: nova/compute/api.py:1976 msgid "Locking" msgstr "" -#: nova/compute/api.py:1976 +#: nova/compute/api.py:1984 msgid "Unlocking" msgstr "" -#: nova/compute/api.py:2044 +#: nova/compute/api.py:2052 msgid "Volume must be attached in order to detach." msgstr "" -#: nova/compute/api.py:2129 +#: nova/compute/api.py:2137 #, python-format msgid "Going to try to live migrate instance to %s" msgstr "" -#: nova/compute/api.py:2286 +#: nova/compute/api.py:2294 msgid "Keypair name contains unsafe characters" msgstr "" -#: nova/compute/api.py:2290 +#: nova/compute/api.py:2298 msgid "Keypair name must be between 1 and 255 characters long" msgstr "" -#: nova/compute/api.py:2391 +#: nova/compute/api.py:2399 #, python-format msgid "Security group %s is not a string or unicode" msgstr "" -#: nova/compute/api.py:2394 +#: nova/compute/api.py:2402 #, python-format msgid "Security group %s cannot be empty." msgstr "" -#: nova/compute/api.py:2402 +#: nova/compute/api.py:2410 #, python-format msgid "" "Value (%(value)s) for parameter Group%(property)s is invalid. Content " "limited to '%(allowed)'." msgstr "" -#: nova/compute/api.py:2408 +#: nova/compute/api.py:2416 #, python-format msgid "Security group %s should not be greater than 255 characters." msgstr "" -#: nova/compute/api.py:2428 +#: nova/compute/api.py:2436 msgid "Quota exceeded, too many security groups." msgstr "" -#: nova/compute/api.py:2431 +#: nova/compute/api.py:2439 #, python-format msgid "Create Security Group %s" msgstr "" -#: nova/compute/api.py:2438 +#: nova/compute/api.py:2446 #, python-format msgid "Security group %s already exists" msgstr "" -#: nova/compute/api.py:2503 +#: nova/compute/api.py:2511 msgid "Security group is still in use" msgstr "" -#: nova/compute/api.py:2511 +#: nova/compute/api.py:2519 msgid "Failed to update usages deallocating security group" msgstr "" -#: nova/compute/api.py:2514 +#: nova/compute/api.py:2522 #, python-format msgid "Delete security group %s" msgstr "" -#: nova/compute/api.py:2771 +#: nova/compute/api.py:2779 #, python-format msgid "Rule (%s) not found" msgstr "" -#: nova/compute/api.py:2780 +#: nova/compute/api.py:2788 msgid "Quota exceeded, too many security group rules." msgstr "" -#: nova/compute/api.py:2783 +#: nova/compute/api.py:2791 #, python-format msgid "Authorize security group ingress %s" msgstr "" -#: nova/compute/api.py:2794 +#: nova/compute/api.py:2802 #, python-format msgid "Revoke security group ingress %s" msgstr "" @@ -3167,478 +3343,478 @@ msgstr "" msgid "Hypervisor driver does not support firewall rules" msgstr "" -#: nova/compute/manager.py:444 +#: nova/compute/manager.py:447 msgid "Checking state" msgstr "" -#: nova/compute/manager.py:520 +#: nova/compute/manager.py:523 #, python-format msgid "Setting up bdm %s" msgstr "" -#: nova/compute/manager.py:592 nova/compute/manager.py:1825 +#: nova/compute/manager.py:595 nova/compute/manager.py:1834 #, python-format msgid "No node specified, defaulting to %(node)s" msgstr "" -#: nova/compute/manager.py:625 +#: nova/compute/manager.py:628 msgid "Failed to dealloc network for deleted instance" msgstr "" -#: nova/compute/manager.py:648 +#: nova/compute/manager.py:651 #, python-format msgid "Error: %s" msgstr "" -#: nova/compute/manager.py:681 nova/compute/manager.py:1876 +#: nova/compute/manager.py:684 nova/compute/manager.py:1885 msgid "Error trying to reschedule" msgstr "" -#: nova/compute/manager.py:698 +#: nova/compute/manager.py:702 msgid "Retry info not present, will not reschedule" msgstr "" -#: nova/compute/manager.py:703 +#: nova/compute/manager.py:707 msgid "No request spec, will not reschedule" msgstr "" -#: nova/compute/manager.py:709 +#: nova/compute/manager.py:713 #, python-format msgid "Re-scheduling %(method)s: attempt %(num)d" msgstr "" -#: nova/compute/manager.py:732 +#: nova/compute/manager.py:741 msgid "Instance build timed out. Set to error state." msgstr "" -#: nova/compute/manager.py:762 +#: nova/compute/manager.py:771 msgid "Instance has already been created" msgstr "" -#: nova/compute/manager.py:808 +#: nova/compute/manager.py:817 #, python-format msgid "" "image_id=%(image_id)s, image_size_bytes=%(size_bytes)d, " "allowed_size_bytes=%(allowed_size_bytes)d" msgstr "" -#: nova/compute/manager.py:814 +#: nova/compute/manager.py:823 #, python-format msgid "" "Image '%(image_id)s' size %(size_bytes)d exceeded instance_type allowed " "size %(allowed_size_bytes)d" msgstr "" -#: nova/compute/manager.py:824 +#: nova/compute/manager.py:833 msgid "Starting instance..." msgstr "" -#: nova/compute/manager.py:845 +#: nova/compute/manager.py:854 msgid "Instance failed network setup" msgstr "" -#: nova/compute/manager.py:849 +#: nova/compute/manager.py:858 #, python-format msgid "Instance network_info: |%s|" msgstr "" -#: nova/compute/manager.py:862 +#: nova/compute/manager.py:871 msgid "Instance failed block device setup" msgstr "" -#: nova/compute/manager.py:880 +#: nova/compute/manager.py:889 msgid "Instance failed to spawn" msgstr "" -#: nova/compute/manager.py:904 +#: nova/compute/manager.py:913 msgid "Deallocating network for instance" msgstr "" -#: nova/compute/manager.py:976 +#: nova/compute/manager.py:985 #, python-format msgid "%(action_str)s instance" msgstr "" -#: nova/compute/manager.py:1007 +#: nova/compute/manager.py:1016 #, python-format msgid "Ignoring DiskNotFound: %s" msgstr "" -#: nova/compute/manager.py:1010 +#: nova/compute/manager.py:1019 #, python-format msgid "Ignoring VolumeNotFound: %s" msgstr "" -#: nova/compute/manager.py:1017 +#: nova/compute/manager.py:1026 #, python-format msgid "terminating bdm %s" msgstr "" -#: nova/compute/manager.py:1042 +#: nova/compute/manager.py:1051 #, python-format msgid "Ignoring volume cleanup failure due to %s" msgstr "" -#: nova/compute/manager.py:1082 nova/compute/manager.py:2044 -#: nova/compute/manager.py:3370 +#: nova/compute/manager.py:1090 nova/compute/manager.py:2053 +#: nova/compute/manager.py:3388 #, python-format msgid "%s. Setting instance vm_state to ERROR" msgstr "" -#: nova/compute/manager.py:1216 +#: nova/compute/manager.py:1224 msgid "Rebuilding instance" msgstr "" -#: nova/compute/manager.py:1302 +#: nova/compute/manager.py:1311 msgid "Rebooting instance" msgstr "" -#: nova/compute/manager.py:1326 +#: nova/compute/manager.py:1335 #, python-format msgid "" "trying to reboot a non-running instance: (state: %(state)s expected: " "%(running)s)" msgstr "" -#: nova/compute/manager.py:1335 +#: nova/compute/manager.py:1344 #, python-format msgid "Cannot reboot instance: %(exc)s" msgstr "" -#: nova/compute/manager.py:1372 +#: nova/compute/manager.py:1381 msgid "instance snapshotting" msgstr "" -#: nova/compute/manager.py:1378 +#: nova/compute/manager.py:1387 #, python-format msgid "" "trying to snapshot a non-running instance: (state: %(state)s expected: " "%(running)s)" msgstr "" -#: nova/compute/manager.py:1431 +#: nova/compute/manager.py:1440 #, python-format msgid "Found %(num_images)d images (rotation: %(rotation)d)" msgstr "" -#: nova/compute/manager.py:1438 +#: nova/compute/manager.py:1447 #, python-format msgid "Rotating out %d backups" msgstr "" -#: nova/compute/manager.py:1443 +#: nova/compute/manager.py:1452 #, python-format msgid "Deleting image %s" msgstr "" -#: nova/compute/manager.py:1474 +#: nova/compute/manager.py:1483 #, python-format msgid "Failed to set admin password. Instance %s is not running" msgstr "" -#: nova/compute/manager.py:1481 +#: nova/compute/manager.py:1490 msgid "Root password set" msgstr "" -#: nova/compute/manager.py:1491 +#: nova/compute/manager.py:1500 msgid "set_admin_password is not implemented by this driver." msgstr "" -#: nova/compute/manager.py:1507 +#: nova/compute/manager.py:1516 #, python-format msgid "set_admin_password failed: %s" msgstr "" -#: nova/compute/manager.py:1515 +#: nova/compute/manager.py:1524 msgid "error setting admin password" msgstr "" -#: nova/compute/manager.py:1530 +#: nova/compute/manager.py:1539 #, python-format msgid "" "trying to inject a file into a non-running (state: " "%(current_power_state)s expected: %(expected_state)s)" msgstr "" -#: nova/compute/manager.py:1534 +#: nova/compute/manager.py:1543 #, python-format msgid "injecting file to %(path)s" msgstr "" -#: nova/compute/manager.py:1555 +#: nova/compute/manager.py:1564 msgid "" "Unable to find a different image to use for rescue VM, using instance's " "current image" msgstr "" -#: nova/compute/manager.py:1568 +#: nova/compute/manager.py:1577 msgid "Rescuing" msgstr "" -#: nova/compute/manager.py:1602 +#: nova/compute/manager.py:1611 msgid "Unrescuing" msgstr "" -#: nova/compute/manager.py:1623 +#: nova/compute/manager.py:1632 #, python-format msgid "Changing instance metadata according to %(diff)r" msgstr "" -#: nova/compute/manager.py:1792 +#: nova/compute/manager.py:1801 msgid "Instance has no source host" msgstr "" -#: nova/compute/manager.py:1798 +#: nova/compute/manager.py:1807 msgid "destination same as source!" msgstr "" -#: nova/compute/manager.py:1807 +#: nova/compute/manager.py:1816 msgid "Migrating" msgstr "" -#: nova/compute/manager.py:2041 +#: nova/compute/manager.py:2050 #, python-format msgid "Failed to rollback quota for failed finish_resize: %(qr_error)s" msgstr "" -#: nova/compute/manager.py:2097 +#: nova/compute/manager.py:2106 msgid "Pausing" msgstr "" -#: nova/compute/manager.py:2114 +#: nova/compute/manager.py:2123 msgid "Unpausing" msgstr "" -#: nova/compute/manager.py:2152 +#: nova/compute/manager.py:2161 msgid "Retrieving diagnostics" msgstr "" -#: nova/compute/manager.py:2182 +#: nova/compute/manager.py:2191 msgid "Resuming" msgstr "" -#: nova/compute/manager.py:2204 +#: nova/compute/manager.py:2213 msgid "Reset network" msgstr "" -#: nova/compute/manager.py:2209 +#: nova/compute/manager.py:2218 msgid "Inject network info" msgstr "" -#: nova/compute/manager.py:2212 +#: nova/compute/manager.py:2221 #, python-format msgid "network_info to inject: |%s|" msgstr "" -#: nova/compute/manager.py:2229 +#: nova/compute/manager.py:2238 msgid "Get console output" msgstr "" -#: nova/compute/manager.py:2254 +#: nova/compute/manager.py:2263 msgid "Getting vnc console" msgstr "" -#: nova/compute/manager.py:2282 +#: nova/compute/manager.py:2291 #, python-format msgid "Booting with volume %(volume_id)s at %(mountpoint)s" msgstr "" -#: nova/compute/manager.py:2326 +#: nova/compute/manager.py:2336 #, python-format msgid "Attaching volume %(volume_id)s to %(mountpoint)s" msgstr "" -#: nova/compute/manager.py:2335 +#: nova/compute/manager.py:2345 #, python-format msgid "" "Failed to connect to volume %(volume_id)s while attaching at " "%(mountpoint)s" msgstr "" -#: nova/compute/manager.py:2350 +#: nova/compute/manager.py:2360 #, python-format msgid "Failed to attach volume %(volume_id)s at %(mountpoint)s" msgstr "" -#: nova/compute/manager.py:2379 +#: nova/compute/manager.py:2390 #, python-format msgid "Detach volume %(volume_id)s from mountpoint %(mp)s" msgstr "" -#: nova/compute/manager.py:2383 +#: nova/compute/manager.py:2394 msgid "Detaching volume from unknown instance" msgstr "" -#: nova/compute/manager.py:2396 +#: nova/compute/manager.py:2407 #, python-format msgid "Faild to detach volume %(volume_id)s from %(mp)s" msgstr "" -#: nova/compute/manager.py:2421 +#: nova/compute/manager.py:2431 msgid "Updating volume usage cache with totals" msgstr "" -#: nova/compute/manager.py:2458 +#: nova/compute/manager.py:2468 #, python-format msgid "Host %(host)s not found" msgstr "" -#: nova/compute/manager.py:2531 +#: nova/compute/manager.py:2541 msgid "Instance has no volume." msgstr "" -#: nova/compute/manager.py:2592 +#: nova/compute/manager.py:2602 #, python-format msgid "Pre live migration failed at %(dest)s" msgstr "" -#: nova/compute/manager.py:2620 +#: nova/compute/manager.py:2630 msgid "_post_live_migration() is started.." msgstr "" -#: nova/compute/manager.py:2673 +#: nova/compute/manager.py:2683 #, python-format msgid "Migrating instance to %(dest)s finished successfully." msgstr "" -#: nova/compute/manager.py:2675 +#: nova/compute/manager.py:2685 msgid "" "You may see the error \"libvirt: QEMU error: Domain not found: no domain " "with matching name.\" This error can be safely ignored." msgstr "" -#: nova/compute/manager.py:2689 +#: nova/compute/manager.py:2699 msgid "Post operation of migration started" msgstr "" -#: nova/compute/manager.py:2833 +#: nova/compute/manager.py:2842 msgid "Updated the info_cache for instance" msgstr "" -#: nova/compute/manager.py:2877 +#: nova/compute/manager.py:2887 #, python-format msgid "" "Found %(migration_count)d unconfirmed migrations older than " "%(confirm_window)d seconds" msgstr "" -#: nova/compute/manager.py:2883 +#: nova/compute/manager.py:2893 #, python-format msgid "Setting migration %(migration_id)s to error: %(reason)s" msgstr "" -#: nova/compute/manager.py:2892 +#: nova/compute/manager.py:2902 #, python-format msgid "" "Automatically confirming migration %(migration_id)s for instance " "%(instance_uuid)s" msgstr "" -#: nova/compute/manager.py:2899 +#: nova/compute/manager.py:2909 #, python-format msgid "Instance %(instance_uuid)s not found" msgstr "" -#: nova/compute/manager.py:2903 +#: nova/compute/manager.py:2913 msgid "In ERROR state" msgstr "" -#: nova/compute/manager.py:2910 +#: nova/compute/manager.py:2920 #, python-format msgid "In states %(vm_state)s/%(task_state)s, notRESIZED/None" msgstr "" -#: nova/compute/manager.py:2918 +#: nova/compute/manager.py:2928 #, python-format msgid "Error auto-confirming resize: %(e)s. Will retry later." msgstr "" -#: nova/compute/manager.py:2935 +#: nova/compute/manager.py:2943 #, python-format msgid "" "Running instance usage audit for host %(host)s from %(begin_time)s to " "%(end_time)s. %(number_instances)s instances." msgstr "" -#: nova/compute/manager.py:2953 +#: nova/compute/manager.py:2961 #, python-format msgid "Failed to generate usage audit for instance on host %s" msgstr "" -#: nova/compute/manager.py:2976 +#: nova/compute/manager.py:2984 msgid "Updating bandwidth usage cache" msgstr "" -#: nova/compute/manager.py:3089 +#: nova/compute/manager.py:3102 msgid "Updating volume usage cache" msgstr "" -#: nova/compute/manager.py:3107 +#: nova/compute/manager.py:3120 msgid "Updating host status" msgstr "" -#: nova/compute/manager.py:3135 +#: nova/compute/manager.py:3149 #, python-format msgid "" "Found %(num_db_instances)s in the database and %(num_vm_instances)s on " "the hypervisor." msgstr "" -#: nova/compute/manager.py:3141 nova/compute/manager.py:3179 +#: nova/compute/manager.py:3155 nova/compute/manager.py:3193 msgid "During sync_power_state the instance has a pending task. Skip." msgstr "" -#: nova/compute/manager.py:3166 +#: nova/compute/manager.py:3180 #, python-format msgid "" "During the sync_power process the instance has moved from host %(src)s to" " host %(dst)s" msgstr "" -#: nova/compute/manager.py:3202 +#: nova/compute/manager.py:3216 msgid "Instance shutdown by itself. Calling the stop API." msgstr "" -#: nova/compute/manager.py:3214 nova/compute/manager.py:3225 -#: nova/compute/manager.py:3239 +#: nova/compute/manager.py:3228 nova/compute/manager.py:3239 +#: nova/compute/manager.py:3253 msgid "error during stop() in sync_power_state." msgstr "" -#: nova/compute/manager.py:3219 +#: nova/compute/manager.py:3233 msgid "Instance is paused or suspended unexpectedly. Calling the stop API." msgstr "" -#: nova/compute/manager.py:3232 +#: nova/compute/manager.py:3246 msgid "Instance is not stopped. Calling the stop API." msgstr "" -#: nova/compute/manager.py:3248 +#: nova/compute/manager.py:3262 msgid "Instance is not (soft-)deleted." msgstr "" -#: nova/compute/manager.py:3256 +#: nova/compute/manager.py:3270 msgid "CONF.reclaim_instance_interval <= 0, skipping..." msgstr "" -#: nova/compute/manager.py:3269 +#: nova/compute/manager.py:3285 msgid "Reclaiming deleted instance" msgstr "" -#: nova/compute/manager.py:3324 +#: nova/compute/manager.py:3341 #, python-format msgid "" "Detected instance with name label '%(name)s' which is marked as DELETED " "but still present on host." msgstr "" -#: nova/compute/manager.py:3331 +#: nova/compute/manager.py:3348 #, python-format msgid "" "Destroying instance with name label '%(name)s' which is marked as DELETED" " but still present on host." msgstr "" -#: nova/compute/manager.py:3338 +#: nova/compute/manager.py:3355 #, python-format msgid "Unrecognized value '%(action)s' for CONF.running_deleted_instance_action" msgstr "" @@ -3727,14 +3903,14 @@ msgstr "" msgid "Instance not resizing, skipping migration." msgstr "" -#: nova/compute/resource_tracker.py:535 +#: nova/compute/resource_tracker.py:538 #, python-format msgid "" "Detected running orphan instance: %(uuid)s (consuming %(memory_mb)s MB " "memory" msgstr "" -#: nova/compute/resource_tracker.py:548 +#: nova/compute/resource_tracker.py:551 #, python-format msgid "Missing keys: %s" msgstr "" @@ -3748,16 +3924,20 @@ msgstr "" msgid "Unable to find host for Instance %s" msgstr "" -#: nova/compute/utils.py:94 +#: nova/compute/utils.py:101 #, python-format msgid "Using %(prefix)s instead of %(req_prefix)s" msgstr "" -#: nova/conductor/manager.py:59 +#: nova/conductor/manager.py:62 #, python-format msgid "Instance update attempted for '%(key)s' on %(instance_uuid)s" msgstr "" +#: nova/conductor/manager.py:198 +msgid "Invalid block_device_mapping_destroy invocation" +msgstr "" + #: nova/console/manager.py:80 nova/console/vmrc_manager.py:62 msgid "Adding console" msgstr "" @@ -3820,19 +4000,39 @@ msgstr "" msgid "Checking Token: %(token)s, %(token_valid)s)" msgstr "" -#: nova/db/sqlalchemy/api.py:183 nova/virt/baremetal/db/sqlalchemy/api.py:61 +#: nova/db/api.py:580 +msgid "Failed to notify cells of instance destroy" +msgstr "" + +#: nova/db/api.py:689 nova/db/api.py:710 +msgid "Failed to notify cells of instance update" +msgstr "" + +#: nova/db/api.py:749 +msgid "Failed to notify cells of instance info cache update" +msgstr "" + +#: nova/db/api.py:1458 +msgid "Failed to notify cells of bw_usage update" +msgstr "" + +#: nova/db/api.py:1602 +msgid "Failed to notify cells of instance fault" +msgstr "" + +#: nova/db/sqlalchemy/api.py:182 nova/virt/baremetal/db/sqlalchemy/api.py:61 #, python-format msgid "Unrecognized read_deleted value '%s'" msgstr "" -#: nova/db/sqlalchemy/api.py:1372 +#: nova/db/sqlalchemy/api.py:1374 #, python-format msgid "" "Unknown osapi_compute_unique_server_name_scope value: %s Flag must be " "empty, \"global\" or \"project\"" msgstr "" -#: nova/db/sqlalchemy/api.py:2732 +#: nova/db/sqlalchemy/api.py:2736 #, python-format msgid "Change will make usage less than 0 for the following resources: %(unders)s" msgstr "" @@ -3847,20 +4047,20 @@ msgstr "" msgid "Upgrade DB using Essex release first." msgstr "" -#: nova/db/sqlalchemy/session.py:385 +#: nova/db/sqlalchemy/session.py:384 msgid "DB exception wrapped." msgstr "" -#: nova/db/sqlalchemy/session.py:442 +#: nova/db/sqlalchemy/session.py:434 #, python-format msgid "Got mysql server has gone away: %s" msgstr "" -#: nova/db/sqlalchemy/session.py:483 +#: nova/db/sqlalchemy/session.py:475 msgid "Using mysql/eventlet db_pool." msgstr "" -#: nova/db/sqlalchemy/session.py:527 +#: nova/db/sqlalchemy/session.py:519 #, python-format msgid "SQL connection failed. %s attempts left." msgstr "" @@ -4110,69 +4310,69 @@ msgstr "" msgid "Unplugged gateway interface '%s'" msgstr "" -#: nova/network/manager.py:320 +#: nova/network/manager.py:326 #, python-format msgid "Fixed ip %(fixed_ip_id)s not found" msgstr "" -#: nova/network/manager.py:329 nova/network/manager.py:591 +#: nova/network/manager.py:335 nova/network/manager.py:606 #, python-format msgid "Interface %(interface)s not found" msgstr "" -#: nova/network/manager.py:344 +#: nova/network/manager.py:350 #, python-format msgid "floating IP allocation for instance |%s|" msgstr "" -#: nova/network/manager.py:408 +#: nova/network/manager.py:414 msgid "Floating IP is not associated. Ignore." msgstr "" -#: nova/network/manager.py:426 +#: nova/network/manager.py:432 #, python-format msgid "Address |%(address)s| is not allocated" msgstr "" -#: nova/network/manager.py:430 +#: nova/network/manager.py:436 #, python-format msgid "Address |%(address)s| is not allocated to your project |%(project)s|" msgstr "" -#: nova/network/manager.py:451 +#: nova/network/manager.py:457 #, python-format msgid "Quota exceeded for %(pid)s, tried to allocate floating IP" msgstr "" -#: nova/network/manager.py:513 +#: nova/network/manager.py:519 msgid "Failed to update usages deallocating floating IP" msgstr "" -#: nova/network/manager.py:722 +#: nova/network/manager.py:753 #, python-format msgid "Starting migration network for instance %(instance_uuid)s" msgstr "" -#: nova/network/manager.py:729 +#: nova/network/manager.py:760 #, python-format msgid "" "Floating ip address |%(address)s| no longer belongs to instance " "%(instance_uuid)s. Will notmigrate it " msgstr "" -#: nova/network/manager.py:759 +#: nova/network/manager.py:790 #, python-format msgid "Finishing migration network for instance %(instance_uuid)s" msgstr "" -#: nova/network/manager.py:767 +#: nova/network/manager.py:798 #, python-format msgid "" "Floating ip address |%(address)s| no longer belongs to instance " "%(instance_uuid)s. Will notsetup it." msgstr "" -#: nova/network/manager.py:814 +#: nova/network/manager.py:845 #, python-format msgid "" "Database inconsistency: DNS domain |%s| is registered in the Nova db but " @@ -4180,39 +4380,39 @@ msgid "" "ignored." msgstr "" -#: nova/network/manager.py:860 +#: nova/network/manager.py:891 #, python-format msgid "Domain |%(domain)s| already exists, changing zone to |%(av_zone)s|." msgstr "" -#: nova/network/manager.py:870 +#: nova/network/manager.py:901 #, python-format msgid "Domain |%(domain)s| already exists, changing project to |%(project)s|." msgstr "" -#: nova/network/manager.py:986 +#: nova/network/manager.py:1017 #, python-format msgid "Disassociated %s stale fixed ip(s)" msgstr "" -#: nova/network/manager.py:990 +#: nova/network/manager.py:1021 msgid "setting network host" msgstr "" -#: nova/network/manager.py:1117 +#: nova/network/manager.py:1148 msgid "network allocations" msgstr "" -#: nova/network/manager.py:1122 +#: nova/network/manager.py:1153 #, python-format msgid "networks retrieved for instance: |%(networks)s|" msgstr "" -#: nova/network/manager.py:1158 +#: nova/network/manager.py:1189 msgid "network deallocation for instance" msgstr "" -#: nova/network/manager.py:1388 +#: nova/network/manager.py:1419 #, python-format msgid "" "instance-dns-zone is |%(domain)s|, which is in availability zone " @@ -4220,89 +4420,89 @@ msgid "" "created." msgstr "" -#: nova/network/manager.py:1476 +#: nova/network/manager.py:1507 #, python-format msgid "Unable to release %s because vif doesn't exist." msgstr "" -#: nova/network/manager.py:1495 +#: nova/network/manager.py:1526 #, python-format msgid "Leased IP |%(address)s|" msgstr "" -#: nova/network/manager.py:1499 +#: nova/network/manager.py:1530 #, python-format msgid "IP %s leased that is not associated" msgstr "" -#: nova/network/manager.py:1507 +#: nova/network/manager.py:1538 #, python-format msgid "IP |%s| leased that isn't allocated" msgstr "" -#: nova/network/manager.py:1512 +#: nova/network/manager.py:1543 #, python-format msgid "Released IP |%(address)s|" msgstr "" -#: nova/network/manager.py:1516 +#: nova/network/manager.py:1547 #, python-format msgid "IP %s released that is not associated" msgstr "" -#: nova/network/manager.py:1519 +#: nova/network/manager.py:1550 #, python-format msgid "IP %s released that was not leased" msgstr "" -#: nova/network/manager.py:1538 +#: nova/network/manager.py:1569 #, python-format msgid "%s must be an integer" msgstr "" -#: nova/network/manager.py:1562 +#: nova/network/manager.py:1593 msgid "Maximum allowed length for 'label' is 255." msgstr "" -#: nova/network/manager.py:1582 +#: nova/network/manager.py:1613 #, python-format msgid "" "Subnet(s) too large, defaulting to /%s. To override, specify " "network_size flag." msgstr "" -#: nova/network/manager.py:1663 +#: nova/network/manager.py:1694 msgid "cidr already in use" msgstr "" -#: nova/network/manager.py:1666 +#: nova/network/manager.py:1697 #, python-format msgid "requested cidr (%(cidr)s) conflicts with existing supernet (%(super)s)" msgstr "" -#: nova/network/manager.py:1677 +#: nova/network/manager.py:1708 #, python-format msgid "" "requested cidr (%(cidr)s) conflicts with existing smaller cidr " "(%(smaller)s)" msgstr "" -#: nova/network/manager.py:1734 +#: nova/network/manager.py:1765 msgid "Network already exists!" msgstr "" -#: nova/network/manager.py:1754 +#: nova/network/manager.py:1785 #, python-format msgid "Network must be disassociated from project %s before delete" msgstr "" -#: nova/network/manager.py:2268 +#: nova/network/manager.py:2308 msgid "" "The sum between the number of networks and the vlan start cannot be " "greater than 4094" msgstr "" -#: nova/network/manager.py:2275 +#: nova/network/manager.py:2315 #, python-format msgid "" "The network range is not big enough to fit %(num_networks)s. Network size" @@ -4336,7 +4536,7 @@ msgstr "" msgid "v4 subnets are required for legacy nw_info" msgstr "" -#: nova/network/quantumv2/__init__.py:41 +#: nova/network/quantumv2/__init__.py:40 msgid "_get_auth_token() failed" msgstr "" @@ -4350,36 +4550,36 @@ msgstr "" msgid "empty project id for instance %s" msgstr "" -#: nova/network/quantumv2/api.py:150 +#: nova/network/quantumv2/api.py:151 msgid "Port not found" msgstr "" -#: nova/network/quantumv2/api.py:158 +#: nova/network/quantumv2/api.py:159 #, python-format msgid "Fail to delete port %(portid)s with failure: %(exception)s" msgstr "" -#: nova/network/quantumv2/api.py:170 +#: nova/network/quantumv2/api.py:171 #, python-format msgid "deallocate_for_instance() for %s" msgstr "" -#: nova/network/quantumv2/api.py:179 +#: nova/network/quantumv2/api.py:180 #, python-format msgid "Failed to delete quantum port %(portid)s " msgstr "" -#: nova/network/quantumv2/api.py:189 +#: nova/network/quantumv2/api.py:190 #, python-format msgid "get_instance_nw_info() for %s" msgstr "" -#: nova/network/quantumv2/api.py:204 +#: nova/network/quantumv2/api.py:205 #, python-format msgid "validate_networks() for %s" msgstr "" -#: nova/network/quantumv2/api.py:458 +#: nova/network/quantumv2/api.py:459 #, python-format msgid "Multiple floating IP pools matches found for name '%s'" msgstr "" @@ -4835,19 +5035,24 @@ msgstr "" msgid "Invalid value for 'scheduler_max_attempts', must be >= 1" msgstr "" -#: nova/scheduler/filter_scheduler.py:217 +#: nova/scheduler/filter_scheduler.py:207 +#, python-format +msgid "Error from last host: %(last_host)s (node %(last_node)s): %(exc)s" +msgstr "" + +#: nova/scheduler/filter_scheduler.py:236 #, python-format msgid "" "Exceeded max scheduling attempts %(max_attempts)d for instance " "%(instance_uuid)s" msgstr "" -#: nova/scheduler/filter_scheduler.py:271 +#: nova/scheduler/filter_scheduler.py:290 #, python-format msgid "Filtered %(hosts)s" msgstr "" -#: nova/scheduler/filter_scheduler.py:276 +#: nova/scheduler/filter_scheduler.py:295 #, python-format msgid "Choosing host %(best_host)s" msgstr "" @@ -4980,9 +5185,9 @@ msgid "" " %(usable_ram)s MB usable ram." msgstr "" -#: nova/scheduler/filters/retry_filter.py:38 +#: nova/scheduler/filters/retry_filter.py:41 #, python-format -msgid "Previously tried hosts: %(hosts)s. (host=%(host)s)" +msgid "Host %(host)s %(pass_msg)s. Previously tried hosts: %(hosts)s" msgstr "" #: nova/scheduler/filters/trusted_filter.py:200 @@ -5085,15 +5290,15 @@ msgstr "" msgid "Reply to faked command is stdout='%(stdout)s' stderr='%(stderr)s'" msgstr "" -#: nova/tests/fake_volume.py:182 nova/volume/cinder.py:175 +#: nova/tests/fake_volume.py:182 nova/volume/cinder.py:179 msgid "status must be available" msgstr "" -#: nova/tests/fake_volume.py:186 nova/volume/cinder.py:178 +#: nova/tests/fake_volume.py:186 nova/volume/cinder.py:182 msgid "already attached" msgstr "" -#: nova/tests/fake_volume.py:191 nova/volume/cinder.py:184 +#: nova/tests/fake_volume.py:191 nova/volume/cinder.py:188 msgid "already detached" msgstr "" @@ -5113,7 +5318,7 @@ msgstr "" msgid "Expected a function in 'auth[1]' parameter" msgstr "" -#: nova/tests/test_misc.py:57 +#: nova/tests/test_misc.py:59 #, python-format msgid "" "The following migrations are missing a downgrade:\n" @@ -5178,17 +5383,17 @@ msgstr "" msgid "unexpected role header" msgstr "" -#: nova/tests/api/openstack/compute/test_servers.py:3059 +#: nova/tests/api/openstack/compute/test_servers.py:3097 msgid "" "Quota exceeded for instances: Requested 1, but already used 10 of 10 " "instances" msgstr "" -#: nova/tests/api/openstack/compute/test_servers.py:3064 +#: nova/tests/api/openstack/compute/test_servers.py:3102 msgid "Quota exceeded for ram: Requested 4096, but already used 8192 of 10240 ram" msgstr "" -#: nova/tests/api/openstack/compute/test_servers.py:3069 +#: nova/tests/api/openstack/compute/test_servers.py:3107 msgid "Quota exceeded for cores: Requested 2, but already used 9 of 10 cores" msgstr "" @@ -5243,42 +5448,42 @@ msgstr "" msgid "test_snapshot_detail: resp_dict=%s" msgstr "" -#: nova/tests/compute/test_compute.py:649 -#: nova/tests/compute/test_compute.py:667 -#: nova/tests/compute/test_compute.py:718 -#: nova/tests/compute/test_compute.py:743 -#: nova/tests/compute/test_compute.py:2592 +#: nova/tests/compute/test_compute.py:650 +#: nova/tests/compute/test_compute.py:668 +#: nova/tests/compute/test_compute.py:719 +#: nova/tests/compute/test_compute.py:746 +#: nova/tests/compute/test_compute.py:2604 #, python-format msgid "Running instances: %s" msgstr "" -#: nova/tests/compute/test_compute.py:655 -#: nova/tests/compute/test_compute.py:690 -#: nova/tests/compute/test_compute.py:731 -#: nova/tests/compute/test_compute.py:761 +#: nova/tests/compute/test_compute.py:656 +#: nova/tests/compute/test_compute.py:691 +#: nova/tests/compute/test_compute.py:734 +#: nova/tests/compute/test_compute.py:764 #, python-format msgid "After terminating instances: %s" msgstr "" -#: nova/tests/compute/test_compute.py:1179 +#: nova/tests/compute/test_compute.py:1182 msgid "Internal error" msgstr "" -#: nova/tests/compute/test_compute.py:2603 +#: nova/tests/compute/test_compute.py:2615 #, python-format msgid "After force-killing instances: %s" msgstr "" -#: nova/tests/compute/test_compute.py:3072 +#: nova/tests/compute/test_compute.py:3085 msgid "wrong host/node" msgstr "" -#: nova/tests/hyperv/hypervutils.py:150 nova/virt/hyperv/vmops.py:559 +#: nova/tests/hyperv/hypervutils.py:150 nova/virt/hyperv/vmops.py:552 #, python-format msgid "Failed to change vm state of %(vm_name)s to %(req_state)s" msgstr "" -#: nova/tests/hyperv/hypervutils.py:208 nova/virt/hyperv/vmops.py:494 +#: nova/tests/hyperv/hypervutils.py:208 nova/virt/hyperv/vmops.py:487 #, python-format msgid "Failed to destroy vm %s" msgstr "" @@ -5310,12 +5515,12 @@ msgid "" "arguments \"%(params)s\"" msgstr "" -#: nova/tests/integrated/test_api_samples.py:153 +#: nova/tests/integrated/test_api_samples.py:157 #, python-format msgid "Result: %(result)s is not a dict." msgstr "" -#: nova/tests/integrated/test_api_samples.py:157 +#: nova/tests/integrated/test_api_samples.py:161 #, python-format msgid "" "Key mismatch:\n" @@ -5323,25 +5528,25 @@ msgid "" "%(res_keys)s" msgstr "" -#: nova/tests/integrated/test_api_samples.py:165 +#: nova/tests/integrated/test_api_samples.py:169 #, python-format msgid "Result: %(result)s is not a list." msgstr "" -#: nova/tests/integrated/test_api_samples.py:168 +#: nova/tests/integrated/test_api_samples.py:172 #, python-format msgid "" "Length mismatch: %(result)s\n" "%(expected)s." msgstr "" -#: nova/tests/integrated/test_api_samples.py:179 +#: nova/tests/integrated/test_api_samples.py:183 #, python-format msgid "Result: %(res_obj)s not in %(expected)s." msgstr "" -#: nova/tests/integrated/test_api_samples.py:197 -#: nova/tests/integrated/test_api_samples.py:210 +#: nova/tests/integrated/test_api_samples.py:201 +#: nova/tests/integrated/test_api_samples.py:214 #, python-format msgid "" "Values do not match:\n" @@ -5403,7 +5608,7 @@ msgstr "" msgid "Decoding JSON: %s" msgstr "" -#: nova/virt/configdrive.py:80 +#: nova/virt/configdrive.py:92 #, python-format msgid "Added %(filepath)s to config drive" msgstr "" @@ -5467,32 +5672,85 @@ msgstr "" msgid "Converted to raw, but format is now %s" msgstr "" -#: nova/virt/baremetal/driver.py:86 +#: nova/virt/baremetal/driver.py:90 #, python-format msgid "Request for baremetal node %s sent to wrong service host" msgstr "" -#: nova/virt/baremetal/driver.py:136 +#: nova/virt/baremetal/driver.py:142 msgid "cpu_arch is not found in instance_type_extra_specs" msgstr "" -#: nova/virt/baremetal/driver.py:175 +#: nova/virt/baremetal/driver.py:182 msgid "Baremetal node id not supplied to driver" msgstr "" -#: nova/virt/baremetal/driver.py:179 +#: nova/virt/baremetal/driver.py:241 #, python-format -msgid "Baremetal node %s already in use" +msgid "Failed to update state record for baremetal node %s" msgstr "" -#: nova/virt/baremetal/driver.py:241 +#: nova/virt/baremetal/driver.py:260 #, python-format msgid "Delete called on non-existing instance %s" msgstr "" -#: nova/virt/baremetal/utils.py:37 +#: nova/virt/baremetal/ipmi.py:83 +#, python-format +msgid "pid file %s does not contain any pid" +msgstr "" + +#: nova/virt/baremetal/ipmi.py:106 +msgid "Node id not supplied to IPMI" +msgstr "" + +#: nova/virt/baremetal/ipmi.py:108 +msgid "Address not supplied to IPMI" +msgstr "" + +#: nova/virt/baremetal/ipmi.py:110 +msgid "User not supplied to IPMI" +msgstr "" + +#: nova/virt/baremetal/ipmi.py:112 +msgid "Password not supplied to IPMI" +msgstr "" + +#: nova/virt/baremetal/ipmi.py:128 +#, python-format +msgid "ipmitool stdout: '%(out)s', stderr: '%(err)%s'" +msgstr "" + +#: nova/virt/baremetal/ipmi.py:154 +msgid "IPMI power on failed" +msgstr "" + +#: nova/virt/baremetal/ipmi.py:176 +msgid "IPMI power off failed" +msgstr "" + +#: nova/virt/baremetal/ipmi.py:186 +msgid "IPMI set next bootdev failed" +msgstr "" + +#: nova/virt/baremetal/ipmi.py:191 +#, python-format +msgid "Activate node called, but node %s is already active" +msgstr "" + +#: nova/virt/baremetal/utils.py:39 +#, python-format +msgid "Failed to inject data into image %(image)s. Error: %(e)s" +msgstr "" + +#: nova/virt/baremetal/utils.py:47 #, python-format -msgid "failed to unlink %s" +msgid "Failed to unlink %s" +msgstr "" + +#: nova/virt/baremetal/utils.py:59 +#, python-format +msgid "Failed to create symlink from %(source)s to %(link)s" msgstr "" #: nova/virt/baremetal/vif_driver.py:37 @@ -5527,51 +5785,82 @@ msgstr "" msgid "no pif for vif_uuid=%s" msgstr "" -#: nova/virt/baremetal/volume_driver.py:128 +#: nova/virt/baremetal/volume_driver.py:131 #, python-format msgid "baremetal driver was unable to delete tid %s" msgstr "" -#: nova/virt/baremetal/volume_driver.py:192 +#: nova/virt/baremetal/volume_driver.py:195 #, python-format msgid "Could not determine iscsi initiator name for instance %s" msgstr "" -#: nova/virt/baremetal/volume_driver.py:234 +#: nova/virt/baremetal/volume_driver.py:237 #, python-format msgid "No fixed PXE IP is associated to %s" msgstr "" -#: nova/virt/baremetal/volume_driver.py:267 +#: nova/virt/baremetal/volume_driver.py:270 #, python-format msgid "detach volume could not find tid for %s" msgstr "" -#: nova/virt/baremetal/db/sqlalchemy/api.py:253 +#: nova/virt/baremetal/db/sqlalchemy/api.py:164 +msgid "instance_uuid must be supplied to bm_node_set_uuid_safe" +msgstr "" + +#: nova/virt/baremetal/db/sqlalchemy/api.py:176 +#, python-format +msgid "Failed to associate instance %(uuid)s to baremetal node %(id)s." +msgstr "" + +#: nova/virt/baremetal/db/sqlalchemy/api.py:284 msgid "No more PXE IPs available" msgstr "" -#: nova/virt/baremetal/db/sqlalchemy/api.py:275 -#: nova/virt/baremetal/db/sqlalchemy/api.py:316 +#: nova/virt/baremetal/db/sqlalchemy/api.py:306 +#: nova/virt/baremetal/db/sqlalchemy/api.py:347 #, python-format msgid "Baremetal interface %s not found" msgstr "" -#: nova/virt/baremetal/db/sqlalchemy/api.py:326 +#: nova/virt/baremetal/db/sqlalchemy/api.py:357 #, python-format msgid "Baremetal interface %s already in use" msgstr "" -#: nova/virt/baremetal/db/sqlalchemy/api.py:340 +#: nova/virt/baremetal/db/sqlalchemy/api.py:371 #, python-format msgid "Baremetal virtual interface %s not found" msgstr "" -#: nova/virt/disk/api.py:220 +#: nova/virt/disk/api.py:127 +#, python-format +msgid "Checking if we can resize image %(image)s. size=%(size)s, CoW=%(use_cow)s" +msgstr "" + +#: nova/virt/disk/api.py:133 +#, python-format +msgid "Cannot resize filesystem %s to a smaller size." +msgstr "" + +#: nova/virt/disk/api.py:144 +#, python-format +msgid "Unable to mount image %(image)s with error %(error)s. Cannot resize." +msgstr "" + +#: nova/virt/disk/api.py:154 +#, python-format +msgid "" +"Unable to determine label for image %(image)s with error %(errror)s. " +"Cannot resize." +msgstr "" + +#: nova/virt/disk/api.py:234 msgid "image already mounted" msgstr "" -#: nova/virt/disk/api.py:266 +#: nova/virt/disk/api.py:279 #, python-format msgid "" "Inject data image=%(image)s key=%(key)s net=%(net)s metadata=%(metadata)s" @@ -5579,53 +5868,53 @@ msgid "" "partition=%(partition)s use_cow=%(use_cow)s" msgstr "" -#: nova/virt/disk/api.py:290 +#: nova/virt/disk/api.py:303 #, python-format msgid "" "Failed to mount container filesystem '%(image)s' on '%(target)s': " "%(errors)s" msgstr "" -#: nova/virt/disk/api.py:307 +#: nova/virt/disk/api.py:320 #, python-format msgid "Failed to unmount container filesystem: %s" msgstr "" -#: nova/virt/disk/api.py:329 +#: nova/virt/disk/api.py:342 #, python-format msgid "Inject file fs=%(fs)s path=%(path)s append=%(append)s" msgstr "" -#: nova/virt/disk/api.py:338 +#: nova/virt/disk/api.py:351 #, python-format msgid "Inject metadata fs=%(fs)s metadata=%(metadata)s" msgstr "" -#: nova/virt/disk/api.py:379 +#: nova/virt/disk/api.py:392 #, python-format msgid "Inject key fs=%(fs)s key=%(key)s" msgstr "" -#: nova/virt/disk/api.py:407 +#: nova/virt/disk/api.py:420 #, python-format msgid "Inject key fs=%(fs)s net=%(net)s" msgstr "" -#: nova/virt/disk/api.py:433 +#: nova/virt/disk/api.py:446 #, python-format msgid "Inject admin password fs=%(fs)s admin_passwd=ha-ha-not-telling-you" msgstr "" -#: nova/virt/disk/api.py:478 +#: nova/virt/disk/api.py:491 msgid "Not implemented on Windows" msgstr "" -#: nova/virt/disk/api.py:507 +#: nova/virt/disk/api.py:520 #, python-format msgid "User %(username)s not found in password file." msgstr "" -#: nova/virt/disk/api.py:523 +#: nova/virt/disk/api.py:536 #, python-format msgid "User %(username)s not found in shadow file." msgstr "" @@ -5827,66 +6116,71 @@ msgstr "" msgid "Mounting %(dev)s at %(dir)s" msgstr "" -#: nova/virt/disk/vfs/guestfs.py:93 +#: nova/virt/disk/vfs/guestfs.py:92 #, python-format msgid "Setting up appliance for %(imgfile)s %(imgfmt)s" msgstr "" -#: nova/virt/disk/vfs/guestfs.py:108 +#: nova/virt/disk/vfs/guestfs.py:106 +#, python-format +msgid "Error mounting %(imgfile)s with libguestfs (%(e)s)" +msgstr "" + +#: nova/virt/disk/vfs/guestfs.py:113 msgid "Tearing down appliance" msgstr "" -#: nova/virt/disk/vfs/guestfs.py:112 +#: nova/virt/disk/vfs/guestfs.py:117 #, python-format msgid "Failed to close augeas %s" msgstr "" -#: nova/virt/disk/vfs/guestfs.py:116 +#: nova/virt/disk/vfs/guestfs.py:121 #, python-format msgid "Failed to shutdown appliance %s" msgstr "" -#: nova/virt/disk/vfs/guestfs.py:120 +#: nova/virt/disk/vfs/guestfs.py:125 #, python-format msgid "Failed to close guest handle %s" msgstr "" -#: nova/virt/disk/vfs/guestfs.py:130 nova/virt/disk/vfs/localfs.py:103 +#: nova/virt/disk/vfs/guestfs.py:135 nova/virt/disk/vfs/localfs.py:102 #, python-format msgid "Make directory path=%(path)s" msgstr "" -#: nova/virt/disk/vfs/guestfs.py:135 nova/virt/disk/vfs/localfs.py:108 +#: nova/virt/disk/vfs/guestfs.py:140 nova/virt/disk/vfs/localfs.py:107 #, python-format msgid "Append file path=%(path)s" msgstr "" -#: nova/virt/disk/vfs/guestfs.py:140 nova/virt/disk/vfs/localfs.py:117 +#: nova/virt/disk/vfs/guestfs.py:145 nova/virt/disk/vfs/localfs.py:116 #, python-format msgid "Replace file path=%(path)s" msgstr "" -#: nova/virt/disk/vfs/guestfs.py:145 nova/virt/disk/vfs/localfs.py:126 +#: nova/virt/disk/vfs/guestfs.py:150 nova/virt/disk/vfs/localfs.py:125 #, python-format msgid "Read file path=%(path)s" msgstr "" -#: nova/virt/disk/vfs/guestfs.py:150 nova/virt/disk/vfs/localfs.py:132 +#: nova/virt/disk/vfs/guestfs.py:155 nova/virt/disk/vfs/localfs.py:131 #, python-format msgid "Has file path=%(path)s" msgstr "" -#: nova/virt/disk/vfs/guestfs.py:159 +#: nova/virt/disk/vfs/guestfs.py:164 #, python-format msgid "Set permissions path=%(path)s mode=%(mode)s" msgstr "" -#: nova/virt/disk/vfs/guestfs.py:164 +#: nova/virt/disk/vfs/guestfs.py:169 #, python-format msgid "Set ownership path=%(path)s user=%(user)s group=%(group)s" msgstr "" -#: nova/virt/disk/vfs/guestfs.py:177 +#: nova/virt/disk/vfs/guestfs.py:182 #, python-format msgid "chown uid=%(uid)d gid=%(gid)s" msgstr "" @@ -5896,32 +6190,27 @@ msgstr "" msgid "File path %s not valid" msgstr "" -#: nova/virt/disk/vfs/localfs.py:77 -#, python-format -msgid "Failed to mount image: %s" -msgstr "" - -#: nova/virt/disk/vfs/localfs.py:81 +#: nova/virt/disk/vfs/localfs.py:80 #, python-format msgid "Failed to mount image %(ex)s)" msgstr "" -#: nova/virt/disk/vfs/localfs.py:91 +#: nova/virt/disk/vfs/localfs.py:90 #, python-format msgid "Failed to unmount %(imgdir)s: %(ex)s" msgstr "" -#: nova/virt/disk/vfs/localfs.py:97 +#: nova/virt/disk/vfs/localfs.py:96 #, python-format msgid "Failed to remove %(imgdir)s: %(ex)s" msgstr "" -#: nova/virt/disk/vfs/localfs.py:140 +#: nova/virt/disk/vfs/localfs.py:139 #, python-format msgid "Set permissions path=%(path)s mode=%(mode)o" msgstr "" -#: nova/virt/disk/vfs/localfs.py:145 +#: nova/virt/disk/vfs/localfs.py:144 #, python-format msgid "Set permissions path=%(path)s user=%(user)s group=%(group)s" msgstr "" @@ -5930,7 +6219,7 @@ msgstr "" msgid "The ISCSI initiator name can't be found. Choosing the default one" msgstr "" -#: nova/virt/hyperv/basevolumeutils.py:79 nova/virt/libvirt/driver.py:1422 +#: nova/virt/hyperv/basevolumeutils.py:79 nova/virt/libvirt/driver.py:1421 #: nova/virt/xenapi/vm_utils.py:504 #, python-format msgid "block_device_list %s" @@ -5979,7 +6268,7 @@ msgstr "" msgid "get_available_resource called" msgstr "" -#: nova/virt/hyperv/hostops.py:163 nova/virt/libvirt/driver.py:3107 +#: nova/virt/hyperv/hostops.py:163 nova/virt/libvirt/driver.py:3105 #: nova/virt/xenapi/host.py:149 msgid "Updating host stats" msgstr "" @@ -6126,203 +6415,203 @@ msgstr "" msgid "get_info called for instance" msgstr "" -#: nova/virt/hyperv/vmops.py:104 +#: nova/virt/hyperv/vmops.py:103 #, python-format msgid "hyperv vm state: %s" msgstr "" -#: nova/virt/hyperv/vmops.py:110 +#: nova/virt/hyperv/vmops.py:109 #, python-format msgid "" "Got Info for vm %(instance_name)s: state=%(state)d, mem=%(memusage)s, " "num_cpu=%(numprocs)s, uptime=%(uptime)s" msgstr "" -#: nova/virt/hyperv/vmops.py:146 +#: nova/virt/hyperv/vmops.py:144 #, python-format msgid "cache image failed: %s" msgstr "" -#: nova/virt/hyperv/vmops.py:170 +#: nova/virt/hyperv/vmops.py:168 #, python-format msgid "Starting VM %s " msgstr "" -#: nova/virt/hyperv/vmops.py:172 +#: nova/virt/hyperv/vmops.py:170 #, python-format msgid "Started VM %s " msgstr "" -#: nova/virt/hyperv/vmops.py:174 +#: nova/virt/hyperv/vmops.py:172 #, python-format msgid "spawn vm failed: %s" msgstr "" -#: nova/virt/hyperv/vmops.py:180 +#: nova/virt/hyperv/vmops.py:178 #, python-format msgid "Invalid config_drive_format \"%s\"" msgstr "" -#: nova/virt/hyperv/vmops.py:183 nova/virt/libvirt/driver.py:1360 +#: nova/virt/hyperv/vmops.py:181 nova/virt/libvirt/driver.py:1362 msgid "Using config drive" msgstr "" -#: nova/virt/hyperv/vmops.py:194 nova/virt/libvirt/driver.py:1370 +#: nova/virt/hyperv/vmops.py:192 nova/virt/libvirt/driver.py:1371 #, python-format msgid "Creating config drive at %(path)s" msgstr "" -#: nova/virt/hyperv/vmops.py:201 nova/virt/libvirt/driver.py:1375 +#: nova/virt/hyperv/vmops.py:199 nova/virt/libvirt/driver.py:1377 #, python-format msgid "Creating config drive failed with error: %s" msgstr "" -#: nova/virt/hyperv/vmops.py:243 +#: nova/virt/hyperv/vmops.py:238 #, python-format msgid "Failed to create VM %s" msgstr "" -#: nova/virt/hyperv/vmops.py:246 +#: nova/virt/hyperv/vmops.py:241 #, python-format msgid "Created VM %s..." msgstr "" -#: nova/virt/hyperv/vmops.py:263 +#: nova/virt/hyperv/vmops.py:258 #, python-format msgid "Set memory for vm %s..." msgstr "" -#: nova/virt/hyperv/vmops.py:276 +#: nova/virt/hyperv/vmops.py:271 #, python-format msgid "Set vcpus for vm %s..." msgstr "" -#: nova/virt/hyperv/vmops.py:280 +#: nova/virt/hyperv/vmops.py:275 #, python-format msgid "Creating a scsi controller for %(vm_name)s for volume attaching" msgstr "" -#: nova/virt/hyperv/vmops.py:289 +#: nova/virt/hyperv/vmops.py:284 msgid "Controller not found" msgstr "" -#: nova/virt/hyperv/vmops.py:297 +#: nova/virt/hyperv/vmops.py:292 #, python-format msgid "Failed to add scsi controller to VM %s" msgstr "" -#: nova/virt/hyperv/vmops.py:314 +#: nova/virt/hyperv/vmops.py:309 #, python-format msgid "Creating disk for %(vm_name)s by attaching disk file %(path)s" msgstr "" -#: nova/virt/hyperv/vmops.py:342 +#: nova/virt/hyperv/vmops.py:337 #, python-format msgid "Failed to add drive to VM %s" msgstr "" -#: nova/virt/hyperv/vmops.py:345 +#: nova/virt/hyperv/vmops.py:340 #, python-format msgid "New %(drive_type)s drive path is %(drive_path)s" msgstr "" -#: nova/virt/hyperv/vmops.py:370 +#: nova/virt/hyperv/vmops.py:365 #, python-format msgid "Failed to add %(drive_type)s image to VM %(vm_name)s" msgstr "" -#: nova/virt/hyperv/vmops.py:372 +#: nova/virt/hyperv/vmops.py:367 #, python-format msgid "Created drive type %(drive_type)s for %(vm_name)s" msgstr "" -#: nova/virt/hyperv/vmops.py:377 +#: nova/virt/hyperv/vmops.py:372 #, python-format msgid "Creating nic for %s " msgstr "" -#: nova/virt/hyperv/vmops.py:382 +#: nova/virt/hyperv/vmops.py:377 msgid "Cannot find vSwitch" msgstr "" -#: nova/virt/hyperv/vmops.py:402 +#: nova/virt/hyperv/vmops.py:397 msgid "Failed creating a port on the external vswitch" msgstr "" -#: nova/virt/hyperv/vmops.py:403 +#: nova/virt/hyperv/vmops.py:398 #, python-format msgid "Failed creating port for %s" msgstr "" -#: nova/virt/hyperv/vmops.py:406 +#: nova/virt/hyperv/vmops.py:401 #, python-format msgid "Created switch port %(vm_name)s on switch %(ext_path)s" msgstr "" -#: nova/virt/hyperv/vmops.py:418 +#: nova/virt/hyperv/vmops.py:413 #, python-format msgid "Failed to add nic to VM %s" msgstr "" -#: nova/virt/hyperv/vmops.py:420 +#: nova/virt/hyperv/vmops.py:415 #, python-format msgid "Created nic for %s " msgstr "" -#: nova/virt/hyperv/vmops.py:427 nova/virt/hyperv/vmops.py:430 +#: nova/virt/hyperv/vmops.py:422 nova/virt/hyperv/vmops.py:425 #, python-format msgid "Attempting to bind NIC to %s " msgstr "" -#: nova/virt/hyperv/vmops.py:435 +#: nova/virt/hyperv/vmops.py:430 msgid "No vSwitch specified, attaching to default" msgstr "" -#: nova/virt/hyperv/vmops.py:460 +#: nova/virt/hyperv/vmops.py:453 #, python-format msgid "Got request to destroy vm %s" msgstr "" -#: nova/virt/hyperv/vmops.py:504 +#: nova/virt/hyperv/vmops.py:497 #, python-format -msgid "Del: disk %(vhdfile)s vm %(instance_name)s" +msgid "Del: disk %(vhdfile)s vm %(name)s" msgstr "" -#: nova/virt/hyperv/vmops.py:510 +#: nova/virt/hyperv/vmops.py:503 msgid "Pause instance" msgstr "" -#: nova/virt/hyperv/vmops.py:515 +#: nova/virt/hyperv/vmops.py:508 msgid "Unpause instance" msgstr "" -#: nova/virt/hyperv/vmops.py:521 +#: nova/virt/hyperv/vmops.py:514 msgid "Suspend instance" msgstr "" -#: nova/virt/hyperv/vmops.py:526 +#: nova/virt/hyperv/vmops.py:519 msgid "Resume instance" msgstr "" -#: nova/virt/hyperv/vmops.py:531 +#: nova/virt/hyperv/vmops.py:524 msgid "Power off instance" msgstr "" -#: nova/virt/hyperv/vmops.py:536 +#: nova/virt/hyperv/vmops.py:529 msgid "Power on instance" msgstr "" -#: nova/virt/hyperv/vmops.py:556 +#: nova/virt/hyperv/vmops.py:549 #, python-format msgid "Successfully changed vm state of %(vm_name)s to %(req_state)s" msgstr "" -#: nova/virt/hyperv/vmops.py:585 +#: nova/virt/hyperv/vmops.py:578 #, python-format msgid "use_cow_image:%s" msgstr "" -#: nova/virt/hyperv/vmops.py:605 +#: nova/virt/hyperv/vmops.py:598 #, python-format msgid "Failed to create Difference Disk from %(base)s to %(target)s" msgstr "" @@ -6379,7 +6668,7 @@ msgstr "" msgid "Unable to attach boot volume to instance %s" msgstr "" -#: nova/virt/hyperv/volumeops.py:130 nova/virt/xenapi/volumeops.py:114 +#: nova/virt/hyperv/volumeops.py:130 nova/virt/xenapi/volumeops.py:115 #, python-format msgid "Attach_volume: %(connection_info)s, %(instance_name)s, %(mountpoint)s" msgstr "" @@ -6389,7 +6678,7 @@ msgstr "" msgid "Attach volume failed: %s" msgstr "" -#: nova/virt/hyperv/volumeops.py:158 nova/virt/xenapi/volumeops.py:180 +#: nova/virt/hyperv/volumeops.py:158 nova/virt/xenapi/volumeops.py:182 #, python-format msgid "Unable to attach volume to instance %s" msgstr "" @@ -6619,150 +6908,150 @@ msgstr "" msgid "Instance rebooted successfully." msgstr "" -#: nova/virt/libvirt/driver.py:1037 +#: nova/virt/libvirt/driver.py:1039 msgid "Instance is running" msgstr "" -#: nova/virt/libvirt/driver.py:1044 nova/virt/powervm/operator.py:255 +#: nova/virt/libvirt/driver.py:1046 nova/virt/powervm/operator.py:255 msgid "Instance spawned successfully." msgstr "" -#: nova/virt/libvirt/driver.py:1060 +#: nova/virt/libvirt/driver.py:1062 #, python-format msgid "data: %(data)r, fpath: %(fpath)r" msgstr "" -#: nova/virt/libvirt/driver.py:1098 nova/virt/libvirt/driver.py:1124 +#: nova/virt/libvirt/driver.py:1100 nova/virt/libvirt/driver.py:1126 #, python-format msgid "Truncated console log returned, %d bytes ignored" msgstr "" -#: nova/virt/libvirt/driver.py:1113 +#: nova/virt/libvirt/driver.py:1115 msgid "Guest does not have a console available" msgstr "" -#: nova/virt/libvirt/driver.py:1162 +#: nova/virt/libvirt/driver.py:1164 #, python-format msgid "Path '%(path)s' supports direct I/O" msgstr "" -#: nova/virt/libvirt/driver.py:1166 +#: nova/virt/libvirt/driver.py:1168 #, python-format msgid "Path '%(path)s' does not support direct I/O: '%(ex)s'" msgstr "" -#: nova/virt/libvirt/driver.py:1170 nova/virt/libvirt/driver.py:1174 +#: nova/virt/libvirt/driver.py:1172 nova/virt/libvirt/driver.py:1176 #, python-format msgid "Error on '%(path)s' while checking direct I/O: '%(ex)s'" msgstr "" -#: nova/virt/libvirt/driver.py:1240 +#: nova/virt/libvirt/driver.py:1241 msgid "Creating image" msgstr "" -#: nova/virt/libvirt/driver.py:1390 +#: nova/virt/libvirt/driver.py:1389 #, python-format msgid "Injecting %(injection)s into image %(img_id)s" msgstr "" -#: nova/virt/libvirt/driver.py:1400 +#: nova/virt/libvirt/driver.py:1399 #, python-format msgid "Ignoring error injecting data into image %(img_id)s (%(e)s)" msgstr "" -#: nova/virt/libvirt/driver.py:1474 +#: nova/virt/libvirt/driver.py:1473 #, python-format msgid "" "Config requested an explicit CPU model, but the current libvirt " "hypervisor '%s' does not support selecting CPU models" msgstr "" -#: nova/virt/libvirt/driver.py:1480 +#: nova/virt/libvirt/driver.py:1479 msgid "Config requested a custom CPU model, but no model name was provided" msgstr "" -#: nova/virt/libvirt/driver.py:1484 +#: nova/virt/libvirt/driver.py:1483 msgid "A CPU model name should not be set when a host CPU model is requested" msgstr "" -#: nova/virt/libvirt/driver.py:1488 +#: nova/virt/libvirt/driver.py:1487 #, python-format msgid "CPU mode '%(mode)s' model '%(model)s' was chosen" msgstr "" -#: nova/virt/libvirt/driver.py:1504 +#: nova/virt/libvirt/driver.py:1503 msgid "" "Passthrough of the host CPU was requested but this libvirt version does " "not support this feature" msgstr "" -#: nova/virt/libvirt/driver.py:1821 +#: nova/virt/libvirt/driver.py:1819 msgid "Starting toXML method" msgstr "" -#: nova/virt/libvirt/driver.py:1825 +#: nova/virt/libvirt/driver.py:1823 msgid "Finished toXML method" msgstr "" -#: nova/virt/libvirt/driver.py:1842 +#: nova/virt/libvirt/driver.py:1840 #, python-format msgid "" "Error from libvirt while looking up %(instance_name)s: [Error Code " "%(error_code)s] %(ex)s" msgstr "" -#: nova/virt/libvirt/driver.py:1996 +#: nova/virt/libvirt/driver.py:1994 msgid "" "Cannot get the number of cpu, because this function is not implemented " "for this platform. " msgstr "" -#: nova/virt/libvirt/driver.py:2113 +#: nova/virt/libvirt/driver.py:2111 msgid "libvirt version is too old (does not support getVersion)" msgstr "" -#: nova/virt/libvirt/driver.py:2196 +#: nova/virt/libvirt/driver.py:2194 #, python-format msgid "Trying to get stats for the volume %s" msgstr "" -#: nova/virt/libvirt/driver.py:2220 +#: nova/virt/libvirt/driver.py:2218 #, python-format msgid "" "Getting block stats failed, device might have been detached. " "Code=%(errcode)s Error=%(e)s" msgstr "" -#: nova/virt/libvirt/driver.py:2224 +#: nova/virt/libvirt/driver.py:2222 #, python-format msgid "" "Could not find domain in libvirt for instance %s. Cannot get block stats " "for device" msgstr "" -#: nova/virt/libvirt/driver.py:2340 +#: nova/virt/libvirt/driver.py:2338 msgid "Block migration can not be used with shared storage." msgstr "" -#: nova/virt/libvirt/driver.py:2348 +#: nova/virt/libvirt/driver.py:2346 msgid "Live migration can not be used without shared storage." msgstr "" -#: nova/virt/libvirt/driver.py:2385 +#: nova/virt/libvirt/driver.py:2383 #, python-format msgid "" "Unable to migrate %(instance_uuid)s: Disk of instance is too " "large(available on destination host:%(available)s < need:%(necessary)s)" msgstr "" -#: nova/virt/libvirt/driver.py:2410 +#: nova/virt/libvirt/driver.py:2408 #, python-format msgid "" "Instance launched has CPU info:\n" "%s" msgstr "" -#: nova/virt/libvirt/driver.py:2422 +#: nova/virt/libvirt/driver.py:2420 #, python-format msgid "" "CPU doesn't have compatibility.\n" @@ -6772,51 +7061,51 @@ msgid "" "Refer to %(u)s" msgstr "" -#: nova/virt/libvirt/driver.py:2439 +#: nova/virt/libvirt/driver.py:2437 #, python-format msgid "" "Creating tmpfile %s to notify to other compute nodes that they should " "mount the same storage." msgstr "" -#: nova/virt/libvirt/driver.py:2487 +#: nova/virt/libvirt/driver.py:2485 #, python-format msgid "The firewall filter for %s does not exist" msgstr "" -#: nova/virt/libvirt/driver.py:2559 +#: nova/virt/libvirt/driver.py:2557 #, python-format msgid "Live Migration failure: %(e)s" msgstr "" -#: nova/virt/libvirt/driver.py:2649 +#: nova/virt/libvirt/driver.py:2647 #, python-format msgid "plug_vifs() failed %(cnt)d.Retry up to %(max_retry)d for %(hostname)s." msgstr "" -#: nova/virt/libvirt/driver.py:2764 +#: nova/virt/libvirt/driver.py:2762 #, python-format msgid "skipping %(path)s since it looks like volume" msgstr "" -#: nova/virt/libvirt/driver.py:2813 +#: nova/virt/libvirt/driver.py:2811 #, python-format msgid "Getting disk size of %(i_name)s: %(e)s" msgstr "" -#: nova/virt/libvirt/driver.py:2862 +#: nova/virt/libvirt/driver.py:2860 msgid "Starting migrate_disk_and_power_off" msgstr "" -#: nova/virt/libvirt/driver.py:2921 +#: nova/virt/libvirt/driver.py:2919 msgid "Instance running successfully." msgstr "" -#: nova/virt/libvirt/driver.py:2928 +#: nova/virt/libvirt/driver.py:2926 msgid "Starting finish_migration" msgstr "" -#: nova/virt/libvirt/driver.py:2979 +#: nova/virt/libvirt/driver.py:2977 msgid "Starting finish_revert_migration" msgstr "" @@ -7027,17 +7316,17 @@ msgstr "" msgid "Can't retrieve root device path from instance libvirt configuration" msgstr "" -#: nova/virt/libvirt/vif.py:111 +#: nova/virt/libvirt/vif.py:110 #, python-format msgid "Ensuring vlan %(vlan)s and bridge %(bridge)s" msgstr "" -#: nova/virt/libvirt/vif.py:121 +#: nova/virt/libvirt/vif.py:120 #, python-format msgid "Ensuring bridge %s" msgstr "" -#: nova/virt/libvirt/vif.py:198 nova/virt/libvirt/vif.py:269 +#: nova/virt/libvirt/vif.py:197 nova/virt/libvirt/vif.py:268 msgid "Failed while unplugging vif" msgstr "" @@ -7246,33 +7535,33 @@ msgstr "" msgid "PowerVM instance cleanup failed" msgstr "" -#: nova/virt/vmwareapi/driver.py:110 +#: nova/virt/vmwareapi/driver.py:107 msgid "" "Must specify vmwareapi_host_ip,vmwareapi_host_username and " "vmwareapi_host_password to usecompute_driver=vmwareapi.VMWareESXDriver" msgstr "" -#: nova/virt/vmwareapi/driver.py:261 +#: nova/virt/vmwareapi/driver.py:258 #, python-format msgid "In vmwareapi:_create_session, got this exception: %s" msgstr "" -#: nova/virt/vmwareapi/driver.py:344 +#: nova/virt/vmwareapi/driver.py:341 #, python-format msgid "In vmwareapi:_call_method, got this exception: %s" msgstr "" -#: nova/virt/vmwareapi/driver.py:379 +#: nova/virt/vmwareapi/driver.py:376 #, python-format msgid "Task [%(task_name)s] %(task_ref)s status: success" msgstr "" -#: nova/virt/vmwareapi/driver.py:384 +#: nova/virt/vmwareapi/driver.py:381 #, python-format msgid "Task [%(task_name)s] %(task_ref)s status: error %(error_info)s" msgstr "" -#: nova/virt/vmwareapi/driver.py:388 +#: nova/virt/vmwareapi/driver.py:385 #, python-format msgid "In vmwareapi:_poll_task, Got this error %s" msgstr "" @@ -7382,241 +7671,241 @@ msgstr "" msgid "Exception in %s " msgstr "" -#: nova/virt/vmwareapi/vmops.py:65 +#: nova/virt/vmwareapi/vmops.py:60 msgid "Getting list of instances" msgstr "" -#: nova/virt/vmwareapi/vmops.py:81 +#: nova/virt/vmwareapi/vmops.py:76 #, python-format msgid "Got total of %s instances" msgstr "" -#: nova/virt/vmwareapi/vmops.py:125 +#: nova/virt/vmwareapi/vmops.py:120 msgid "Couldn't get a local Datastore reference" msgstr "" -#: nova/virt/vmwareapi/vmops.py:195 +#: nova/virt/vmwareapi/vmops.py:190 msgid "Creating VM on the ESX host" msgstr "" -#: nova/virt/vmwareapi/vmops.py:203 +#: nova/virt/vmwareapi/vmops.py:198 msgid "Created VM on the ESX host" msgstr "" -#: nova/virt/vmwareapi/vmops.py:231 +#: nova/virt/vmwareapi/vmops.py:226 #, python-format msgid "" "Creating Virtual Disk of size %(vmdk_file_size_in_kb)s KB and adapter " "type %(adapter_type)s on the ESX host local store %(data_store_name)s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:249 +#: nova/virt/vmwareapi/vmops.py:244 #, python-format msgid "" "Created Virtual Disk of size %(vmdk_file_size_in_kb)s KB on the ESX host " "local store %(data_store_name)s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:259 +#: nova/virt/vmwareapi/vmops.py:254 #, python-format msgid "" "Deleting the file %(flat_uploaded_vmdk_path)s on the ESX host localstore " "%(data_store_name)s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:272 +#: nova/virt/vmwareapi/vmops.py:267 #, python-format msgid "" "Deleted the file %(flat_uploaded_vmdk_path)s on the ESX host local store " "%(data_store_name)s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:284 +#: nova/virt/vmwareapi/vmops.py:279 #, python-format msgid "" "Downloading image file data %(image_ref)s to the ESX data store " "%(data_store_name)s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:300 +#: nova/virt/vmwareapi/vmops.py:295 #, python-format msgid "" "Downloaded image file data %(image_ref)s to the ESX data store " "%(data_store_name)s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:318 +#: nova/virt/vmwareapi/vmops.py:313 msgid "Reconfiguring VM instance to attach the image disk" msgstr "" -#: nova/virt/vmwareapi/vmops.py:325 +#: nova/virt/vmwareapi/vmops.py:320 msgid "Reconfigured VM instance to attach the image disk" msgstr "" -#: nova/virt/vmwareapi/vmops.py:332 +#: nova/virt/vmwareapi/vmops.py:327 msgid "Powering on the VM instance" msgstr "" -#: nova/virt/vmwareapi/vmops.py:338 +#: nova/virt/vmwareapi/vmops.py:333 msgid "Powered on the VM instance" msgstr "" -#: nova/virt/vmwareapi/vmops.py:384 +#: nova/virt/vmwareapi/vmops.py:379 msgid "Creating Snapshot of the VM instance" msgstr "" -#: nova/virt/vmwareapi/vmops.py:394 +#: nova/virt/vmwareapi/vmops.py:389 msgid "Created Snapshot of the VM instance" msgstr "" -#: nova/virt/vmwareapi/vmops.py:437 +#: nova/virt/vmwareapi/vmops.py:432 msgid "Copying disk data before snapshot of the VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:450 +#: nova/virt/vmwareapi/vmops.py:445 msgid "Copied disk data before snapshot of the VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:459 +#: nova/virt/vmwareapi/vmops.py:454 #, python-format msgid "Uploading image %s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:473 +#: nova/virt/vmwareapi/vmops.py:468 #, python-format msgid "Uploaded image %s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:484 +#: nova/virt/vmwareapi/vmops.py:479 #, python-format msgid "Deleting temporary vmdk file %s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:493 +#: nova/virt/vmwareapi/vmops.py:488 #, python-format msgid "Deleted temporary vmdk file %s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:525 +#: nova/virt/vmwareapi/vmops.py:520 msgid "instance is not powered on" msgstr "" -#: nova/virt/vmwareapi/vmops.py:532 +#: nova/virt/vmwareapi/vmops.py:527 msgid "Rebooting guest OS of VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:535 +#: nova/virt/vmwareapi/vmops.py:530 msgid "Rebooted guest OS of VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:537 +#: nova/virt/vmwareapi/vmops.py:532 msgid "Doing hard reboot of VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:541 +#: nova/virt/vmwareapi/vmops.py:536 msgid "Did hard reboot of VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:553 +#: nova/virt/vmwareapi/vmops.py:548 msgid "instance not present" msgstr "" -#: nova/virt/vmwareapi/vmops.py:572 +#: nova/virt/vmwareapi/vmops.py:567 msgid "Powering off the VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:577 +#: nova/virt/vmwareapi/vmops.py:572 msgid "Powered off the VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:581 +#: nova/virt/vmwareapi/vmops.py:576 msgid "Unregistering the VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:584 +#: nova/virt/vmwareapi/vmops.py:579 msgid "Unregistered the VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:586 +#: nova/virt/vmwareapi/vmops.py:581 #, python-format msgid "" "In vmwareapi:vmops:destroy, got this exception while un-registering the " "VM: %s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:598 +#: nova/virt/vmwareapi/vmops.py:593 #, python-format msgid "Deleting contents of the VM from datastore %(datastore_name)s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:608 +#: nova/virt/vmwareapi/vmops.py:603 #, python-format msgid "Deleted contents of the VM from datastore %(datastore_name)s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:613 +#: nova/virt/vmwareapi/vmops.py:608 #, python-format msgid "" "In vmwareapi:vmops:destroy, got this exception while deleting the VM " "contents from the disk: %s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:622 +#: nova/virt/vmwareapi/vmops.py:617 msgid "pause not supported for vmwareapi" msgstr "" -#: nova/virt/vmwareapi/vmops.py:626 +#: nova/virt/vmwareapi/vmops.py:621 msgid "unpause not supported for vmwareapi" msgstr "" -#: nova/virt/vmwareapi/vmops.py:640 +#: nova/virt/vmwareapi/vmops.py:635 msgid "Suspending the VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:644 +#: nova/virt/vmwareapi/vmops.py:639 msgid "Suspended the VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:647 +#: nova/virt/vmwareapi/vmops.py:642 msgid "instance is powered off and can not be suspended." msgstr "" -#: nova/virt/vmwareapi/vmops.py:650 +#: nova/virt/vmwareapi/vmops.py:645 msgid "VM was already in suspended state. So returning without doing anything" msgstr "" -#: nova/virt/vmwareapi/vmops.py:663 +#: nova/virt/vmwareapi/vmops.py:658 msgid "Resuming the VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:668 +#: nova/virt/vmwareapi/vmops.py:663 msgid "Resumed the VM" msgstr "" -#: nova/virt/vmwareapi/vmops.py:670 +#: nova/virt/vmwareapi/vmops.py:665 msgid "instance is not in a suspended state" msgstr "" -#: nova/virt/vmwareapi/vmops.py:706 +#: nova/virt/vmwareapi/vmops.py:701 msgid "get_diagnostics not implemented for vmwareapi" msgstr "" -#: nova/virt/vmwareapi/vmops.py:764 +#: nova/virt/vmwareapi/vmops.py:759 #, python-format msgid "Reconfiguring VM instance to set the machine id with ip - %(ip_addr)s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:772 +#: nova/virt/vmwareapi/vmops.py:767 #, python-format msgid "Reconfigured VM instance to set the machine id with ip - %(ip_addr)s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:809 +#: nova/virt/vmwareapi/vmops.py:804 #, python-format msgid "Creating directory with path %s" msgstr "" -#: nova/virt/vmwareapi/vmops.py:813 +#: nova/virt/vmwareapi/vmops.py:808 #, python-format msgid "Created directory with path %s" msgstr "" @@ -7651,19 +7940,19 @@ msgstr "" msgid "Got image size of %(size)s for the image %(image)s" msgstr "" -#: nova/virt/xenapi/agent.py:84 nova/virt/xenapi/vmops.py:1455 +#: nova/virt/xenapi/agent.py:84 nova/virt/xenapi/vmops.py:1476 #, python-format msgid "TIMEOUT: The call to %(method)s timed out. args=%(args)r" msgstr "" -#: nova/virt/xenapi/agent.py:88 nova/virt/xenapi/vmops.py:1459 +#: nova/virt/xenapi/agent.py:88 nova/virt/xenapi/vmops.py:1480 #, python-format msgid "" "NOT IMPLEMENTED: The call to %(method)s is not supported by the agent. " "args=%(args)r" msgstr "" -#: nova/virt/xenapi/agent.py:93 nova/virt/xenapi/vmops.py:1464 +#: nova/virt/xenapi/agent.py:93 nova/virt/xenapi/vmops.py:1485 #, python-format msgid "The call to %(method)s returned an error: %(e)s. args=%(args)r" msgstr "" @@ -7763,30 +8052,30 @@ msgstr "" msgid "Failure while cleaning up attached VDIs" msgstr "" -#: nova/virt/xenapi/driver.py:354 +#: nova/virt/xenapi/driver.py:344 #, python-format msgid "Could not determine key: %s" msgstr "" -#: nova/virt/xenapi/driver.py:566 +#: nova/virt/xenapi/driver.py:556 msgid "Host startup on XenServer is not supported." msgstr "" -#: nova/virt/xenapi/driver.py:626 +#: nova/virt/xenapi/driver.py:616 msgid "Unable to log in to XenAPI (is the Dom0 disk full?)" msgstr "" -#: nova/virt/xenapi/driver.py:666 +#: nova/virt/xenapi/driver.py:656 msgid "Host is member of a pool, but DB says otherwise" msgstr "" -#: nova/virt/xenapi/driver.py:750 nova/virt/xenapi/driver.py:764 +#: nova/virt/xenapi/driver.py:740 nova/virt/xenapi/driver.py:754 #, python-format msgid "Got exception: %s" msgstr "" -#: nova/virt/xenapi/fake.py:680 nova/virt/xenapi/fake.py:782 -#: nova/virt/xenapi/fake.py:801 nova/virt/xenapi/fake.py:869 +#: nova/virt/xenapi/fake.py:680 nova/virt/xenapi/fake.py:784 +#: nova/virt/xenapi/fake.py:803 nova/virt/xenapi/fake.py:871 msgid "Raising NotImplemented" msgstr "" @@ -7810,7 +8099,7 @@ msgstr "" msgid "Calling setter %s" msgstr "" -#: nova/virt/xenapi/fake.py:784 +#: nova/virt/xenapi/fake.py:786 #, python-format msgid "" "xenapi.fake does not have an implementation for %s or it has been called " @@ -7910,12 +8199,12 @@ msgstr "" msgid "Pool-set_name_label failed: %(e)s" msgstr "" -#: nova/virt/xenapi/vif.py:103 +#: nova/virt/xenapi/vif.py:102 #, python-format msgid "Found no PIF for device %s" msgstr "" -#: nova/virt/xenapi/vif.py:122 +#: nova/virt/xenapi/vif.py:121 #, python-format msgid "" "PIF %(pif_rec['uuid'])s for network %(bridge)s has VLAN id %(pif_vlan)d. " @@ -8345,201 +8634,201 @@ msgstr "" msgid "This domU must be running on the host specified by xenapi_connection_url" msgstr "" -#: nova/virt/xenapi/vmops.py:124 nova/virt/xenapi/vmops.py:671 +#: nova/virt/xenapi/vmops.py:126 nova/virt/xenapi/vmops.py:692 #, python-format msgid "Updating progress to %(progress)d" msgstr "" -#: nova/virt/xenapi/vmops.py:165 +#: nova/virt/xenapi/vmops.py:168 msgid "Error: Agent is disabled" msgstr "" -#: nova/virt/xenapi/vmops.py:233 +#: nova/virt/xenapi/vmops.py:254 msgid "Starting instance" msgstr "" -#: nova/virt/xenapi/vmops.py:302 +#: nova/virt/xenapi/vmops.py:323 msgid "Removing kernel/ramdisk files from dom0" msgstr "" -#: nova/virt/xenapi/vmops.py:374 +#: nova/virt/xenapi/vmops.py:395 #, python-format msgid "Block device information present: %s" msgstr "" -#: nova/virt/xenapi/vmops.py:405 +#: nova/virt/xenapi/vmops.py:426 msgid "Failed to spawn, rolling back" msgstr "" -#: nova/virt/xenapi/vmops.py:478 +#: nova/virt/xenapi/vmops.py:499 msgid "Detected ISO image type, creating blank VM for install" msgstr "" -#: nova/virt/xenapi/vmops.py:495 +#: nova/virt/xenapi/vmops.py:516 msgid "Auto configuring disk, attempting to resize partition..." msgstr "" -#: nova/virt/xenapi/vmops.py:521 +#: nova/virt/xenapi/vmops.py:542 msgid "Starting VM" msgstr "" -#: nova/virt/xenapi/vmops.py:527 +#: nova/virt/xenapi/vmops.py:548 msgid "Waiting for instance state to become running" msgstr "" -#: nova/virt/xenapi/vmops.py:541 +#: nova/virt/xenapi/vmops.py:562 #, python-format msgid "" "Latest agent build for %(hypervisor)s/%(os)s/%(architecture)s is " "%(version)s" msgstr "" -#: nova/virt/xenapi/vmops.py:544 +#: nova/virt/xenapi/vmops.py:565 #, python-format msgid "No agent build found for %(hypervisor)s/%(os)s/%(architecture)s" msgstr "" -#: nova/virt/xenapi/vmops.py:555 +#: nova/virt/xenapi/vmops.py:576 #, python-format msgid "Instance agent version: %s" msgstr "" -#: nova/virt/xenapi/vmops.py:582 +#: nova/virt/xenapi/vmops.py:603 msgid "Setting VCPU weight" msgstr "" -#: nova/virt/xenapi/vmops.py:590 +#: nova/virt/xenapi/vmops.py:611 #, python-format msgid "Could not find VM with name %s" msgstr "" -#: nova/virt/xenapi/vmops.py:640 +#: nova/virt/xenapi/vmops.py:661 msgid "Finished snapshot and upload for VM" msgstr "" -#: nova/virt/xenapi/vmops.py:644 +#: nova/virt/xenapi/vmops.py:665 #, python-format msgid "Migrating VHD '%(vdi_uuid)s' with seq_num %(seq_num)d" msgstr "" -#: nova/virt/xenapi/vmops.py:652 +#: nova/virt/xenapi/vmops.py:673 msgid "Failed to transfer vhd to new host" msgstr "" -#: nova/virt/xenapi/vmops.py:689 +#: nova/virt/xenapi/vmops.py:710 #, python-format msgid "Resizing down VDI %(vdi_uuid)s from %(old_gb)dGB to %(new_gb)dGB" msgstr "" -#: nova/virt/xenapi/vmops.py:695 nova/virt/xenapi/vmops.py:745 +#: nova/virt/xenapi/vmops.py:716 nova/virt/xenapi/vmops.py:766 msgid "Clean shutdown did not complete successfully, trying hard shutdown." msgstr "" -#: nova/virt/xenapi/vmops.py:774 +#: nova/virt/xenapi/vmops.py:795 msgid "Resize down not allowed without auto_disk_config" msgstr "" -#: nova/virt/xenapi/vmops.py:819 +#: nova/virt/xenapi/vmops.py:840 #, python-format msgid "Resizing up VDI %(vdi_uuid)s from %(old_gb)dGB to %(new_gb)dGB" msgstr "" -#: nova/virt/xenapi/vmops.py:824 +#: nova/virt/xenapi/vmops.py:845 msgid "Resize complete" msgstr "" -#: nova/virt/xenapi/vmops.py:868 +#: nova/virt/xenapi/vmops.py:889 msgid "Starting halted instance found during reboot" msgstr "" -#: nova/virt/xenapi/vmops.py:959 +#: nova/virt/xenapi/vmops.py:980 msgid "Unable to find root VBD/VDI for VM" msgstr "" -#: nova/virt/xenapi/vmops.py:985 +#: nova/virt/xenapi/vmops.py:1006 msgid "Destroying VDIs" msgstr "" -#: nova/virt/xenapi/vmops.py:1012 +#: nova/virt/xenapi/vmops.py:1033 msgid "Using RAW or VHD, skipping kernel and ramdisk deletion" msgstr "" -#: nova/virt/xenapi/vmops.py:1019 +#: nova/virt/xenapi/vmops.py:1040 msgid "instance has a kernel or ramdisk but not both" msgstr "" -#: nova/virt/xenapi/vmops.py:1026 +#: nova/virt/xenapi/vmops.py:1047 msgid "kernel/ramdisk files removed" msgstr "" -#: nova/virt/xenapi/vmops.py:1052 +#: nova/virt/xenapi/vmops.py:1073 msgid "Destroying VM" msgstr "" -#: nova/virt/xenapi/vmops.py:1078 +#: nova/virt/xenapi/vmops.py:1099 msgid "VM is not present, skipping destroy..." msgstr "" -#: nova/virt/xenapi/vmops.py:1129 +#: nova/virt/xenapi/vmops.py:1150 #, python-format msgid "Instance is already in Rescue Mode: %s" msgstr "" -#: nova/virt/xenapi/vmops.py:1163 +#: nova/virt/xenapi/vmops.py:1184 msgid "VM is not present, skipping soft delete..." msgstr "" -#: nova/virt/xenapi/vmops.py:1212 +#: nova/virt/xenapi/vmops.py:1233 #, python-format msgid "Found %(instance_count)d hung reboots older than %(timeout)d seconds" msgstr "" -#: nova/virt/xenapi/vmops.py:1216 +#: nova/virt/xenapi/vmops.py:1237 msgid "Automatically hard rebooting" msgstr "" -#: nova/virt/xenapi/vmops.py:1276 +#: nova/virt/xenapi/vmops.py:1297 msgid "Fetching VM ref while BUILDING failed" msgstr "" -#: nova/virt/xenapi/vmops.py:1359 +#: nova/virt/xenapi/vmops.py:1380 msgid "Injecting network info to xenstore" msgstr "" -#: nova/virt/xenapi/vmops.py:1378 +#: nova/virt/xenapi/vmops.py:1399 msgid "Creating vifs" msgstr "" -#: nova/virt/xenapi/vmops.py:1387 +#: nova/virt/xenapi/vmops.py:1408 #, python-format msgid "Creating VIF for network %(network_ref)s" msgstr "" -#: nova/virt/xenapi/vmops.py:1390 +#: nova/virt/xenapi/vmops.py:1411 #, python-format msgid "Created VIF %(vif_ref)s, network %(network_ref)s" msgstr "" -#: nova/virt/xenapi/vmops.py:1418 +#: nova/virt/xenapi/vmops.py:1439 msgid "Injecting hostname to xenstore" msgstr "" -#: nova/virt/xenapi/vmops.py:1514 +#: nova/virt/xenapi/vmops.py:1535 #, python-format msgid "" "Destination host:%(hostname)s must be in the same aggregate as the source" " server" msgstr "" -#: nova/virt/xenapi/vmops.py:1546 +#: nova/virt/xenapi/vmops.py:1567 msgid "Migrate Receive failed" msgstr "" -#: nova/virt/xenapi/vmops.py:1594 +#: nova/virt/xenapi/vmops.py:1615 msgid "VM.assert_can_migratefailed" msgstr "" -#: nova/virt/xenapi/vmops.py:1630 +#: nova/virt/xenapi/vmops.py:1651 msgid "Migrate Send failed" msgstr "" @@ -8571,7 +8860,7 @@ msgid "introducing sr within volume_utils" msgstr "" #: nova/virt/xenapi/volume_utils.py:93 nova/virt/xenapi/volume_utils.py:160 -#: nova/virt/xenapi/volumeops.py:140 +#: nova/virt/xenapi/volumeops.py:141 #, python-format msgid "Introduced %(label)s as %(sr_ref)s." msgstr "" @@ -8584,7 +8873,7 @@ msgstr "" msgid "Plugging SR" msgstr "" -#: nova/virt/xenapi/volume_utils.py:106 nova/virt/xenapi/volumeops.py:144 +#: nova/virt/xenapi/volume_utils.py:106 nova/virt/xenapi/volumeops.py:145 msgid "Unable to introduce Storage Repository" msgstr "" @@ -8705,47 +8994,47 @@ msgstr "" msgid "Could not forget SR" msgstr "" -#: nova/virt/xenapi/volumeops.py:127 +#: nova/virt/xenapi/volumeops.py:128 #, python-format msgid "Mountpoint %(mountpoint)s attached to instance %(instance_name)s" msgstr "" -#: nova/virt/xenapi/volumeops.py:162 +#: nova/virt/xenapi/volumeops.py:163 #, python-format msgid "Unable to create VDI on SR %(sr_ref)s for instance %(instance_name)s" msgstr "" -#: nova/virt/xenapi/volumeops.py:172 +#: nova/virt/xenapi/volumeops.py:173 #, python-format msgid "Unable to use SR %(sr_ref)s for instance %(instance_name)s" msgstr "" -#: nova/virt/xenapi/volumeops.py:189 +#: nova/virt/xenapi/volumeops.py:191 #, python-format msgid "Detach_volume: %(instance_name)s, %(mountpoint)s" msgstr "" -#: nova/virt/xenapi/volumeops.py:197 +#: nova/virt/xenapi/volumeops.py:199 #, python-format msgid "Unable to locate volume %s" msgstr "" -#: nova/virt/xenapi/volumeops.py:206 +#: nova/virt/xenapi/volumeops.py:208 #, python-format msgid "Unable to detach volume %s" msgstr "" -#: nova/virt/xenapi/volumeops.py:211 +#: nova/virt/xenapi/volumeops.py:213 #, python-format msgid "Unable to destroy vbd %s" msgstr "" -#: nova/virt/xenapi/volumeops.py:218 +#: nova/virt/xenapi/volumeops.py:220 #, python-format msgid "Error purging SR %s" msgstr "" -#: nova/virt/xenapi/volumeops.py:220 +#: nova/virt/xenapi/volumeops.py:222 #, python-format msgid "Mountpoint %(mountpoint)s detached from instance %(instance_name)s" msgstr "" @@ -8785,7 +9074,7 @@ msgstr "" msgid "Starting nova-xvpvncproxy node (version %s)" msgstr "" -#: nova/volume/cinder.py:69 +#: nova/volume/cinder.py:70 #, python-format msgid "Cinderclient connection created using URL: %s" msgstr "" diff --git a/nova/manager.py b/nova/manager.py index e7130fb4a..636424d1c 100644 --- a/nova/manager.py +++ b/nova/manager.py @@ -54,8 +54,10 @@ This module provides Manager, a base class for managers. """ import eventlet +import time from nova.db import base +from nova import exception from nova.openstack.common import cfg from nova.openstack.common import log as logging from nova.openstack.common.plugin import pluginmanager @@ -63,25 +65,50 @@ from nova.openstack.common.rpc import dispatcher as rpc_dispatcher from nova.scheduler import rpcapi as scheduler_rpcapi from nova import version + +periodic_opts = [ + cfg.BoolOpt('run_external_periodic_tasks', + default=True, + help=('Some periodic tasks can be run in a separate process. ' + 'Should we run them here?')), + ] + CONF = cfg.CONF +CONF.register_opts(periodic_opts) CONF.import_opt('host', 'nova.config') LOG = logging.getLogger(__name__) +DEFAULT_INTERVAL = 60.0 + def periodic_task(*args, **kwargs): """Decorator to indicate that a method is a periodic task. This decorator can be used in two ways: - 1. Without arguments '@periodic_task', this will be run on every tick + 1. Without arguments '@periodic_task', this will be run on every cycle of the periodic scheduler. - 2. With arguments, @periodic_task(ticks_between_runs=N), this will be - run on every N ticks of the periodic scheduler. + 2. With arguments, @periodic_task(periodic_spacing=N), this will be + run on approximately every N seconds. If this number is negative the + periodic task will be disabled. """ def decorator(f): + # Test for old style invocation + if 'ticks_between_runs' in kwargs: + raise exception.InvalidPeriodicTaskArg(arg='ticks_between_runs') + + # Control if run at all f._periodic_task = True - f._ticks_between_runs = kwargs.pop('ticks_between_runs', 0) + f._periodic_external_ok = kwargs.pop('external_process_ok', False) + if f._periodic_external_ok and not CONF.run_external_periodic_tasks: + f._periodic_enabled = False + else: + f._periodic_enabled = kwargs.pop('enabled', True) + + # Control frequency + f._periodic_spacing = kwargs.pop('spacing', 0) + f._periodic_last_run = time.time() return f # NOTE(sirp): The `if` is necessary to allow the decorator to be used with @@ -117,17 +144,39 @@ class ManagerMeta(type): cls._periodic_tasks = [] try: - cls._ticks_to_skip = cls._ticks_to_skip.copy() + cls._periodic_last_run = cls._periodic_last_run.copy() + except AttributeError: + cls._periodic_last_run = {} + + try: + cls._periodic_spacing = cls._periodic_spacing.copy() except AttributeError: - cls._ticks_to_skip = {} + cls._periodic_spacing = {} for value in cls.__dict__.values(): if getattr(value, '_periodic_task', False): task = value name = task.__name__ - if task._ticks_between_runs >= 0: - cls._periodic_tasks.append((name, task)) - cls._ticks_to_skip[name] = task._ticks_between_runs + + if task._periodic_spacing < 0: + LOG.info(_('Skipping periodic task %(task)s because ' + 'its interval is negative'), + {'task': name}) + continue + if not task._periodic_enabled: + LOG.info(_('Skipping periodic task %(task)s because ' + 'it is disabled'), + {'task': name}) + continue + + # A periodic spacing of zero indicates that this task should + # be run every pass + if task._periodic_spacing == 0: + task._periodic_spacing = None + + cls._periodic_tasks.append((name, task)) + cls._periodic_spacing[name] = task._periodic_spacing + cls._periodic_last_run[name] = task._periodic_last_run class Manager(base.Base): @@ -158,30 +207,39 @@ class Manager(base.Base): def periodic_tasks(self, context, raise_on_error=False): """Tasks to be run at a periodic interval.""" + idle_for = DEFAULT_INTERVAL for task_name, task in self._periodic_tasks: full_task_name = '.'.join([self.__class__.__name__, task_name]) - ticks_to_skip = self._ticks_to_skip[task_name] - if ticks_to_skip > 0: - LOG.debug(_("Skipping %(full_task_name)s, %(ticks_to_skip)s" - " ticks left until next run"), locals()) - self._ticks_to_skip[task_name] -= 1 - continue + # If a periodic task is _nearly_ due, then we'll run it early + if self._periodic_spacing[task_name] is None: + wait = 0 + else: + wait = time.time() - (self._periodic_last_run[task_name] + + self._periodic_spacing[task_name]) + if wait > 0.2: + if wait < idle_for: + idle_for = wait + continue - self._ticks_to_skip[task_name] = task._ticks_between_runs LOG.debug(_("Running periodic task %(full_task_name)s"), locals()) + self._periodic_last_run[task_name] = time.time() try: task(self, context) - # NOTE(tiantian): After finished a task, allow manager to - # do other work (report_state, processing AMPQ request etc.) - eventlet.sleep(0) except Exception as e: if raise_on_error: raise LOG.exception(_("Error during %(full_task_name)s: %(e)s"), locals()) + if (not self._periodic_spacing[task_name] is None and + self._periodic_spacing[task_name] < idle_for): + idle_for = self._periodic_spacing[task_name] + eventlet.sleep(0) + + return idle_for + def init_host(self): """Hook to do additional manager initialization when one requests the service be started. This is called before any service record diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index a1c03bc51..e803488d2 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -95,7 +95,7 @@ linux_net_opts = [ CONF = cfg.CONF CONF.register_opts(linux_net_opts) CONF.import_opt('bindir', 'nova.config') -CONF.import_opt('fake_network', 'nova.config') +CONF.import_opt('fake_network', 'nova.network.manager') CONF.import_opt('host', 'nova.config') CONF.import_opt('use_ipv6', 'nova.config') CONF.import_opt('my_ip', 'nova.config') diff --git a/nova/network/manager.py b/nova/network/manager.py index 0e8530d14..e263ac730 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -150,6 +150,9 @@ network_opts = [ cfg.StrOpt('network_host', default=socket.getfqdn(), help='Network host to use for ip allocation in flat modes'), + cfg.BoolOpt('fake_network', + default=False, + help='If passed, use fake network devices and addresses'), cfg.BoolOpt('fake_call', default=False, help='If True, skip using the queue and make local calls'), @@ -172,8 +175,8 @@ network_opts = [ 'entries in multi host mode'), cfg.IntOpt("dns_update_periodic_interval", default=-1, - help='Number of periodic scheduler ticks to wait between ' - 'runs of updates to DNS entries.'), + help='Number of seconds to wait between runs of updates to DNS ' + 'entries.'), cfg.StrOpt('dhcp_domain', default='novalocal', help='domain to use for building the hostnames'), @@ -193,7 +196,6 @@ network_opts = [ CONF = cfg.CONF CONF.register_opts(network_opts) -CONF.import_opt('fake_network', 'nova.config') CONF.import_opt('use_ipv6', 'nova.config') CONF.import_opt('my_ip', 'nova.config') @@ -575,36 +577,43 @@ class FloatingIP(object): else: # send to correct host self.network_rpcapi._associate_floating_ip(context, - floating_address, fixed_address, interface, host) + floating_address, fixed_address, interface, host, + fixed_ip['instance_uuid']) return orig_instance_uuid def _associate_floating_ip(self, context, floating_address, fixed_address, interface, instance_uuid): """Performs db and driver calls to associate floating ip & fixed ip""" - # associate floating ip - self.db.floating_ip_fixed_ip_associate(context, - floating_address, - fixed_address, - self.host) - try: - # gogo driver time - self.l3driver.add_floating_ip(floating_address, fixed_address, - interface) - except exception.ProcessExecutionError as e: - fixed_address = self.db.floating_ip_disassociate(context, - floating_address) - if "Cannot find device" in str(e): - LOG.error(_('Interface %(interface)s not found'), locals()) - raise exception.NoFloatingIpInterface(interface=interface) - - payload = dict(project_id=context.project_id, - instance_id=instance_uuid, - floating_ip=floating_address) - notifier.notify(context, - notifier.publisher_id("network"), - 'network.floating_ip.associate', - notifier.INFO, payload=payload) + + @lockutils.synchronized(unicode(floating_address), 'nova-') + def do_associate(): + # associate floating ip + res = self.db.floating_ip_fixed_ip_associate(context, + floating_address, + fixed_address, + self.host) + if not res: + # NOTE(vish): ip was already associated + return + try: + # gogo driver time + self.l3driver.add_floating_ip(floating_address, fixed_address, + interface) + except exception.ProcessExecutionError as e: + self.db.floating_ip_disassociate(context, floating_address) + if "Cannot find device" in str(e): + LOG.error(_('Interface %(interface)s not found'), locals()) + raise exception.NoFloatingIpInterface(interface=interface) + + payload = dict(project_id=context.project_id, + instance_id=instance_uuid, + floating_ip=floating_address) + notifier.notify(context, + notifier.publisher_id("network"), + 'network.floating_ip.associate', + notifier.INFO, payload=payload) + do_associate() @rpc_common.client_exceptions(exception.FloatingIpNotFoundForAddress) @wrap_check_policy @@ -658,24 +667,39 @@ class FloatingIP(object): else: # send to correct host self.network_rpcapi._disassociate_floating_ip(context, address, - interface, host) + interface, host, fixed_ip['instance_uuid']) def _disassociate_floating_ip(self, context, address, interface, instance_uuid): """Performs db and driver calls to disassociate floating ip""" # disassociate floating ip - fixed_address = self.db.floating_ip_disassociate(context, address) - - if interface: - # go go driver time - self.l3driver.remove_floating_ip(address, fixed_address, interface) - payload = dict(project_id=context.project_id, - instance_id=instance_uuid, - floating_ip=address) - notifier.notify(context, - notifier.publisher_id("network"), - 'network.floating_ip.disassociate', - notifier.INFO, payload=payload) + + @lockutils.synchronized(unicode(address), 'nova-') + def do_disassociate(): + # NOTE(vish): Note that we are disassociating in the db before we + # actually remove the ip address on the host. We are + # safe from races on this host due to the decorator, + # but another host might grab the ip right away. We + # don't worry about this case because the miniscule + # window where the ip is on both hosts shouldn't cause + # any problems. + fixed_address = self.db.floating_ip_disassociate(context, address) + + if not fixed_address: + # NOTE(vish): ip was already disassociated + return + if interface: + # go go driver time + self.l3driver.remove_floating_ip(address, fixed_address, + interface) + payload = dict(project_id=context.project_id, + instance_id=instance_uuid, + floating_ip=address) + notifier.notify(context, + notifier.publisher_id("network"), + 'network.floating_ip.disassociate', + notifier.INFO, payload=payload) + do_disassociate() @rpc_common.client_exceptions(exception.FloatingIpNotFound) @wrap_check_policy @@ -900,7 +924,7 @@ class NetworkManager(manager.SchedulerDependentManager): The one at a time part is to flatten the layout to help scale """ - RPC_API_VERSION = '1.5' + RPC_API_VERSION = '1.6' # If True, this manager requires VIF to create a bridge. SHOULD_CREATE_BRIDGE = False @@ -1951,7 +1975,7 @@ class NetworkManager(manager.SchedulerDependentManager): mac_address) @manager.periodic_task( - ticks_between_runs=CONF.dns_update_periodic_interval) + spacing=CONF.dns_update_periodic_interval) def _periodic_update_dns(self, context): """Update local DNS entries of all networks on this host""" networks = self.db.network_get_all_by_host(context, self.host) diff --git a/nova/network/quantumv2/__init__.py b/nova/network/quantumv2/__init__.py index ff96edf30..914600ed8 100644 --- a/nova/network/quantumv2/__init__.py +++ b/nova/network/quantumv2/__init__.py @@ -15,7 +15,6 @@ # License for the specific language governing permissions and limitations # under the License. -from nova import exception from nova.openstack.common import cfg from nova.openstack.common import excutils from nova.openstack.common import log as logging diff --git a/nova/network/quantumv2/api.py b/nova/network/quantumv2/api.py index 3a20e1ed0..88431b91d 100644 --- a/nova/network/quantumv2/api.py +++ b/nova/network/quantumv2/api.py @@ -136,7 +136,8 @@ class API(base.Base): touched_port_ids.append(port['id']) else: if fixed_ips.get(network_id): - port_req_body['port']['fixed_ip'] = fixed_ip + port_req_body['port']['fixed_ips'] = [{'ip_address': + fixed_ip}] port_req_body['port']['network_id'] = network_id port_req_body['port']['admin_state_up'] = True port_req_body['port']['tenant_id'] = instance['project_id'] diff --git a/nova/network/rpcapi.py b/nova/network/rpcapi.py index 8ee1ce443..4f8ebeb22 100644 --- a/nova/network/rpcapi.py +++ b/nova/network/rpcapi.py @@ -38,6 +38,7 @@ class NetworkAPI(rpc_proxy.RpcProxy): 1.3 - Adds fanout cast update_dns for multi_host networks 1.4 - Add get_backdoor_port() 1.5 - Adds associate + 1.6 - Adds instance_uuid to _{dis,}associate_floating_ip ''' # @@ -259,20 +260,24 @@ class NetworkAPI(rpc_proxy.RpcProxy): # 1.0 API was being documented using this client proxy class. It should be # changed if there was ever a 2.0. def _associate_floating_ip(self, ctxt, floating_address, fixed_address, - interface, host): + interface, host, instance_uuid=None): return self.call(ctxt, self.make_msg('_associate_floating_ip', floating_address=floating_address, fixed_address=fixed_address, - interface=interface), - topic=rpc.queue_get_for(ctxt, self.topic, host)) + interface=interface, instance_uuid=instance_uuid), + topic=rpc.queue_get_for(ctxt, self.topic, host), + version='1.6') # NOTE(russellb): Ideally this would not have a prefix of '_' since it is # a part of the rpc API. However, this is how it was being called when the # 1.0 API was being documented using this client proxy class. It should be # changed if there was ever a 2.0. - def _disassociate_floating_ip(self, ctxt, address, interface, host): + def _disassociate_floating_ip(self, ctxt, address, interface, host, + instance_uuid=None): return self.call(ctxt, self.make_msg('_disassociate_floating_ip', - address=address, interface=interface), - topic=rpc.queue_get_for(ctxt, self.topic, host)) + address=address, interface=interface, + instance_uuid=instance_uuid), + topic=rpc.queue_get_for(ctxt, self.topic, host), + version='1.6') def lease_fixed_ip(self, ctxt, address, host): self.cast(ctxt, self.make_msg('lease_fixed_ip', address=address), diff --git a/nova/openstack/common/rpc/__init__.py b/nova/openstack/common/rpc/__init__.py index a223e8fde..cfdac03bd 100644 --- a/nova/openstack/common/rpc/__init__.py +++ b/nova/openstack/common/rpc/__init__.py @@ -57,19 +57,19 @@ rpc_opts = [ cfg.BoolOpt('fake_rabbit', default=False, help='If passed, use a fake RabbitMQ provider'), - # - # The following options are not registered here, but are expected to be - # present. The project using this library must register these options with - # the configuration so that project-specific defaults may be defined. - # - #cfg.StrOpt('control_exchange', - # default='nova', - # help='AMQP exchange to connect to if using RabbitMQ or Qpid'), + cfg.StrOpt('control_exchange', + default='openstack', + help='AMQP exchange to connect to if using RabbitMQ or Qpid'), ] cfg.CONF.register_opts(rpc_opts) +def set_defaults(control_exchange): + cfg.set_defaults(rpc_opts, + control_exchange=control_exchange) + + def create_connection(new=True): """Create a connection to the message bus used for rpc. diff --git a/nova/openstack/common/rpc/amqp.py b/nova/openstack/common/rpc/amqp.py index a5a79cc30..6464914db 100644 --- a/nova/openstack/common/rpc/amqp.py +++ b/nova/openstack/common/rpc/amqp.py @@ -428,7 +428,4 @@ def cleanup(connection_pool): def get_control_exchange(conf): - try: - return conf.control_exchange - except cfg.NoSuchOptError: - return 'openstack' + return conf.control_exchange diff --git a/nova/openstack/common/timeutils.py b/nova/openstack/common/timeutils.py index 86004391d..f433b718b 100644 --- a/nova/openstack/common/timeutils.py +++ b/nova/openstack/common/timeutils.py @@ -71,11 +71,15 @@ def normalize_time(timestamp): def is_older_than(before, seconds): """Return True if before is older than seconds.""" + if isinstance(before, str): + before = parse_strtime(before).replace(tzinfo=None) return utcnow() - before > datetime.timedelta(seconds=seconds) def is_newer_than(after, seconds): """Return True if after is newer than seconds.""" + if isinstance(after, str): + after = parse_strtime(after).replace(tzinfo=None) return after - utcnow() > datetime.timedelta(seconds=seconds) diff --git a/nova/scheduler/filter_scheduler.py b/nova/scheduler/filter_scheduler.py index ea9a39b6f..07a3f578a 100644 --- a/nova/scheduler/filter_scheduler.py +++ b/nova/scheduler/filter_scheduler.py @@ -165,7 +165,7 @@ class FilterScheduler(driver.Scheduler): if not retry: return hosts = retry['hosts'] - hosts.append((host, node)) + hosts.append([host, node]) def _add_oversubscription_policy(self, filter_properties, host_state): filter_properties['limits'] = host_state.limits diff --git a/nova/scheduler/filters/retry_filter.py b/nova/scheduler/filters/retry_filter.py index 108e4d206..91d2cb2a2 100644 --- a/nova/scheduler/filters/retry_filter.py +++ b/nova/scheduler/filters/retry_filter.py @@ -33,10 +33,13 @@ class RetryFilter(filters.BaseHostFilter): return True hosts = retry.get('hosts', []) - host = (host_state.host, host_state.nodename) + host = [host_state.host, host_state.nodename] - LOG.debug(_("Previously tried hosts: %(hosts)s. (host=%(host)s)") % - locals()) + passes = host not in hosts + pass_msg = "passes" if passes else "fails" + + LOG.debug(_("Host %(host)s %(pass_msg)s. Previously tried hosts: " + "%(hosts)s") % locals()) # Host passes if it's not in the list of previously attempted hosts: - return host not in hosts + return passes diff --git a/nova/scheduler/host_manager.py b/nova/scheduler/host_manager.py index 0c64d7aa2..d5b8aeb52 100644 --- a/nova/scheduler/host_manager.py +++ b/nova/scheduler/host_manager.py @@ -140,7 +140,8 @@ class HostState(object): def update_from_compute_node(self, compute): """Update information about a host from its compute_node info.""" - if self.updated and self.updated > compute['updated_at']: + if (self.updated and compute['updated_at'] + and self.updated > compute['updated_at']): return all_ram_mb = compute['memory_mb'] diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 535eb7797..f3eb6e2e8 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -54,7 +54,7 @@ QUOTAS = quota.QUOTAS class SchedulerManager(manager.Manager): """Chooses a host to run instances on.""" - RPC_API_VERSION = '2.4' + RPC_API_VERSION = '2.5' def __init__(self, scheduler_driver=None, *args, **kwargs): if not scheduler_driver: @@ -263,3 +263,6 @@ class SchedulerManager(manager.Manager): @manager.periodic_task def _expire_reservations(self, context): QUOTAS.expire(context) + + def get_backdoor_port(self, context): + return self.backdoor_port diff --git a/nova/scheduler/rpcapi.py b/nova/scheduler/rpcapi.py index 4bc6e0e45..6ae4adcae 100644 --- a/nova/scheduler/rpcapi.py +++ b/nova/scheduler/rpcapi.py @@ -49,6 +49,7 @@ class SchedulerAPI(nova.openstack.common.rpc.proxy.RpcProxy): 2.3 - Remove create_volume() 2.4 - Change update_service_capabilities() - accepts a list of capabilities + 2.5 - Add get_backdoor_port() ''' # @@ -106,3 +107,7 @@ class SchedulerAPI(nova.openstack.common.rpc.proxy.RpcProxy): service_name=service_name, host=host, capabilities=capabilities), version='2.4') + + def get_backdoor_port(self, context, host): + return self.call(context, self.make_msg('get_backdoor_port'), + version='2.5') diff --git a/nova/service.py b/nova/service.py index 4c93fefa8..fc0ac4a1b 100644 --- a/nova/service.py +++ b/nova/service.py @@ -49,9 +49,9 @@ service_opts = [ cfg.IntOpt('report_interval', default=10, help='seconds between nodes reporting state to datastore'), - cfg.IntOpt('periodic_interval', - default=60, - help='seconds between running periodic tasks'), + cfg.BoolOpt('periodic_enable', + default=True, + help='enable periodic tasks'), cfg.IntOpt('periodic_fuzzy_delay', default=60, help='range of seconds to randomly delay when starting the' @@ -371,7 +371,8 @@ class Service(object): it state to the database services table.""" def __init__(self, host, binary, topic, manager, report_interval=None, - periodic_interval=None, periodic_fuzzy_delay=None, + periodic_enable=None, periodic_fuzzy_delay=None, + periodic_interval_max=None, *args, **kwargs): self.host = host self.binary = binary @@ -380,8 +381,9 @@ class Service(object): manager_class = importutils.import_class(self.manager_class_name) self.manager = manager_class(host=self.host, *args, **kwargs) self.report_interval = report_interval - self.periodic_interval = periodic_interval + self.periodic_enable = periodic_enable self.periodic_fuzzy_delay = periodic_fuzzy_delay + self.periodic_interval_max = periodic_interval_max self.saved_args, self.saved_kwargs = args, kwargs self.timers = [] self.backdoor_port = None @@ -433,15 +435,15 @@ class Service(object): if pulse: self.timers.append(pulse) - if self.periodic_interval: + if self.periodic_enable: if self.periodic_fuzzy_delay: initial_delay = random.randint(0, self.periodic_fuzzy_delay) else: initial_delay = None - periodic = utils.LoopingCall(self.periodic_tasks) - periodic.start(interval=self.periodic_interval, - initial_delay=initial_delay) + periodic = utils.DynamicLoopingCall(self.periodic_tasks) + periodic.start(initial_delay=initial_delay, + periodic_interval_max=self.periodic_interval_max) self.timers.append(periodic) def _create_service_ref(self, context): @@ -460,8 +462,8 @@ class Service(object): @classmethod def create(cls, host=None, binary=None, topic=None, manager=None, - report_interval=None, periodic_interval=None, - periodic_fuzzy_delay=None): + report_interval=None, periodic_enable=None, + periodic_fuzzy_delay=None, periodic_interval_max=None): """Instantiates class and passes back application object. :param host: defaults to CONF.host @@ -469,8 +471,9 @@ class Service(object): :param topic: defaults to bin_name - 'nova-' part :param manager: defaults to CONF.<topic>_manager :param report_interval: defaults to CONF.report_interval - :param periodic_interval: defaults to CONF.periodic_interval + :param periodic_enable: defaults to CONF.periodic_enable :param periodic_fuzzy_delay: defaults to CONF.periodic_fuzzy_delay + :param periodic_interval_max: if set, the max time to wait between runs """ if not host: @@ -486,14 +489,15 @@ class Service(object): manager = CONF.get(manager_cls, None) if report_interval is None: report_interval = CONF.report_interval - if periodic_interval is None: - periodic_interval = CONF.periodic_interval + if periodic_enable is None: + periodic_enable = CONF.periodic_enable if periodic_fuzzy_delay is None: periodic_fuzzy_delay = CONF.periodic_fuzzy_delay service_obj = cls(host, binary, topic, manager, report_interval=report_interval, - periodic_interval=periodic_interval, - periodic_fuzzy_delay=periodic_fuzzy_delay) + periodic_enable=periodic_enable, + periodic_fuzzy_delay=periodic_fuzzy_delay, + periodic_interval_max=periodic_interval_max) return service_obj @@ -529,7 +533,7 @@ class Service(object): def periodic_tasks(self, raise_on_error=False): """Tasks to be run at a periodic interval.""" ctxt = context.get_admin_context() - self.manager.periodic_tasks(ctxt, raise_on_error=raise_on_error) + return self.manager.periodic_tasks(ctxt, raise_on_error=raise_on_error) class WSGIService(object): diff --git a/nova/servicegroup/db_driver.py b/nova/servicegroup/db_driver.py index a4481406c..a52ed258c 100644 --- a/nova/servicegroup/db_driver.py +++ b/nova/servicegroup/db_driver.py @@ -41,7 +41,7 @@ class DbDriver(api.ServiceGroupDriver): ' ServiceGroup driver')) report_interval = service.report_interval if report_interval: - pulse = utils.LoopingCall(self._report_state, service) + pulse = utils.FixedIntervalLoopingCall(self._report_state, service) pulse.start(interval=report_interval, initial_delay=report_interval) return pulse diff --git a/nova/test.py b/nova/test.py index d9f68cc76..fd9c4a522 100644 --- a/nova/test.py +++ b/nova/test.py @@ -191,6 +191,8 @@ class TestCase(testtools.TestCase): def setUp(self): """Run before each test method to initialize test environment.""" super(TestCase, self).setUp() + # Give each test a maximum of one minute to run. + self.useFixture(fixtures.Timeout(60, gentle=True)) self.useFixture(fixtures.NestedTempfile()) self.useFixture(fixtures.TempHomeDir()) @@ -220,6 +222,7 @@ class TestCase(testtools.TestCase): self.addCleanup(self._clear_attrs) self.useFixture(fixtures.EnvironmentVariable('http_proxy')) self.policy = self.useFixture(policy_fixture.PolicyFixture()) + CONF.set_override('fatal_exception_format_errors', True) def _clear_attrs(self): # Delete attributes that don't start with _ so they don't pin diff --git a/nova/tests/api/ec2/test_cinder_cloud.py b/nova/tests/api/ec2/test_cinder_cloud.py index 3c119a2c2..61402ce0e 100644 --- a/nova/tests/api/ec2/test_cinder_cloud.py +++ b/nova/tests/api/ec2/test_cinder_cloud.py @@ -672,12 +672,12 @@ class CinderCloudTestCase(test.TestCase): instance_id = rv['instancesSet'][0]['instanceId'] return instance_id - def _restart_compute_service(self, periodic_interval=None): + def _restart_compute_service(self, periodic_interval_max=None): """restart compute service. NOTE: fake driver forgets all instances.""" self.compute.kill() - if periodic_interval: + if periodic_interval_max: self.compute = self.start_service( - 'compute', periodic_interval=periodic_interval) + 'compute', periodic_interval_max=periodic_interval_max) else: self.compute = self.start_service('compute') @@ -716,7 +716,7 @@ class CinderCloudTestCase(test.TestCase): vol1_uuid = ec2utils.ec2_vol_id_to_uuid(vol1['volumeId']) vol2_uuid = ec2utils.ec2_vol_id_to_uuid(vol2['volumeId']) # enforce periodic tasks run in short time to avoid wait for 60s. - self._restart_compute_service(periodic_interval=0.3) + self._restart_compute_service(periodic_interval_max=0.3) kwargs = {'image_id': 'ami-1', 'instance_type': CONF.default_instance_type, @@ -799,7 +799,7 @@ class CinderCloudTestCase(test.TestCase): vol2_uuid = ec2utils.ec2_vol_id_to_uuid(vol2['volumeId']) # enforce periodic tasks run in short time to avoid wait for 60s. - self._restart_compute_service(periodic_interval=0.3) + self._restart_compute_service(periodic_interval_max=0.3) kwargs = {'image_id': 'ami-1', 'instance_type': CONF.default_instance_type, 'max_count': 1, @@ -936,7 +936,7 @@ class CinderCloudTestCase(test.TestCase): def test_create_image(self): """Make sure that CreateImage works""" # enforce periodic tasks run in short time to avoid wait for 60s. - self._restart_compute_service(periodic_interval=0.3) + self._restart_compute_service(periodic_interval_max=0.3) (volumes, snapshots) = self._setUpImageSet( create_volumes_and_snapshots=True) diff --git a/nova/tests/api/ec2/test_cloud.py b/nova/tests/api/ec2/test_cloud.py index 831143326..284298585 100644 --- a/nova/tests/api/ec2/test_cloud.py +++ b/nova/tests/api/ec2/test_cloud.py @@ -781,11 +781,30 @@ class CloudTestCase(test.TestCase): self.assertEqual(instance['privateIpAddress'], '192.168.0.3') self.assertEqual(instance['dnsNameV6'], 'fe80:b33f::a8bb:ccff:fedd:eeff') + + # A filter with even one invalid id should cause an exception to be + # raised + self.assertRaises(exception.InstanceNotFound, + self.cloud.describe_instances, self.context, + instance_id=[instance_id, '435679']) + db.instance_destroy(self.context, inst1['uuid']) db.instance_destroy(self.context, inst2['uuid']) db.service_destroy(self.context, comp1['id']) db.service_destroy(self.context, comp2['id']) + def test_describe_instances_all_invalid(self): + """Makes sure describe_instances works and filters results.""" + self.flags(use_ipv6=True) + + self._stub_instance_get_with_fixed_ips('get_all') + self._stub_instance_get_with_fixed_ips('get') + + instance_id = ec2utils.id_to_ec2_inst_id('435679') + self.assertRaises(exception.InstanceNotFound, + self.cloud.describe_instances, self.context, + instance_id=[instance_id]) + def test_describe_instances_sorting(self): """Makes sure describe_instances works and is sorted as expected.""" self.flags(use_ipv6=True) @@ -1600,19 +1619,19 @@ class CloudTestCase(test.TestCase): result = run_instances(self.context, **kwargs) self.assertEqual(len(result['instancesSet']), 1) - def _restart_compute_service(self, periodic_interval=None): + def _restart_compute_service(self, periodic_interval_max=None): """restart compute service. NOTE: fake driver forgets all instances.""" self.compute.kill() - if periodic_interval: + if periodic_interval_max: self.compute = self.start_service( - 'compute', periodic_interval=periodic_interval) + 'compute', periodic_interval_max=periodic_interval_max) else: self.compute = self.start_service('compute') def test_stop_start_instance(self): """Makes sure stop/start instance works""" # enforce periodic tasks run in short time to avoid wait for 60s. - self._restart_compute_service(periodic_interval=0.3) + self._restart_compute_service(periodic_interval_max=0.3) kwargs = {'image_id': 'ami-1', 'instance_type': CONF.default_instance_type, @@ -1815,7 +1834,7 @@ class CloudTestCase(test.TestCase): def _do_test_create_image(self, no_reboot): """Make sure that CreateImage works""" # enforce periodic tasks run in short time to avoid wait for 60s. - self._restart_compute_service(periodic_interval=0.3) + self._restart_compute_service(periodic_interval_max=0.3) (volumes, snapshots) = self._setUpImageSet( create_volumes_and_snapshots=True) @@ -1923,7 +1942,7 @@ class CloudTestCase(test.TestCase): instance """ # enforce periodic tasks run in short time to avoid wait for 60s. - self._restart_compute_service(periodic_interval=0.3) + self._restart_compute_service(periodic_interval_max=0.3) (volumes, snapshots) = self._setUpImageSet( create_volumes_and_snapshots=True) @@ -2125,7 +2144,6 @@ class CloudTestCase(test.TestCase): def fake_show(self, context, id_): LOG.debug("id_ %s", id_) - print id_ prop = {} if id_ == 'ami-3': diff --git a/nova/tests/api/openstack/compute/contrib/test_admin_actions.py b/nova/tests/api/openstack/compute/contrib/test_admin_actions.py index c121e3afb..99f00e07c 100644 --- a/nova/tests/api/openstack/compute/contrib/test_admin_actions.py +++ b/nova/tests/api/openstack/compute/contrib/test_admin_actions.py @@ -54,7 +54,9 @@ def fake_compute_api(*args, **kwargs): def fake_compute_api_raises_invalid_state(*args, **kwargs): - raise exception.InstanceInvalidState + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') def fake_compute_api_get(self, context, instance_id): @@ -124,7 +126,7 @@ class AdminActionsTest(test.TestCase): req.content_type = 'application/json' res = req.get_response(app) self.assertEqual(res.status_int, 409) - self.assertIn("invalid state for '%(_action)s'" % locals(), + self.assertIn("Cannot \'%(_action)s\' while instance" % locals(), res.body) def test_migrate_live_enabled(self): @@ -345,7 +347,7 @@ class ResetStateTests(test.TestCase): def fake_get(inst, context, instance_id): if self.exists: return dict(id=1, uuid=instance_id, vm_state=vm_states.ACTIVE) - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id=instance_id) def fake_update(inst, context, instance, **kwargs): self.kwargs = kwargs diff --git a/nova/tests/api/openstack/compute/contrib/test_aggregates.py b/nova/tests/api/openstack/compute/contrib/test_aggregates.py index 41a87ac6a..0f60b8128 100644 --- a/nova/tests/api/openstack/compute/contrib/test_aggregates.py +++ b/nova/tests/api/openstack/compute/contrib/test_aggregates.py @@ -78,7 +78,7 @@ class AggregateTestCase(test.TestCase): def test_create_with_duplicate_aggregate_name(self): def stub_create_aggregate(context, name, availability_zone): - raise exception.AggregateNameExists + raise exception.AggregateNameExists(aggregate_name=name) self.stubs.Set(self.controller.api, "create_aggregate", stub_create_aggregate) @@ -232,7 +232,8 @@ class AggregateTestCase(test.TestCase): def test_add_host_with_already_added_host(self): def stub_add_host_to_aggregate(context, aggregate, host): - raise exception.AggregateHostExists() + raise exception.AggregateHostExists(aggregate_id=aggregate, + host=host) self.stubs.Set(self.controller.api, "add_host_to_aggregate", stub_add_host_to_aggregate) @@ -242,7 +243,7 @@ class AggregateTestCase(test.TestCase): def test_add_host_with_bad_aggregate(self): def stub_add_host_to_aggregate(context, aggregate, host): - raise exception.AggregateNotFound() + raise exception.AggregateNotFound(aggregate_id=aggregate) self.stubs.Set(self.controller.api, "add_host_to_aggregate", stub_add_host_to_aggregate) @@ -252,7 +253,7 @@ class AggregateTestCase(test.TestCase): def test_add_host_with_bad_host(self): def stub_add_host_to_aggregate(context, aggregate, host): - raise exception.ComputeHostNotFound() + raise exception.ComputeHostNotFound(host=host) self.stubs.Set(self.controller.api, "add_host_to_aggregate", stub_add_host_to_aggregate) @@ -262,7 +263,9 @@ class AggregateTestCase(test.TestCase): def test_add_host_with_host_in_wrong_availability_zone(self): def stub_add_host_to_aggregate(context, aggregate, host): - raise exception.InvalidAggregateAction() + raise exception.InvalidAggregateAction(action='create_aggregate', + aggregate_id="'N/A'", + reason='wrong zone') self.stubs.Set(self.controller.api, "add_host_to_aggregate", stub_add_host_to_aggregate) @@ -290,7 +293,7 @@ class AggregateTestCase(test.TestCase): def test_remove_host_with_bad_aggregate(self): def stub_remove_host_from_aggregate(context, aggregate, host): - raise exception.AggregateNotFound() + raise exception.AggregateNotFound(aggregate_id=aggregate) self.stubs.Set(self.controller.api, "remove_host_from_aggregate", stub_remove_host_from_aggregate) @@ -301,7 +304,8 @@ class AggregateTestCase(test.TestCase): def test_remove_host_with_bad_host(self): def stub_remove_host_from_aggregate(context, aggregate, host): - raise exception.AggregateHostNotFound() + raise exception.AggregateHostNotFound(aggregate_id=aggregate, + host=host) self.stubs.Set(self.controller.api, "remove_host_from_aggregate", stub_remove_host_from_aggregate) @@ -339,7 +343,7 @@ class AggregateTestCase(test.TestCase): body = {"set_metadata": {"metadata": {"foo": "bar"}}} def stub_update_aggregate(context, aggregate, metadata): - raise exception.AggregateNotFound() + raise exception.AggregateNotFound(aggregate_id=aggregate) self.stubs.Set(self.controller.api, "update_aggregate_metadata", stub_update_aggregate) @@ -370,7 +374,7 @@ class AggregateTestCase(test.TestCase): def test_delete_aggregate_with_bad_aggregate(self): def stub_delete_aggregate(context, aggregate): - raise exception.AggregateNotFound() + raise exception.AggregateNotFound(aggregate_id=aggregate) self.stubs.Set(self.controller.api, "delete_aggregate", stub_delete_aggregate) diff --git a/nova/tests/api/openstack/compute/contrib/test_coverage_ext.py b/nova/tests/api/openstack/compute/contrib/test_coverage_ext.py index b8df3a118..39a883049 100644 --- a/nova/tests/api/openstack/compute/contrib/test_coverage_ext.py +++ b/nova/tests/api/openstack/compute/contrib/test_coverage_ext.py @@ -88,6 +88,8 @@ class CoverageExtensionTest(test.TestCase): res = req.get_response(fakes.wsgi_app( fake_auth_context=self.admin_context)) self.assertEqual(res.status_int, 200) + resp_dict = jsonutils.loads(res.body) + self.assertTrue('path' in resp_dict) def test_report_coverage_action_file(self): self.stubs.Set(coverage_ext.CoverageController, @@ -178,7 +180,7 @@ class CoverageExtensionTest(test.TestCase): self.assertEqual(res.status_int, 404) def test_report_coverage_action_nostart(self): - body = {'stop': {}} + body = {'report': {}} req = webob.Request.blank('/v2/fake/os-coverage/action') req.method = "POST" req.body = jsonutils.dumps(body) diff --git a/nova/tests/api/openstack/compute/contrib/test_deferred_delete.py b/nova/tests/api/openstack/compute/contrib/test_deferred_delete.py index e7da8f191..eba4154e2 100644 --- a/nova/tests/api/openstack/compute/contrib/test_deferred_delete.py +++ b/nova/tests/api/openstack/compute/contrib/test_deferred_delete.py @@ -61,9 +61,13 @@ class DeferredDeleteExtensionTest(test.TestCase): compute_api.API.get(self.fake_context, self.fake_uuid).AndReturn( fake_instance) + + exc = exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + compute_api.API.force_delete(self.fake_context, fake_instance)\ - .AndRaise( - exception.InstanceInvalidState) + .AndRaise(exc) self.mox.ReplayAll() self.assertRaises(webob.exc.HTTPConflict, @@ -90,11 +94,14 @@ class DeferredDeleteExtensionTest(test.TestCase): self.mox.StubOutWithMock(compute_api.API, 'restore') fake_instance = 'fake_instance' + exc = exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') compute_api.API.get(self.fake_context, self.fake_uuid).AndReturn( fake_instance) compute_api.API.restore(self.fake_context, fake_instance).AndRaise( - exception.InstanceInvalidState) + exc) self.mox.ReplayAll() self.assertRaises(webob.exc.HTTPConflict, self.extension._restore, diff --git a/nova/tests/api/openstack/compute/contrib/test_extended_server_attributes.py b/nova/tests/api/openstack/compute/contrib/test_extended_server_attributes.py index 036c240d4..63e1b6126 100644 --- a/nova/tests/api/openstack/compute/contrib/test_extended_server_attributes.py +++ b/nova/tests/api/openstack/compute/contrib/test_extended_server_attributes.py @@ -100,7 +100,7 @@ class ExtendedServerAttributesTest(test.TestCase): def test_no_instance_passthrough_404(self): def fake_compute_get(*args, **kwargs): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id='fake') self.stubs.Set(compute.api.API, 'get', fake_compute_get) url = '/v2/fake/servers/70f6db34-de8d-4fbd-aafb-4065bdfa6115' diff --git a/nova/tests/api/openstack/compute/contrib/test_extended_status.py b/nova/tests/api/openstack/compute/contrib/test_extended_status.py index f9d4cb43a..e368c5986 100644 --- a/nova/tests/api/openstack/compute/contrib/test_extended_status.py +++ b/nova/tests/api/openstack/compute/contrib/test_extended_status.py @@ -98,7 +98,7 @@ class ExtendedStatusTest(test.TestCase): def test_no_instance_passthrough_404(self): def fake_compute_get(*args, **kwargs): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id='fake') self.stubs.Set(compute.api.API, 'get', fake_compute_get) url = '/v2/fake/servers/70f6db34-de8d-4fbd-aafb-4065bdfa6115' diff --git a/nova/tests/api/openstack/compute/contrib/test_flavor_access.py b/nova/tests/api/openstack/compute/contrib/test_flavor_access.py index 0bf1f1b66..0818dfdd3 100644 --- a/nova/tests/api/openstack/compute/contrib/test_flavor_access.py +++ b/nova/tests/api/openstack/compute/contrib/test_flavor_access.py @@ -226,7 +226,8 @@ class FlavorAccessTest(test.TestCase): def test_add_tenant_access_with_already_added_access(self): def stub_add_instance_type_access(flavorid, projectid, ctxt=None): - raise exception.FlavorAccessExists() + raise exception.FlavorAccessExists(flavor_id=flavorid, + project_id=projectid) self.stubs.Set(instance_types, 'add_instance_type_access', stub_add_instance_type_access) body = {'addTenantAccess': {'tenant': 'proj2'}} @@ -238,22 +239,8 @@ class FlavorAccessTest(test.TestCase): def test_remove_tenant_access_with_bad_access(self): def stub_remove_instance_type_access(flavorid, projectid, ctxt=None): - self.assertEqual('3', flavorid, "flavorid") - self.assertEqual("proj2", projectid, "projectid") - expected = {'flavor_access': [ - {'flavor_id': '3', 'tenant_id': 'proj3'}]} - self.stubs.Set(instance_types, 'remove_instance_type_access', - stub_remove_instance_type_access) - body = {'removeTenantAccess': {'tenant': 'proj2'}} - req = fakes.HTTPRequest.blank('/v2/fake/flavors/2/action', - use_admin_context=True) - result = self.flavor_action_controller.\ - _addTenantAccess(req, '3', body) - self.assertEqual(result, expected) - - def test_remove_tenant_access_with_bad_access(self): - def stub_remove_instance_type_access(flavorid, projectid, ctxt=None): - raise exception.FlavorAccessNotFound() + raise exception.FlavorAccessNotFound(flavor_id=flavorid, + project_id=projectid) self.stubs.Set(instance_types, 'remove_instance_type_access', stub_remove_instance_type_access) body = {'removeTenantAccess': {'tenant': 'proj2'}} diff --git a/nova/tests/api/openstack/compute/contrib/test_flavor_manage.py b/nova/tests/api/openstack/compute/contrib/test_flavor_manage.py index 3df9f956b..9b58e7b74 100644 --- a/nova/tests/api/openstack/compute/contrib/test_flavor_manage.py +++ b/nova/tests/api/openstack/compute/contrib/test_flavor_manage.py @@ -208,7 +208,7 @@ class FlavorManageTest(test.TestCase): def fake_create(name, memory_mb, vcpus, root_gb, ephemeral_gb, flavorid, swap, rxtx_factor, is_public): - raise exception.InstanceTypeExists() + raise exception.InstanceTypeExists(name=name) self.stubs.Set(instance_types, "create", fake_create) url = '/v2/fake/flavors' diff --git a/nova/tests/api/openstack/compute/contrib/test_floating_ips.py b/nova/tests/api/openstack/compute/contrib/test_floating_ips.py index d67682a4f..a72430fd9 100644 --- a/nova/tests/api/openstack/compute/contrib/test_floating_ips.py +++ b/nova/tests/api/openstack/compute/contrib/test_floating_ips.py @@ -229,7 +229,7 @@ class FloatingIpTest(test.TestCase): def test_floating_ip_show_not_found(self): def fake_get_floating_ip(*args, **kwargs): - raise exception.FloatingIpNotFound() + raise exception.FloatingIpNotFound(id='fake') self.stubs.Set(network.api.API, "get_floating_ip", fake_get_floating_ip) @@ -379,7 +379,8 @@ class FloatingIpTest(test.TestCase): fixed_address=None): floating_ips = ["10.10.10.10", "10.10.10.11"] if floating_address not in floating_ips: - raise exception.FloatingIpNotFoundForAddress() + raise exception.FloatingIpNotFoundForAddress( + address=flaoting_address) self.stubs.Set(network.api.API, "associate_floating_ip", fake_network_api_associate) @@ -395,7 +396,8 @@ class FloatingIpTest(test.TestCase): floating_address): floating_ips = ["10.10.10.10", "10.10.10.11"] if floating_address not in floating_ips: - raise exception.FloatingIpNotFoundForAddress() + raise exception.FloatingIpNotFoundForAddress( + address=floating_address) self.stubs.Set(network.api.API, "get_floating_ip_by_address", network_api_get_floating_ip_by_address) diff --git a/nova/tests/api/openstack/compute/contrib/test_hide_server_addresses.py b/nova/tests/api/openstack/compute/contrib/test_hide_server_addresses.py index 7991bc27f..804decdff 100644 --- a/nova/tests/api/openstack/compute/contrib/test_hide_server_addresses.py +++ b/nova/tests/api/openstack/compute/contrib/test_hide_server_addresses.py @@ -124,7 +124,7 @@ class HideServerAddressesTest(test.TestCase): def test_no_instance_passthrough_404(self): def fake_compute_get(*args, **kwargs): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id='fake') self.stubs.Set(compute.api.API, 'get', fake_compute_get) res = self._make_request('/v2/fake/servers/' + fakes.get_fake_uuid()) diff --git a/nova/tests/api/openstack/compute/contrib/test_hypervisors.py b/nova/tests/api/openstack/compute/contrib/test_hypervisors.py index 740477ca3..4e4d214cc 100644 --- a/nova/tests/api/openstack/compute/contrib/test_hypervisors.py +++ b/nova/tests/api/openstack/compute/contrib/test_hypervisors.py @@ -91,7 +91,7 @@ def fake_compute_node_get(context, compute_id): for hyper in TEST_HYPERS: if hyper['id'] == compute_id: return hyper - raise exception.ComputeHostNotFound + raise exception.ComputeHostNotFound(host=compute_id) def fake_compute_node_statistics(context): diff --git a/nova/tests/api/openstack/compute/contrib/test_networks.py b/nova/tests/api/openstack/compute/contrib/test_networks.py index bab6cef68..5cd522f72 100644 --- a/nova/tests/api/openstack/compute/contrib/test_networks.py +++ b/nova/tests/api/openstack/compute/contrib/test_networks.py @@ -103,14 +103,14 @@ class FakeNetworkAPI(object): if network['id'] == network_id: del self.networks[0] return True - raise exception.NetworkNotFoundForUUID() + raise exception.NetworkNotFoundForUUID(uuid=network_id) def disassociate(self, context, network_uuid): for network in self.networks: if network.get('uuid') == network_uuid: network['project_id'] = None return True - raise exception.NetworkNotFound() + raise exception.NetworkNotFound(network_id=network_uuid) def associate(self, context, network_uuid, host=_sentinel, project=_sentinel): @@ -121,7 +121,7 @@ class FakeNetworkAPI(object): if project is not FakeNetworkAPI._sentinel: network['project_id'] = project return True - raise exception.NetworkNotFound() + raise exception.NetworkNotFound(network_id=network_uuid) def add_network_to_project(self, context, project_id, network_uuid=None): @@ -143,7 +143,7 @@ class FakeNetworkAPI(object): for network in self.networks: if network.get('uuid') == network_id: return network - raise exception.NetworkNotFound() + raise exception.NetworkNotFound(network_id=network_id) def create(self, context, **kwargs): subnet_bits = int(math.ceil(math.log(kwargs.get( diff --git a/nova/tests/api/openstack/compute/contrib/test_security_groups.py b/nova/tests/api/openstack/compute/contrib/test_security_groups.py index 2e5a22835..ccb58f858 100644 --- a/nova/tests/api/openstack/compute/contrib/test_security_groups.py +++ b/nova/tests/api/openstack/compute/contrib/test_security_groups.py @@ -1370,7 +1370,7 @@ class SecurityGroupsOutputTest(test.TestCase): def test_no_instance_passthrough_404(self): def fake_compute_get(*args, **kwargs): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id='fake') self.stubs.Set(compute.api.API, 'get', fake_compute_get) url = '/v2/fake/servers/70f6db34-de8d-4fbd-aafb-4065bdfa6115' diff --git a/nova/tests/api/openstack/compute/contrib/test_volumes.py b/nova/tests/api/openstack/compute/contrib/test_volumes.py index e8a315edd..3119f55e8 100644 --- a/nova/tests/api/openstack/compute/contrib/test_volumes.py +++ b/nova/tests/api/openstack/compute/contrib/test_volumes.py @@ -289,6 +289,27 @@ class VolumeAttachTests(test.TestCase): self.assertEqual(result['volumeAttachment']['id'], '00000000-aaaa-aaaa-aaaa-000000000000') + def test_attach_volume_bad_id(self): + self.stubs.Set(compute_api.API, + 'attach_volume', + fake_attach_volume) + attachments = volumes.VolumeAttachmentController() + + body = { + 'volumeAttachment': { + 'device': None, + 'volumeId': 'TESTVOLUME', + } + } + + req = fakes.HTTPRequest.blank('/v2/fake/os-volumes/attach') + req.method = 'POST' + req.content_type = 'application/json' + req.body = jsonutils.dumps(body) + + self.assertRaises(webob.exc.HTTPBadRequest, attachments.create, + req, FAKE_UUID, body) + class VolumeSerializerTest(test.TestCase): def _verify_volume_attachment(self, attach, tree): diff --git a/nova/tests/api/openstack/compute/test_flavors.py b/nova/tests/api/openstack/compute/test_flavors.py index da633d371..050384aa2 100644 --- a/nova/tests/api/openstack/compute/test_flavors.py +++ b/nova/tests/api/openstack/compute/test_flavors.py @@ -77,7 +77,7 @@ def empty_instance_type_get_all(inactive=False, filters=None): def return_instance_type_not_found(flavor_id): - raise exception.InstanceTypeNotFound(flavor_id=flavor_id) + raise exception.InstanceTypeNotFound(instance_type_id=flavor_id) class FlavorsTest(test.TestCase): diff --git a/nova/tests/api/openstack/compute/test_server_actions.py b/nova/tests/api/openstack/compute/test_server_actions.py index fe085f99d..414d70c7c 100644 --- a/nova/tests/api/openstack/compute/test_server_actions.py +++ b/nova/tests/api/openstack/compute/test_server_actions.py @@ -184,7 +184,9 @@ class ServerActionsControllerTest(test.TestCase): body = dict(reboot=dict(type="HARD")) def fake_reboot(*args, **kwargs): - raise exception.InstanceInvalidState + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') self.stubs.Set(compute_api.API, 'reboot', fake_reboot) @@ -306,7 +308,9 @@ class ServerActionsControllerTest(test.TestCase): } def fake_rebuild(*args, **kwargs): - raise exception.InstanceInvalidState + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') self.stubs.Set(compute_api.API, 'rebuild', fake_rebuild) @@ -347,6 +351,21 @@ class ServerActionsControllerTest(test.TestCase): self.controller._action_rebuild, req, FAKE_UUID, body) + def test_rebuild_with_too_large_metadata(self): + body = { + "rebuild": { + "imageRef": self._image_href, + "metadata": { + 256 * "k": "value" + } + } + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, + self.controller._action_rebuild, req, + FAKE_UUID, body) + def test_rebuild_bad_entity(self): body = { "rebuild": { @@ -589,7 +608,9 @@ class ServerActionsControllerTest(test.TestCase): body = dict(resize=dict(flavorRef="http://localhost/3")) def fake_resize(*args, **kwargs): - raise exception.InstanceInvalidState + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') self.stubs.Set(compute_api.API, 'resize', fake_resize) @@ -633,7 +654,9 @@ class ServerActionsControllerTest(test.TestCase): body = dict(confirmResize=None) def fake_confirm_resize(*args, **kwargs): - raise exception.InstanceInvalidState + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') self.stubs.Set(compute_api.API, 'confirm_resize', fake_confirm_resize) @@ -659,6 +682,14 @@ class ServerActionsControllerTest(test.TestCase): self.controller._action_revert_resize, req, FAKE_UUID, body) + def test_revert_resize_server_not_found(self): + body = dict(revertResize=None) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob. exc.HTTPNotFound, + self.controller._action_revert_resize, + req, "bad_server_id", body) + def test_revert_resize_server(self): body = dict(revertResize=None) @@ -678,7 +709,9 @@ class ServerActionsControllerTest(test.TestCase): body = dict(revertResize=None) def fake_revert_resize(*args, **kwargs): - raise exception.InstanceInvalidState + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') self.stubs.Set(compute_api.API, 'revert_resize', fake_revert_resize) @@ -881,7 +914,9 @@ class ServerActionsControllerTest(test.TestCase): def test_create_image_raises_conflict_on_invalid_state(self): def snapshot(*args, **kwargs): - raise exception.InstanceInvalidState + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') self.stubs.Set(compute_api.API, 'snapshot', snapshot) body = { diff --git a/nova/tests/api/openstack/compute/test_server_metadata.py b/nova/tests/api/openstack/compute/test_server_metadata.py index 78e3f866b..1e992c2a3 100644 --- a/nova/tests/api/openstack/compute/test_server_metadata.py +++ b/nova/tests/api/openstack/compute/test_server_metadata.py @@ -86,7 +86,7 @@ def return_server_by_uuid(context, server_uuid): def return_server_nonexistent(context, server_id): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id=server_id) def fake_change_instance_metadata(self, context, instance, diff): @@ -253,6 +253,22 @@ class ServerMetaDataTest(test.TestCase): self.assertRaises(webob.exc.HTTPNotFound, self.controller.create, req, self.uuid, body) + def test_update_metadata(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url) + req.method = 'POST' + req.content_type = 'application/json' + expected = { + 'metadata': { + 'key1': 'updatedvalue', + 'key29': 'newkey', + } + } + req.body = jsonutils.dumps(expected) + response = self.controller.update_all(req, self.uuid, expected) + self.assertEqual(expected, response) + def test_update_all(self): self.stubs.Set(nova.db, 'instance_metadata_update', return_create_instance_metadata) diff --git a/nova/tests/api/openstack/compute/test_servers.py b/nova/tests/api/openstack/compute/test_servers.py index d32640bf2..734297501 100644 --- a/nova/tests/api/openstack/compute/test_servers.py +++ b/nova/tests/api/openstack/compute/test_servers.py @@ -77,6 +77,10 @@ def return_servers_by_reservation(context, reservation_id=""): reservation_id=reservation_id) for i in xrange(5)] +def return_servers_empty(context, *args, **kwargs): + return [] + + def return_servers_by_reservation_empty(context, reservation_id=""): return [] @@ -518,7 +522,7 @@ class ServersControllerTest(test.TestCase): def test_get_server_addresses_nonexistent_server(self): def fake_instance_get(*args, **kwargs): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id='fake') self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get) @@ -527,6 +531,16 @@ class ServersControllerTest(test.TestCase): self.assertRaises(webob.exc.HTTPNotFound, self.ips_controller.index, req, server_id) + def test_get_server_list_empty(self): + self.stubs.Set(db, 'instance_get_all_by_filters', + return_servers_empty) + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + res_dict = self.controller.index(req) + + num_servers = len(res_dict['servers']) + self.assertEqual(0, num_servers) + def test_get_server_list_with_reservation_id(self): self.stubs.Set(db, 'instance_get_all_by_reservation', return_servers_by_reservation) @@ -610,6 +624,16 @@ class ServersControllerTest(test.TestCase): self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) + def test_get_server_details_empty(self): + self.stubs.Set(db, 'instance_get_all_by_filters', + return_servers_empty) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/detail') + res_dict = self.controller.index(req) + + num_servers = len(res_dict['servers']) + self.assertEqual(0, num_servers) + def test_get_server_details_with_limit(self): req = fakes.HTTPRequest.blank('/v2/fake/servers/detail?limit=3') res = self.controller.detail(req) @@ -1184,6 +1208,20 @@ class ServersControllerTest(test.TestCase): self.assertEqual(res_dict['server']['id'], FAKE_UUID) self.assertEqual(res_dict['server']['accessIPv6'], '') + def test_update_server_personality(self): + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + req.method = 'PUT' + req.content_type = 'application/json' + body = { + 'server': { + 'personality': [] + } + } + req.body = jsonutils.dumps(body) + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, FAKE_UUID, body) + def test_update_server_adminPass_ignored(self): inst_dict = dict(name='server_test', adminPass='bacon') body = dict(server=inst_dict) @@ -1212,7 +1250,7 @@ class ServersControllerTest(test.TestCase): def test_update_server_not_found(self): def fake_get(*args, **kwargs): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id='fake') self.stubs.Set(compute_api.API, 'get', fake_get) req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) @@ -1225,7 +1263,7 @@ class ServersControllerTest(test.TestCase): def test_update_server_not_found_on_update(self): def fake_update(*args, **kwargs): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id='fake') self.stubs.Set(compute_api.API, 'update', fake_update) req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) diff --git a/nova/tests/api/openstack/compute/test_versions.py b/nova/tests/api/openstack/compute/test_versions.py index 16790860c..28b109215 100644 --- a/nova/tests/api/openstack/compute/test_versions.py +++ b/nova/tests/api/openstack/compute/test_versions.py @@ -37,17 +37,17 @@ NS = { } -LINKS = { +EXP_LINKS = { 'v2.0': { 'pdf': 'http://docs.openstack.org/' - 'api/openstack-compute/1.1/os-compute-devguide-1.1.pdf', + 'api/openstack-compute/2/os-compute-devguide-2.pdf', 'wadl': 'http://docs.openstack.org/' - 'api/openstack-compute/1.1/wadl/os-compute-1.1.wadl', + 'api/openstack-compute/2/wadl/os-compute-2.wadl', }, } -VERSIONS = { +EXP_VERSIONS = { "v2.0": { "id": "v2.0", "status": "CURRENT", @@ -56,12 +56,12 @@ VERSIONS = { { "rel": "describedby", "type": "application/pdf", - "href": LINKS['v2.0']['pdf'], + "href": EXP_LINKS['v2.0']['pdf'], }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", - "href": LINKS['v2.0']['wadl'], + "href": EXP_LINKS['v2.0']['wadl'], }, ], "media-types": [ @@ -79,9 +79,6 @@ VERSIONS = { class VersionsTest(test.TestCase): - def setUp(self): - super(VersionsTest, self).setUp() - self.stubs.Set(versions, 'VERSIONS', VERSIONS) def test_get_version_list(self): req = webob.Request.blank('/') @@ -132,12 +129,12 @@ class VersionsTest(test.TestCase): { "rel": "describedby", "type": "application/pdf", - "href": LINKS['v2.0']['pdf'], + "href": EXP_LINKS['v2.0']['pdf'], }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", - "href": LINKS['v2.0']['wadl'], + "href": EXP_LINKS['v2.0']['wadl'], }, ], "media-types": [ @@ -176,12 +173,12 @@ class VersionsTest(test.TestCase): { "rel": "describedby", "type": "application/pdf", - "href": LINKS['v2.0']['pdf'], + "href": EXP_LINKS['v2.0']['pdf'], }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", - "href": LINKS['v2.0']['wadl'], + "href": EXP_LINKS['v2.0']['wadl'], }, ], "media-types": [ @@ -210,7 +207,7 @@ class VersionsTest(test.TestCase): version = etree.XML(res.body) xmlutil.validate_schema(version, 'version') - expected = VERSIONS['v2.0'] + expected = EXP_VERSIONS['v2.0'] self.assertTrue(version.xpath('/ns:version', namespaces=NS)) media_types = version.xpath('ns:media-types/ns:media-type', namespaces=NS) @@ -240,7 +237,7 @@ class VersionsTest(test.TestCase): for i, v in enumerate(['v2.0']): version = versions[i] - expected = VERSIONS[v] + expected = EXP_VERSIONS[v] for key in ['id', 'status', 'updated']: self.assertEqual(version.get(key), expected[key]) (link,) = version.xpath('atom:link', namespaces=NS) @@ -278,11 +275,11 @@ class VersionsTest(test.TestCase): self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/') self.assertEqual(entry.links[0]['rel'], 'self') self.assertEqual(entry.links[1], { - 'href': LINKS['v2.0']['pdf'], + 'href': EXP_LINKS['v2.0']['pdf'], 'type': 'application/pdf', 'rel': 'describedby'}) self.assertEqual(entry.links[2], { - 'href': LINKS['v2.0']['wadl'], + 'href': EXP_LINKS['v2.0']['wadl'], 'type': 'application/vnd.sun.wadl+xml', 'rel': 'describedby'}) @@ -368,8 +365,11 @@ class VersionsTest(test.TestCase): self.assertEqual(version.get('status'), 'CURRENT') media_types = version.xpath('ns:media-types/ns:media-type', namespaces=NS) - self.assertTrue(common.compare_media_types(media_types, - VERSIONS['v2.0']['media-types'])) + self.assertTrue(common. + compare_media_types(media_types, + EXP_VERSIONS['v2.0']['media-types'] + )) + links = version.xpath('atom:link', namespaces=NS) self.assertTrue(common.compare_links(links, [{'rel': 'self', 'href': 'http://localhost/v2/images/1'}])) @@ -512,7 +512,7 @@ class VersionsSerializerTests(test.TestCase): "id": "2.7", "updated": "2011-07-18T11:30:00Z", "status": "DEPRECATED", - "media-types": VERSIONS['v2.0']['media-types'], + "media-types": EXP_VERSIONS['v2.0']['media-types'], "links": [ { "rel": "self", @@ -601,12 +601,12 @@ class VersionsSerializerTests(test.TestCase): { "rel": "describedby", "type": "application/pdf", - "href": LINKS['v2.0']['pdf'], + "href": EXP_LINKS['v2.0']['pdf'], }, { "rel": "describedby", "type": "application/vnd.sun.wadl+xml", - "href": LINKS['v2.0']['wadl'], + "href": EXP_LINKS['v2.0']['wadl'], }, ], "media-types": [ @@ -651,9 +651,9 @@ class VersionsSerializerTests(test.TestCase): self.assertEqual(entry.links[1], { 'rel': 'describedby', 'type': 'application/pdf', - 'href': LINKS['v2.0']['pdf']}) + 'href': EXP_LINKS['v2.0']['pdf']}) self.assertEqual(entry.links[2], { 'rel': 'describedby', 'type': 'application/vnd.sun.wadl+xml', - 'href': LINKS['v2.0']['wadl'], + 'href': EXP_LINKS['v2.0']['wadl'], }) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 465cf63dc..9b939b324 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -403,7 +403,7 @@ def fake_instance_get_all_by_filters(num_servers=5, **kwargs): found_marker = True servers_list = [] if not marker is None and not found_marker: - raise exc.MarkerNotFound(marker) + raise exc.MarkerNotFound(marker=marker) if not limit is None: servers_list = servers_list[:limit] return servers_list diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py index 4ebd49ca2..28bbb3d25 100644 --- a/nova/tests/api/openstack/test_common.py +++ b/nova/tests/api/openstack/test_common.py @@ -284,9 +284,9 @@ class MiscFunctionsTest(test.TestCase): self.assertEqual(actual, expected) def test_raise_http_conflict_for_instance_invalid_state(self): - # Correct args exc = exception.InstanceInvalidState(attr='fake_attr', - state='fake_state', method='fake_method') + state='fake_state', method='fake_method', + instance_uuid='fake') try: common.raise_http_conflict_for_instance_invalid_state(exc, 'meow') @@ -296,17 +296,6 @@ class MiscFunctionsTest(test.TestCase): else: self.fail("webob.exc.HTTPConflict was not raised") - # Incorrect args - exc = exception.InstanceInvalidState() - try: - common.raise_http_conflict_for_instance_invalid_state(exc, - 'meow') - except webob.exc.HTTPConflict as e: - self.assertEqual(unicode(e), - "Instance is in an invalid state for 'meow'") - else: - self.fail("webob.exc.HTTPConflict was not raised") - def test_check_img_metadata_properties_quota_valid_metadata(self): ctxt = test_utils.get_test_admin_context() metadata1 = {"key": "value"} diff --git a/nova/tests/baremetal/test_driver.py b/nova/tests/baremetal/test_driver.py index b12336da0..d90c6ac50 100644 --- a/nova/tests/baremetal/test_driver.py +++ b/nova/tests/baremetal/test_driver.py @@ -1,3 +1,7 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# coding=utf-8 + +# Copyright 2012 Hewlett-Packard Development Company, L.P. # Copyright (c) 2012 NTT DOCOMO, INC. # Copyright (c) 2011 University of Southern California / ISI # All Rights Reserved. @@ -15,180 +19,156 @@ # under the License. """ -Tests for baremetal driver. +Tests for the base baremetal driver class. """ from nova import exception from nova.openstack.common import cfg -from nova.tests.baremetal.db import base -from nova.tests.baremetal.db import utils +from nova import test +from nova.tests.baremetal.db import base as bm_db_base +from nova.tests.baremetal.db import utils as bm_db_utils from nova.tests.image import fake as fake_image -from nova.tests import test_virt_drivers -from nova.tests import utils as test_utils +from nova.tests import utils from nova.virt.baremetal import baremetal_states from nova.virt.baremetal import db from nova.virt.baremetal import driver as bm_driver -from nova.virt.baremetal import volume_driver -from nova.virt.firewall import NoopFirewallDriver +from nova.virt.baremetal import fake CONF = cfg.CONF - -class FakeVifDriver(object): - - def plug(self, instance, vif): - pass - - def unplug(self, instance, vif): - pass - -FakeFirewallDriver = NoopFirewallDriver - - -class FakeVolumeDriver(volume_driver.VolumeDriver): - def __init__(self, virtapi): - super(FakeVolumeDriver, self).__init__(virtapi) - self._initiator = "testtesttest" - - -NODE = utils.new_bm_node(cpus=2, memory_mb=4096, service_host="host1") -NICS = [ - {'address': '01:23:45:67:89:01', 'datapath_id': '0x1', 'port_no': 1, }, - {'address': '01:23:45:67:89:02', 'datapath_id': '0x2', 'port_no': 2, }, - ] - - -def class_path(class_): - return class_.__module__ + '.' + class_.__name__ - - COMMON_FLAGS = dict( - firewall_driver=class_path(FakeFirewallDriver), + firewall_driver='nova.virt.baremetal.fake.FakeFirewallDriver', + host='test_host', ) BAREMETAL_FLAGS = dict( - driver='nova.virt.baremetal.fake.Fake', - host=NODE['service_host'], - instance_type_extra_specs=['cpu_arch:test'], + driver='nova.virt.baremetal.fake.FakeDriver', + instance_type_extra_specs=['cpu_arch:test', 'test_spec:test_value'], power_manager='nova.virt.baremetal.fake.FakePowerManager', - sql_connection='sqlite:///:memory:', - vif_driver=class_path(FakeVifDriver), - volume_driver=class_path(FakeVolumeDriver), + vif_driver='nova.virt.baremetal.fake.FakeVifDriver', + volume_driver='nova.virt.baremetal.fake.FakeVolumeDriver', group='baremetal', ) -def _create_baremetal_stuff(): - context = test_utils.get_test_admin_context() - node = db.bm_node_create(context, NODE) - for nic in NICS: - db.bm_interface_create(context, - node['id'], - nic['address'], - nic['datapath_id'], - nic['port_no']) - return node - - -class BaremetalDriverSpawnTestCase(base.Database): +class BareMetalDriverNoDBTestCase(test.TestCase): def setUp(self): - super(BaremetalDriverSpawnTestCase, self).setUp() + super(BareMetalDriverNoDBTestCase, self).setUp() self.flags(**COMMON_FLAGS) self.flags(**BAREMETAL_FLAGS) - fake_image.stub_out_image_service(self.stubs) + self.driver = bm_driver.BareMetalDriver(None) - self.node = _create_baremetal_stuff() - self.node_id = self.node['id'] + def test_validate_driver_loading(self): + self.assertTrue(isinstance(self.driver.driver, + fake.FakeDriver)) + self.assertTrue(isinstance(self.driver.vif_driver, + fake.FakeVifDriver)) + self.assertTrue(isinstance(self.driver.volume_driver, + fake.FakeVolumeDriver)) + self.assertTrue(isinstance(self.driver.firewall_driver, + fake.FakeFirewallDriver)) - self.context = test_utils.get_test_admin_context() - self.instance = test_utils.get_test_instance() - self.network_info = test_utils.get_test_network_info() - self.block_device_info = None - self.image_meta = test_utils.get_test_image_info(None, self.instance) - self.driver = bm_driver.BareMetalDriver(None) - self.kwargs = dict( - context=self.context, - instance=self.instance, - image_meta=self.image_meta, - injected_files=[('/foo', 'bar'), ('/abc', 'xyz')], - admin_password='testpass', - network_info=self.network_info, - block_device_info=self.block_device_info) - self.addCleanup(fake_image.FakeImageService_reset) - def test_ok(self): - self.instance['node'] = str(self.node_id) - self.driver.spawn(**self.kwargs) - node = db.bm_node_get(self.context, self.node_id) - self.assertEqual(node['instance_uuid'], self.instance['uuid']) - self.assertEqual(node['task_state'], baremetal_states.ACTIVE) - - def test_without_node(self): - self.assertRaises( - exception.NovaException, - self.driver.spawn, - **self.kwargs) - - def test_node_not_found(self): - self.instance['node'] = "123456789" - self.assertRaises( - exception.InstanceNotFound, - self.driver.spawn, - **self.kwargs) - - def test_node_in_use(self): - self.instance['node'] = str(self.node_id) - db.bm_node_update(self.context, self.node_id, - {'instance_uuid': 'something'}) - self.assertRaises( - exception.NovaException, - self.driver.spawn, - **self.kwargs) - - -class BaremetalDriverTestCase(test_virt_drivers._VirtDriverTestCase, - base.Database): +class BareMetalDriverWithDBTestCase(bm_db_base.BMDBTestCase): def setUp(self): - super(BaremetalDriverTestCase, self).setUp() - self.driver_module = 'nova.virt.baremetal.BareMetalDriver' + super(BareMetalDriverWithDBTestCase, self).setUp() self.flags(**COMMON_FLAGS) - self.node = _create_baremetal_stuff() - self.node_id = self.node['id'] + self.flags(**BAREMETAL_FLAGS) + fake_image.stub_out_image_service(self.stubs) + self.context = utils.get_test_admin_context() + self.driver = bm_driver.BareMetalDriver(None) + self.node_info = bm_db_utils.new_bm_node( + id=123, + service_host='test_host', + cpus=2, + memory_mb=2048, + ) + self.nic_info = [ + {'address': '01:23:45:67:89:01', 'datapath_id': '0x1', + 'port_no': 1}, + {'address': '01:23:45:67:89:02', 'datapath_id': '0x2', + 'port_no': 2}, + ] self.addCleanup(fake_image.FakeImageService_reset) - def _get_running_instance(self): - instance_ref = test_utils.get_test_instance() - instance_ref['node'] = str(self.node_id) - network_info = test_utils.get_test_network_info() - image_info = test_utils.get_test_image_info(None, instance_ref) - self.connection.spawn(self.ctxt, instance_ref, image_info, - [], 'herp', network_info=network_info) - return instance_ref, network_info - - def test_loading_baremetal_drivers(self): - from nova.virt.baremetal import fake - drv = bm_driver.BareMetalDriver(None) - self.assertTrue(isinstance(drv.baremetal_nodes, fake.Fake)) - self.assertTrue(isinstance(drv._vif_driver, FakeVifDriver)) - self.assertTrue(isinstance(drv._firewall_driver, FakeFirewallDriver)) - self.assertTrue(isinstance(drv._volume_driver, FakeVolumeDriver)) + def _create_node(self): + self.node = db.bm_node_create(self.context, self.node_info) + for nic in self.nic_info: + db.bm_interface_create( + self.context, + self.node['id'], + nic['address'], + nic['datapath_id'], + nic['port_no'], + ) + self.test_instance = utils.get_test_instance() + self.test_instance['node'] = self.node['id'] + self.spawn_params = dict( + admin_password='test_pass', + block_device_info=None, + context=self.context, + image_meta=utils.get_test_image_info(None, + self.test_instance), + injected_files=[('/fake/path', 'hello world')], + instance=self.test_instance, + network_info=utils.get_test_network_info(), + ) def test_get_host_stats(self): - self.flags(instance_type_extra_specs=['cpu_arch:x86_64', - 'x:123', - 'y:456', ]) - drv = bm_driver.BareMetalDriver(None) - cap_list = drv.get_host_stats() - self.assertTrue(isinstance(cap_list, list)) - self.assertEqual(len(cap_list), 1) - cap = cap_list[0] - self.assertEqual(cap['cpu_arch'], 'x86_64') - self.assertEqual(cap['x'], '123') - self.assertEqual(cap['y'], '456') - self.assertEqual(cap['hypervisor_type'], 'baremetal') - self.assertEqual(cap['driver'], - 'nova.virt.baremetal.fake.Fake') + self._create_node() + stats = self.driver.get_host_stats() + self.assertTrue(isinstance(stats, list)) + self.assertEqual(len(stats), 1) + stats = stats[0] + self.assertEqual(stats['cpu_arch'], 'test') + self.assertEqual(stats['test_spec'], 'test_value') + self.assertEqual(stats['hypervisor_type'], 'baremetal') + self.assertEqual(stats['hypervisor_hostname'], '123') + self.assertEqual(stats['host'], 'test_host') + self.assertEqual(stats['vcpus'], 2) + self.assertEqual(stats['host_memory_total'], 2048) + + def test_spawn_ok(self): + self._create_node() + self.driver.spawn(**self.spawn_params) + row = db.bm_node_get(self.context, self.node['id']) + self.assertEqual(row['task_state'], baremetal_states.ACTIVE) + + def test_spawn_node_in_use(self): + self._create_node() + db.bm_node_update(self.context, self.node['id'], + {'instance_uuid': '1234-5678'}) + + self.assertRaises(exception.NovaException, + self.driver.spawn, **self.spawn_params) + + row = db.bm_node_get(self.context, self.node['id']) + self.assertEqual(row['task_state'], None) + + def test_spawn_node_not_found(self): + self._create_node() + db.bm_node_update(self.context, self.node['id'], + {'id': 9876}) + + self.assertRaises(exception.NovaException, + self.driver.spawn, **self.spawn_params) + + row = db.bm_node_get(self.context, 9876) + self.assertEqual(row['task_state'], None) + + def test_spawn_fails(self): + self._create_node() + + self.mox.StubOutWithMock(fake.FakePowerManager, 'activate_node') + fake.FakePowerManager.activate_node().AndRaise(test.TestingException) + self.mox.ReplayAll() + + self.assertRaises(test.TestingException, + self.driver.spawn, **self.spawn_params) + + row = db.bm_node_get(self.context, self.node['id']) + self.assertEqual(row['task_state'], baremetal_states.ERROR) diff --git a/nova/tests/baremetal/test_ipmi.py b/nova/tests/baremetal/test_ipmi.py new file mode 100644 index 000000000..def6da66f --- /dev/null +++ b/nova/tests/baremetal/test_ipmi.py @@ -0,0 +1,224 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# coding=utf-8 + +# Copyright 2012 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2012 NTT DOCOMO, 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. + +""" +Test class for baremetal IPMI power manager. +""" + +import os +import stat +import tempfile + +from nova.openstack.common import cfg +from nova import test +from nova.tests.baremetal.db import utils as bm_db_utils +from nova import utils +from nova.virt.baremetal import baremetal_states +from nova.virt.baremetal import ipmi +from nova.virt.baremetal import utils as bm_utils + +CONF = cfg.CONF + + +class BareMetalIPMITestCase(test.TestCase): + + def setUp(self): + super(BareMetalIPMITestCase, self).setUp() + self.node = bm_db_utils.new_bm_node( + id=123, + pm_address='fake-address', + pm_user='fake-user', + pm_password='fake-password') + self.ipmi = ipmi.IPMI(self.node) + + def test_construct(self): + self.assertEqual(self.ipmi.node_id, 123) + self.assertEqual(self.ipmi.address, 'fake-address') + self.assertEqual(self.ipmi.user, 'fake-user') + self.assertEqual(self.ipmi.password, 'fake-password') + + def test_make_password_file(self): + pw_file = ipmi._make_password_file(self.node['pm_password']) + try: + self.assertTrue(os.path.isfile(pw_file)) + self.assertEqual(os.stat(pw_file)[stat.ST_MODE] & 0777, 0600) + with open(pw_file, "r") as f: + pm_password = f.read() + self.assertEqual(pm_password, self.node['pm_password']) + finally: + os.unlink(pw_file) + + def test_exec_ipmitool(self): + pw_file = '/tmp/password_file' + + self.mox.StubOutWithMock(ipmi, '_make_password_file') + self.mox.StubOutWithMock(utils, 'execute') + self.mox.StubOutWithMock(bm_utils, 'unlink_without_raise') + ipmi._make_password_file(self.ipmi.password).AndReturn(pw_file) + args = [ + 'ipmitool', + '-I', 'lanplus', + '-H', self.ipmi.address, + '-U', self.ipmi.user, + '-f', pw_file, + 'A', 'B', 'C', + ] + utils.execute(*args, attempts=3).AndReturn(('', '')) + bm_utils.unlink_without_raise(pw_file).AndReturn(None) + self.mox.ReplayAll() + + self.ipmi._exec_ipmitool('A B C') + self.mox.VerifyAll() + + def test_is_power(self): + self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool') + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is on\n"]) + self.mox.ReplayAll() + + self.ipmi._is_power("on") + self.mox.VerifyAll() + + def test_power_already_on(self): + self.flags(ipmi_power_retry=0, group='baremetal') + self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool') + + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is on\n"]) + self.mox.ReplayAll() + + self.ipmi.state = baremetal_states.DELETED + self.ipmi._power_on() + self.mox.VerifyAll() + self.assertEqual(self.ipmi.state, baremetal_states.ACTIVE) + + def test_power_on_ok(self): + self.flags(ipmi_power_retry=0, group='baremetal') + self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool') + + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is off\n"]) + self.ipmi._exec_ipmitool("power on").AndReturn([]) + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is on\n"]) + self.mox.ReplayAll() + + self.ipmi.state = baremetal_states.DELETED + self.ipmi._power_on() + self.mox.VerifyAll() + self.assertEqual(self.ipmi.state, baremetal_states.ACTIVE) + + def test_power_on_fail(self): + self.flags(ipmi_power_retry=0, group='baremetal') + self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool') + + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is off\n"]) + self.ipmi._exec_ipmitool("power on").AndReturn([]) + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is off\n"]) + self.mox.ReplayAll() + + self.ipmi.state = baremetal_states.DELETED + self.ipmi._power_on() + self.mox.VerifyAll() + self.assertEqual(self.ipmi.state, baremetal_states.ERROR) + + def test_power_on_max_retries(self): + self.flags(ipmi_power_retry=2, group='baremetal') + self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool') + + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is off\n"]) + self.ipmi._exec_ipmitool("power on").AndReturn([]) + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is off\n"]) + self.ipmi._exec_ipmitool("power on").AndReturn([]) + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is off\n"]) + self.ipmi._exec_ipmitool("power on").AndReturn([]) + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is off\n"]) + self.mox.ReplayAll() + + self.ipmi.state = baremetal_states.DELETED + self.ipmi._power_on() + self.mox.VerifyAll() + self.assertEqual(self.ipmi.state, baremetal_states.ERROR) + self.assertEqual(self.ipmi.retries, 3) + + def test_power_off_ok(self): + self.flags(ipmi_power_retry=0, group='baremetal') + self.mox.StubOutWithMock(self.ipmi, '_exec_ipmitool') + + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is on\n"]) + self.ipmi._exec_ipmitool("power off").AndReturn([]) + self.ipmi._exec_ipmitool("power status").AndReturn( + ["Chassis Power is off\n"]) + self.mox.ReplayAll() + + self.ipmi.state = baremetal_states.ACTIVE + self.ipmi._power_off() + self.mox.VerifyAll() + self.assertEqual(self.ipmi.state, baremetal_states.DELETED) + + def test_get_console_pid_path(self): + self.flags(terminal_pid_dir='/tmp', group='baremetal') + path = ipmi._get_console_pid_path(self.ipmi.node_id) + self.assertEqual(path, '/tmp/%s.pid' % self.ipmi.node_id) + + def test_console_pid(self): + fd, path = tempfile.mkstemp() + with os.fdopen(fd, 'w') as f: + f.write("12345\n") + + self.mox.StubOutWithMock(ipmi, '_get_console_pid_path') + ipmi._get_console_pid_path(self.ipmi.node_id).AndReturn(path) + self.mox.ReplayAll() + + pid = ipmi._get_console_pid(self.ipmi.node_id) + bm_utils.unlink_without_raise(path) + self.mox.VerifyAll() + self.assertEqual(pid, 12345) + + def test_console_pid_nan(self): + fd, path = tempfile.mkstemp() + with os.fdopen(fd, 'w') as f: + f.write("hello world\n") + + self.mox.StubOutWithMock(ipmi, '_get_console_pid_path') + ipmi._get_console_pid_path(self.ipmi.node_id).AndReturn(path) + self.mox.ReplayAll() + + pid = ipmi._get_console_pid(self.ipmi.node_id) + bm_utils.unlink_without_raise(path) + self.mox.VerifyAll() + self.assertTrue(pid is None) + + def test_console_pid_file_not_found(self): + pid_path = ipmi._get_console_pid_path(self.ipmi.node_id) + + self.mox.StubOutWithMock(os.path, 'exists') + os.path.exists(pid_path).AndReturn(False) + self.mox.ReplayAll() + + pid = ipmi._get_console_pid(self.ipmi.node_id) + self.mox.VerifyAll() + self.assertTrue(pid is None) diff --git a/nova/tests/baremetal/test_pxe.py b/nova/tests/baremetal/test_pxe.py new file mode 100644 index 000000000..dd679a563 --- /dev/null +++ b/nova/tests/baremetal/test_pxe.py @@ -0,0 +1,534 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# coding=utf-8 + +# Copyright 2012 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2012 NTT DOCOMO, 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. + +""" +Tests for baremetal pxe driver. +""" + +import os + +import mox +from testtools.matchers import Contains +from testtools.matchers import MatchesAll +from testtools.matchers import Not +from testtools.matchers import StartsWith + +from nova import exception +from nova.openstack.common import cfg +from nova import test +from nova.tests.baremetal.db import base as bm_db_base +from nova.tests.baremetal.db import utils as bm_db_utils +from nova.tests.image import fake as fake_image +from nova.tests import utils +from nova.virt.baremetal import db +from nova.virt.baremetal import pxe +from nova.virt.baremetal import utils as bm_utils +from nova.virt.disk import api as disk_api + +CONF = cfg.CONF + +COMMON_FLAGS = dict( + firewall_driver='nova.virt.baremetal.fake.FakeFirewallDriver', + host='test_host', +) + +BAREMETAL_FLAGS = dict( + driver='nova.virt.baremetal.pxe.PXE', + instance_type_extra_specs=['cpu_arch:test', 'test_spec:test_value'], + power_manager='nova.virt.baremetal.fake.FakePowerManager', + vif_driver='nova.virt.baremetal.fake.FakeVifDriver', + volume_driver='nova.virt.baremetal.fake.FakeVolumeDriver', + group='baremetal', +) + + +class BareMetalPXETestCase(bm_db_base.BMDBTestCase): + + def setUp(self): + super(BareMetalPXETestCase, self).setUp() + self.flags(**COMMON_FLAGS) + self.flags(**BAREMETAL_FLAGS) + self.driver = pxe.PXE() + + fake_image.stub_out_image_service(self.stubs) + self.addCleanup(fake_image.FakeImageService_reset) + self.context = utils.get_test_admin_context() + self.test_block_device_info = None, + self.instance = utils.get_test_instance() + self.test_network_info = utils.get_test_network_info(), + self.node_info = bm_db_utils.new_bm_node( + id=123, + service_host='test_host', + cpus=2, + memory_mb=2048, + prov_mac_address='11:11:11:11:11:11', + ) + self.nic_info = [ + {'address': '22:22:22:22:22:22', 'datapath_id': '0x1', + 'port_no': 1}, + {'address': '33:33:33:33:33:33', 'datapath_id': '0x2', + 'port_no': 2}, + ] + + def _create_node(self): + self.node = db.bm_node_create(self.context, self.node_info) + for nic in self.nic_info: + db.bm_interface_create( + self.context, + self.node['id'], + nic['address'], + nic['datapath_id'], + nic['port_no'], + ) + self.instance['node'] = self.node['id'] + self.spawn_params = dict( + admin_password='test_pass', + block_device_info=self.test_block_device_info, + context=self.context, + image_meta=utils.get_test_image_info(None, + self.instance), + injected_files=[('/fake/path', 'hello world')], + instance=self.instance, + network_info=self.test_network_info, + ) + + +class PXEClassMethodsTestCase(BareMetalPXETestCase): + + def test_build_pxe_config(self): + args = { + 'deployment_id': 'aaa', + 'deployment_key': 'bbb', + 'deployment_iscsi_iqn': 'ccc', + 'deployment_aki_path': 'ddd', + 'deployment_ari_path': 'eee', + 'aki_path': 'fff', + 'ari_path': 'ggg', + } + config = pxe.build_pxe_config(**args) + self.assertThat(config, StartsWith('default deploy')) + + # deploy bits are in the deploy section + start = config.index('label deploy') + end = config.index('label boot') + self.assertThat(config[start:end], MatchesAll( + Contains('kernel ddd'), + Contains('initrd=eee'), + Contains('deployment_id=aaa'), + Contains('deployment_key=bbb'), + Contains('iscsi_target_iqn=ccc'), + Not(Contains('kernel fff')), + )) + + # boot bits are in the boot section + start = config.index('label boot') + self.assertThat(config[start:], MatchesAll( + Contains('kernel fff'), + Contains('initrd=ggg'), + Not(Contains('kernel ddd')), + )) + + def test_build_network_config(self): + net = utils.get_test_network_info(1) + config = pxe.build_network_config(net) + self.assertIn('eth0', config) + self.assertNotIn('eth1', config) + self.assertIn('hwaddress ether fake', config) + self.assertNotIn('hwaddress ether aa:bb:cc:dd', config) + + net[0][1]['mac'] = 'aa:bb:cc:dd' + config = pxe.build_network_config(net) + self.assertIn('hwaddress ether aa:bb:cc:dd', config) + + net = utils.get_test_network_info(2) + config = pxe.build_network_config(net) + self.assertIn('eth0', config) + self.assertIn('eth1', config) + + def test_build_network_config_dhcp(self): + self.flags( + net_config_template='$pybasedir/nova/virt/baremetal/' + 'net-dhcp.ubuntu.template', + group='baremetal', + ) + net = utils.get_test_network_info() + net[0][1]['ips'][0]['ip'] = '1.2.3.4' + config = pxe.build_network_config(net) + self.assertIn('iface eth0 inet dhcp', config) + self.assertNotIn('address 1.2.3.4', config) + + def test_build_network_config_static(self): + self.flags( + net_config_template='$pybasedir/nova/virt/baremetal/' + 'net-static.ubuntu.template', + group='baremetal', + ) + net = utils.get_test_network_info() + net[0][1]['ips'][0]['ip'] = '1.2.3.4' + config = pxe.build_network_config(net) + self.assertIn('iface eth0 inet static', config) + self.assertIn('address 1.2.3.4', config) + + def test_image_dir_path(self): + self.assertEqual( + pxe.get_image_dir_path(self.instance), + os.path.join(CONF.instances_path, 'instance-00000001')) + + def test_image_file_path(self): + self.assertEqual( + pxe.get_image_file_path(self.instance), + os.path.join( + CONF.instances_path, 'instance-00000001', 'disk')) + + def test_pxe_config_file_path(self): + self.instance['uuid'] = 'aaaa-bbbb-cccc' + self.assertEqual( + pxe.get_pxe_config_file_path(self.instance), + os.path.join(CONF.baremetal.tftp_root, + 'aaaa-bbbb-cccc', 'config')) + + def test_pxe_mac_path(self): + self.assertEqual( + pxe.get_pxe_mac_path('23:45:67:89:AB'), + os.path.join(CONF.baremetal.tftp_root, + 'pxelinux.cfg', '01-23-45-67-89-ab')) + + def test_get_instance_deploy_ids(self): + self.instance['extra_specs'] = { + 'deploy_kernel_id': 'aaaa', + 'deploy_ramdisk_id': 'bbbb', + } + self.flags(deploy_kernel="fail", group='baremetal') + self.flags(deploy_ramdisk="fail", group='baremetal') + + self.assertEqual( + pxe.get_deploy_aki_id(self.instance), 'aaaa') + self.assertEqual( + pxe.get_deploy_ari_id(self.instance), 'bbbb') + + def test_get_default_deploy_ids(self): + self.instance['extra_specs'] = {} + self.flags(deploy_kernel="aaaa", group='baremetal') + self.flags(deploy_ramdisk="bbbb", group='baremetal') + + self.assertEqual( + pxe.get_deploy_aki_id(self.instance), 'aaaa') + self.assertEqual( + pxe.get_deploy_ari_id(self.instance), 'bbbb') + + def test_get_partition_sizes(self): + # m1.tiny: 10GB root, 0GB swap + self.instance['instance_type_id'] = 1 + sizes = pxe.get_partition_sizes(self.instance) + self.assertEqual(sizes[0], 10240) + self.assertEqual(sizes[1], 1) + + # kinda.big: 40GB root, 1GB swap + ref = utils.get_test_instance_type() + self.instance['instance_type_id'] = ref['id'] + self.instance['root_gb'] = ref['root_gb'] + sizes = pxe.get_partition_sizes(self.instance) + self.assertEqual(sizes[0], 40960) + self.assertEqual(sizes[1], 1024) + + def test_get_tftp_image_info(self): + # Raises an exception when options are neither specified + # on the instance nor in configuration file + CONF.baremetal.deploy_kernel = None + CONF.baremetal.deploy_ramdisk = None + self.assertRaises(exception.NovaException, + pxe.get_tftp_image_info, + self.instance) + + # Even if the instance includes kernel_id and ramdisk_id, + # we still need deploy_kernel_id and deploy_ramdisk_id. + # If those aren't present in instance[], and not specified in + # config file, then we raise an exception. + self.instance['kernel_id'] = 'aaaa' + self.instance['ramdisk_id'] = 'bbbb' + self.assertRaises(exception.NovaException, + pxe.get_tftp_image_info, + self.instance) + + # If an instance doesn't specify deploy_kernel_id or deploy_ramdisk_id, + # but defaults are set in the config file, we should use those. + + # Here, we confirm both that all four values were set + # and that the proper paths are getting set for all of them + CONF.baremetal.deploy_kernel = 'cccc' + CONF.baremetal.deploy_ramdisk = 'dddd' + base = os.path.join(CONF.baremetal.tftp_root, self.instance['uuid']) + res = pxe.get_tftp_image_info(self.instance) + expected = { + 'kernel': ['aaaa', os.path.join(base, 'kernel')], + 'ramdisk': ['bbbb', os.path.join(base, 'ramdisk')], + 'deploy_kernel': ['cccc', os.path.join(base, 'deploy_kernel')], + 'deploy_ramdisk': ['dddd', + os.path.join(base, 'deploy_ramdisk')], + } + self.assertEqual(res, expected) + + # If deploy_kernel_id and deploy_ramdisk_id are specified on + # image extra_specs, this should override any default configuration. + # Note that it is passed on the 'instance' object, despite being + # inherited from the instance_types_extra_specs table. + extra_specs = { + 'deploy_kernel_id': 'eeee', + 'deploy_ramdisk_id': 'ffff', + } + self.instance['extra_specs'] = extra_specs + res = pxe.get_tftp_image_info(self.instance) + self.assertEqual(res['deploy_kernel'][0], 'eeee') + self.assertEqual(res['deploy_ramdisk'][0], 'ffff') + + +class PXEPrivateMethodsTestCase(BareMetalPXETestCase): + + def test_collect_mac_addresses(self): + self._create_node() + address_list = [nic['address'] for nic in self.nic_info] + address_list.append(self.node_info['prov_mac_address']) + address_list.sort() + macs = self.driver._collect_mac_addresses(self.context, self.node) + self.assertEqual(macs, address_list) + + def test_generate_udev_rules(self): + self._create_node() + address_list = [nic['address'] for nic in self.nic_info] + address_list.append(self.node_info['prov_mac_address']) + + rules = self.driver._generate_udev_rules(self.context, self.node) + for address in address_list: + self.assertIn('ATTR{address}=="%s"' % address, rules) + + def test_cache_tftp_images(self): + self.instance['kernel_id'] = 'aaaa' + self.instance['ramdisk_id'] = 'bbbb' + extra_specs = { + 'deploy_kernel_id': 'cccc', + 'deploy_ramdisk_id': 'dddd', + } + self.instance['extra_specs'] = extra_specs + image_info = pxe.get_tftp_image_info(self.instance) + + self.mox.StubOutWithMock(os, 'makedirs') + self.mox.StubOutWithMock(os.path, 'exists') + os.makedirs(os.path.join(CONF.baremetal.tftp_root, + self.instance['uuid'])).AndReturn(True) + for uuid, path in [image_info[label] for label in image_info]: + os.path.exists(path).AndReturn(True) + self.mox.ReplayAll() + + self.driver._cache_tftp_images( + self.context, self.instance, image_info) + self.mox.VerifyAll() + + def test_cache_image(self): + self.mox.StubOutWithMock(os, 'makedirs') + self.mox.StubOutWithMock(os.path, 'exists') + os.makedirs(pxe.get_image_dir_path(self.instance)).\ + AndReturn(True) + os.path.exists(pxe.get_image_file_path(self.instance)).\ + AndReturn(True) + self.mox.ReplayAll() + + image_meta = utils.get_test_image_info( + self.context, self.instance) + self.driver._cache_image( + self.context, self.instance, image_meta) + self.mox.VerifyAll() + + def test_inject_into_image(self): + # NOTE(deva): we could also test this method by stubbing + # nova.virt.disk.api._inject_*_into_fs + self._create_node() + files = [] + files.append(('/etc/udev/rules.d/70-persistent-net.rules', + self.driver._generate_udev_rules(self.context, self.node))) + self.instance['hostname'] = 'fake hostname' + files.append(('/etc/hostname', 'fake hostname')) + self.instance['key_data'] = 'fake ssh key' + net_info = utils.get_test_network_info(1) + net = pxe.build_network_config(net_info) + admin_password = 'fake password' + + self.mox.StubOutWithMock(disk_api, 'inject_data') + disk_api.inject_data( + admin_password=admin_password, + image=pxe.get_image_file_path(self.instance), + key='fake ssh key', + metadata=None, + partition=None, + net=net, + files=files, # this is what we're really testing + ).AndReturn(True) + self.mox.ReplayAll() + + self.driver._inject_into_image( + self.context, self.node, self.instance, + network_info=net_info, + admin_password=admin_password, + injected_files=None) + self.mox.VerifyAll() + + +class PXEPublicMethodsTestCase(BareMetalPXETestCase): + + def test_cache_images(self): + self._create_node() + self.mox.StubOutWithMock(pxe, "get_tftp_image_info") + self.mox.StubOutWithMock(self.driver, "_cache_tftp_images") + self.mox.StubOutWithMock(self.driver, "_cache_image") + self.mox.StubOutWithMock(self.driver, "_inject_into_image") + + pxe.get_tftp_image_info(self.instance).AndReturn([]) + self.driver._cache_tftp_images(self.context, self.instance, []) + self.driver._cache_image(self.context, self.instance, []) + self.driver._inject_into_image(self.context, self.node, self.instance, + self.test_network_info, None, '') + self.mox.ReplayAll() + + self.driver.cache_images( + self.context, self.node, self.instance, + admin_password='', + image_meta=[], + injected_files=None, + network_info=self.test_network_info, + ) + self.mox.VerifyAll() + + def test_destroy_images(self): + self._create_node() + self.mox.StubOutWithMock(os, 'unlink') + + os.unlink(pxe.get_image_file_path(self.instance)) + os.unlink(pxe.get_image_dir_path(self.instance)) + self.mox.ReplayAll() + + self.driver.destroy_images(self.context, self.node, self.instance) + self.mox.VerifyAll() + + def test_activate_bootloader(self): + self._create_node() + macs = [nic['address'] for nic in self.nic_info] + macs.append(self.node_info['prov_mac_address']) + macs.sort() + image_info = { + 'deploy_kernel': [None, 'aaaa'], + 'deploy_ramdisk': [None, 'bbbb'], + 'kernel': [None, 'cccc'], + 'ramdisk': [None, 'dddd'], + } + self.instance['uuid'] = 'fake-uuid' + iqn = "iqn-%s" % self.instance['uuid'] + pxe_config = 'this is a fake pxe config' + pxe_path = pxe.get_pxe_config_file_path(self.instance) + image_path = pxe.get_image_file_path(self.instance) + + self.mox.StubOutWithMock(pxe, 'get_tftp_image_info') + self.mox.StubOutWithMock(pxe, 'get_partition_sizes') + self.mox.StubOutWithMock(bm_utils, 'random_alnum') + self.mox.StubOutWithMock(db, 'bm_deployment_create') + self.mox.StubOutWithMock(pxe, 'build_pxe_config') + self.mox.StubOutWithMock(bm_utils, 'write_to_file') + self.mox.StubOutWithMock(bm_utils, 'create_link_without_raise') + + pxe.get_tftp_image_info(self.instance).AndReturn(image_info) + pxe.get_partition_sizes(self.instance).AndReturn((0, 0)) + bm_utils.random_alnum(32).AndReturn('alnum') + db.bm_deployment_create( + self.context, 'alnum', image_path, pxe_path, 0, 0).\ + AndReturn(1234) + pxe.build_pxe_config( + 1234, 'alnum', iqn, 'aaaa', 'bbbb', 'cccc', 'dddd').\ + AndReturn(pxe_config) + bm_utils.write_to_file(pxe_path, pxe_config) + for mac in macs: + bm_utils.create_link_without_raise( + pxe_path, pxe.get_pxe_mac_path(mac)) + self.mox.ReplayAll() + + self.driver.activate_bootloader( + self.context, self.node, self.instance) + self.mox.VerifyAll() + + def test_deactivate_bootloader(self): + self._create_node() + macs = [nic['address'] for nic in self.nic_info] + macs.append(self.node_info['prov_mac_address']) + macs.sort() + image_info = { + 'deploy_kernel': [None, 'aaaa'], + 'deploy_ramdisk': [None, 'bbbb'], + 'kernel': [None, 'cccc'], + 'ramdisk': [None, 'dddd'], + } + self.instance['uuid'] = 'fake-uuid' + pxe_path = pxe.get_pxe_config_file_path(self.instance) + + self.mox.StubOutWithMock(bm_utils, 'unlink_without_raise') + self.mox.StubOutWithMock(pxe, 'get_tftp_image_info') + self.mox.StubOutWithMock(self.driver, '_collect_mac_addresses') + + pxe.get_tftp_image_info(self.instance).AndReturn(image_info) + for uuid, path in [image_info[label] for label in image_info]: + bm_utils.unlink_without_raise(path) + bm_utils.unlink_without_raise(pxe_path) + self.driver._collect_mac_addresses(self.context, self.node).\ + AndReturn(macs) + for mac in macs: + bm_utils.unlink_without_raise(pxe.get_pxe_mac_path(mac)) + bm_utils.unlink_without_raise( + os.path.join(CONF.baremetal.tftp_root, 'fake-uuid')) + self.mox.ReplayAll() + + self.driver.deactivate_bootloader( + self.context, self.node, self.instance) + self.mox.VerifyAll() + + def test_deactivate_bootloader_for_nonexistent_instance(self): + self._create_node() + macs = [nic['address'] for nic in self.nic_info] + macs.append(self.node_info['prov_mac_address']) + macs.sort() + image_info = { + 'deploy_kernel': [None, 'aaaa'], + 'deploy_ramdisk': [None, 'bbbb'], + 'kernel': [None, 'cccc'], + 'ramdisk': [None, 'dddd'], + } + self.instance['uuid'] = 'fake-uuid' + pxe_path = pxe.get_pxe_config_file_path(self.instance) + + self.mox.StubOutWithMock(bm_utils, 'unlink_without_raise') + self.mox.StubOutWithMock(pxe, 'get_tftp_image_info') + self.mox.StubOutWithMock(self.driver, '_collect_mac_addresses') + + pxe.get_tftp_image_info(self.instance).\ + AndRaise(exception.NovaException) + bm_utils.unlink_without_raise(pxe_path) + self.driver._collect_mac_addresses(self.context, self.node).\ + AndRaise(exception.DBError) + bm_utils.unlink_without_raise( + os.path.join(CONF.baremetal.tftp_root, 'fake-uuid')) + self.mox.ReplayAll() + + self.driver.deactivate_bootloader( + self.context, self.node, self.instance) + self.mox.VerifyAll() diff --git a/nova/virt/vif.py b/nova/tests/baremetal/test_utils.py index 69cfd996c..afba55e76 100644 --- a/nova/virt/vif.py +++ b/nova/tests/baremetal/test_utils.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# coding=utf-8 -# Copyright (C) 2011 Midokura KK +# Copyright 2012 Hewlett-Packard Development Company, L.P. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -15,20 +16,21 @@ # License for the specific language governing permissions and limitations # under the License. -"""VIF module common to all virt layers.""" +""" +Tests for baremetal utils +""" +import mox -class VIFDriver(object): - """Abstract class that defines generic interfaces for all VIF drivers.""" - def __init__(self, **kwargs): - # NOTE(jkoelker) __init__ is here so subclasses *could* take - # advantage of any kwargs should they need to - pass +from nova import exception +from nova import test +from nova.virt.baremetal import utils - def plug(self, instance, vif, **kwargs): - """Plug VIF into network.""" - raise NotImplementedError() - def unplug(self, instance, vif, **kwargs): - """Unplug VIF from network.""" - raise NotImplementedError() +class BareMetalUtilsTestCase(test.TestCase): + + def test_random_alnum(self): + s = utils.random_alnum(10) + self.assertEqual(len(s), 10) + s = utils.random_alnum(100) + self.assertEqual(len(s), 100) diff --git a/nova/tests/baremetal/test_volume_driver.py b/nova/tests/baremetal/test_volume_driver.py index e1b81d0b1..db0dd3591 100644 --- a/nova/tests/baremetal/test_volume_driver.py +++ b/nova/tests/baremetal/test_volume_driver.py @@ -17,11 +17,8 @@ Tests for baremetal volume driver. """ -import mox - from nova.openstack.common import cfg from nova import test -from nova import utils from nova.virt.baremetal import volume_driver diff --git a/nova/tests/cells/__init__.py b/nova/tests/cells/__init__.py new file mode 100644 index 000000000..d1bf725f7 --- /dev/null +++ b/nova/tests/cells/__init__.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 Rackspace Hosting +# 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. + +# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work +from nova.tests import * diff --git a/nova/tests/cells/fakes.py b/nova/tests/cells/fakes.py new file mode 100644 index 000000000..e1f3b6e70 --- /dev/null +++ b/nova/tests/cells/fakes.py @@ -0,0 +1,197 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. +""" +Fakes For Cells tests. +""" + +from nova.cells import driver +from nova.cells import manager as cells_manager +from nova.cells import messaging +from nova.cells import state as cells_state +import nova.db +from nova.db import base +from nova.openstack.common import cfg + +CONF = cfg.CONF +CONF.import_opt('name', 'nova.cells.opts', group='cells') + + +# Fake Cell Hierarchy +FAKE_TOP_LEVEL_CELL_NAME = 'api-cell' +FAKE_CELL_LAYOUT = [{'child-cell1': []}, + {'child-cell2': [{'grandchild-cell1': []}]}, + {'child-cell3': [{'grandchild-cell2': []}, + {'grandchild-cell3': []}]}, + {'child-cell4': []}] + +# build_cell_stub_infos() below will take the above layout and create +# a fake view of the DB from the perspective of each of the cells. +# For each cell, a CellStubInfo will be created with this info. +CELL_NAME_TO_STUB_INFO = {} + + +class FakeDBApi(object): + def __init__(self, cell_db_entries): + self.cell_db_entries = cell_db_entries + + def __getattr__(self, key): + return getattr(nova.db, key) + + def cell_get_all(self, ctxt): + return self.cell_db_entries + + def compute_node_get_all(self, ctxt): + return [] + + def instance_get_all_by_filters(self, ctxt, *args, **kwargs): + return [] + + def instance_get_by_uuid(self, ctxt, *args, **kwargs): + return None + + +class FakeCellsDriver(driver.BaseCellsDriver): + pass + + +class FakeCellState(cells_state.CellState): + def send_message(self, message): + message_runner = get_message_runner(self.name) + orig_ctxt = message.ctxt + json_message = message.to_json() + message = message_runner.message_from_json(json_message) + # Restore this so we can use mox and verify same context + message.ctxt = orig_ctxt + message.process() + + +class FakeCellStateManager(cells_state.CellStateManager): + def __init__(self, *args, **kwargs): + super(FakeCellStateManager, self).__init__(*args, + cell_state_cls=FakeCellState, **kwargs) + + +class FakeCellsManager(cells_manager.CellsManager): + def __init__(self, *args, **kwargs): + super(FakeCellsManager, self).__init__(*args, + cell_state_manager=FakeCellStateManager, + **kwargs) + + +class CellStubInfo(object): + def __init__(self, test_case, cell_name, db_entries): + self.test_case = test_case + self.cell_name = cell_name + self.db_entries = db_entries + + def fake_base_init(_self, *args, **kwargs): + _self.db = FakeDBApi(db_entries) + + test_case.stubs.Set(base.Base, '__init__', fake_base_init) + self.cells_manager = FakeCellsManager() + # Fix the cell name, as it normally uses CONF.cells.name + msg_runner = self.cells_manager.msg_runner + msg_runner.our_name = self.cell_name + self.cells_manager.state_manager.my_cell_state.name = self.cell_name + + +def _build_cell_stub_info(test_case, our_name, parent_path, children): + cell_db_entries = [] + cur_db_id = 1 + sep_char = messaging._PATH_CELL_SEP + if parent_path: + cell_db_entries.append( + dict(id=cur_db_id, + name=parent_path.split(sep_char)[-1], + is_parent=True, + username='username%s' % cur_db_id, + password='password%s' % cur_db_id, + rpc_host='rpc_host%s' % cur_db_id, + rpc_port='rpc_port%s' % cur_db_id, + rpc_virtual_host='rpc_vhost%s' % cur_db_id)) + cur_db_id += 1 + our_path = parent_path + sep_char + our_name + else: + our_path = our_name + for child in children: + for child_name, grandchildren in child.items(): + _build_cell_stub_info(test_case, child_name, our_path, + grandchildren) + cell_entry = dict(id=cur_db_id, + name=child_name, + username='username%s' % cur_db_id, + password='password%s' % cur_db_id, + rpc_host='rpc_host%s' % cur_db_id, + rpc_port='rpc_port%s' % cur_db_id, + rpc_virtual_host='rpc_vhost%s' % cur_db_id, + is_parent=False) + cell_db_entries.append(cell_entry) + cur_db_id += 1 + stub_info = CellStubInfo(test_case, our_name, cell_db_entries) + CELL_NAME_TO_STUB_INFO[our_name] = stub_info + + +def _build_cell_stub_infos(test_case): + _build_cell_stub_info(test_case, FAKE_TOP_LEVEL_CELL_NAME, '', + FAKE_CELL_LAYOUT) + + +def init(test_case): + global CELL_NAME_TO_STUB_INFO + test_case.flags(driver='nova.tests.cells.fakes.FakeCellsDriver', + group='cells') + CELL_NAME_TO_STUB_INFO = {} + _build_cell_stub_infos(test_case) + + +def _get_cell_stub_info(cell_name): + return CELL_NAME_TO_STUB_INFO[cell_name] + + +def get_state_manager(cell_name): + return _get_cell_stub_info(cell_name).cells_manager.state_manager + + +def get_cell_state(cur_cell_name, tgt_cell_name): + state_manager = get_state_manager(cur_cell_name) + cell = state_manager.child_cells.get(tgt_cell_name) + if cell is None: + cell = state_manager.parent_cells.get(tgt_cell_name) + return cell + + +def get_cells_manager(cell_name): + return _get_cell_stub_info(cell_name).cells_manager + + +def get_message_runner(cell_name): + return _get_cell_stub_info(cell_name).cells_manager.msg_runner + + +def stub_tgt_method(test_case, cell_name, method_name, method): + msg_runner = get_message_runner(cell_name) + tgt_msg_methods = msg_runner.methods_by_type['targeted'] + setattr(tgt_msg_methods, method_name, method) + + +def stub_bcast_method(test_case, cell_name, method_name, method): + msg_runner = get_message_runner(cell_name) + tgt_msg_methods = msg_runner.methods_by_type['broadcast'] + setattr(tgt_msg_methods, method_name, method) + + +def stub_bcast_methods(test_case, method_name, method): + for cell_name in CELL_NAME_TO_STUB_INFO.keys(): + stub_bcast_method(test_case, cell_name, method_name, method) diff --git a/nova/tests/cells/test_cells_manager.py b/nova/tests/cells/test_cells_manager.py new file mode 100644 index 000000000..d05bc4098 --- /dev/null +++ b/nova/tests/cells/test_cells_manager.py @@ -0,0 +1,213 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. +""" +Tests For CellsManager +""" +import datetime + +from nova.cells import messaging +from nova.cells import utils as cells_utils +from nova import context +from nova.openstack.common import timeutils +from nova import test +from nova.tests.cells import fakes + + +class CellsManagerClassTestCase(test.TestCase): + """Test case for CellsManager class""" + + def setUp(self): + super(CellsManagerClassTestCase, self).setUp() + fakes.init(self) + # pick a child cell to use for tests. + self.our_cell = 'grandchild-cell1' + self.cells_manager = fakes.get_cells_manager(self.our_cell) + self.msg_runner = self.cells_manager.msg_runner + self.driver = self.cells_manager.driver + self.ctxt = 'fake_context' + + def test_post_start_hook_child_cell(self): + self.mox.StubOutWithMock(self.driver, 'start_consumers') + self.mox.StubOutWithMock(context, 'get_admin_context') + self.mox.StubOutWithMock(self.cells_manager, '_update_our_parents') + + self.driver.start_consumers(self.msg_runner) + context.get_admin_context().AndReturn(self.ctxt) + self.cells_manager._update_our_parents(self.ctxt) + self.mox.ReplayAll() + self.cells_manager.post_start_hook() + + def test_post_start_hook_middle_cell(self): + cells_manager = fakes.get_cells_manager('child-cell2') + msg_runner = cells_manager.msg_runner + driver = cells_manager.driver + + self.mox.StubOutWithMock(driver, 'start_consumers') + self.mox.StubOutWithMock(context, 'get_admin_context') + self.mox.StubOutWithMock(msg_runner, + 'ask_children_for_capabilities') + self.mox.StubOutWithMock(msg_runner, + 'ask_children_for_capacities') + + driver.start_consumers(msg_runner) + context.get_admin_context().AndReturn(self.ctxt) + msg_runner.ask_children_for_capabilities(self.ctxt) + msg_runner.ask_children_for_capacities(self.ctxt) + self.mox.ReplayAll() + cells_manager.post_start_hook() + + def test_update_our_parents(self): + self.mox.StubOutWithMock(self.msg_runner, + 'tell_parents_our_capabilities') + self.mox.StubOutWithMock(self.msg_runner, + 'tell_parents_our_capacities') + + self.msg_runner.tell_parents_our_capabilities(self.ctxt) + self.msg_runner.tell_parents_our_capacities(self.ctxt) + self.mox.ReplayAll() + self.cells_manager._update_our_parents(self.ctxt) + + def test_schedule_run_instance(self): + host_sched_kwargs = 'fake_host_sched_kwargs_silently_passed' + self.mox.StubOutWithMock(self.msg_runner, 'schedule_run_instance') + our_cell = self.msg_runner.state_manager.get_my_state() + self.msg_runner.schedule_run_instance(self.ctxt, our_cell, + host_sched_kwargs) + self.mox.ReplayAll() + self.cells_manager.schedule_run_instance(self.ctxt, + host_sched_kwargs=host_sched_kwargs) + + def test_run_compute_api_method(self): + # Args should just be silently passed through + cell_name = 'fake-cell-name' + method_info = 'fake-method-info' + + fake_response = messaging.Response('fake', 'fake', False) + + self.mox.StubOutWithMock(self.msg_runner, + 'run_compute_api_method') + self.mox.StubOutWithMock(fake_response, + 'value_or_raise') + self.msg_runner.run_compute_api_method(self.ctxt, + cell_name, + method_info, + True).AndReturn(fake_response) + fake_response.value_or_raise().AndReturn('fake-response') + self.mox.ReplayAll() + response = self.cells_manager.run_compute_api_method( + self.ctxt, cell_name=cell_name, method_info=method_info, + call=True) + self.assertEqual('fake-response', response) + + def test_instance_update_at_top(self): + self.mox.StubOutWithMock(self.msg_runner, 'instance_update_at_top') + self.msg_runner.instance_update_at_top(self.ctxt, 'fake-instance') + self.mox.ReplayAll() + self.cells_manager.instance_update_at_top(self.ctxt, + instance='fake-instance') + + def test_instance_destroy_at_top(self): + self.mox.StubOutWithMock(self.msg_runner, 'instance_destroy_at_top') + self.msg_runner.instance_destroy_at_top(self.ctxt, 'fake-instance') + self.mox.ReplayAll() + self.cells_manager.instance_destroy_at_top(self.ctxt, + instance='fake-instance') + + def test_instance_delete_everywhere(self): + self.mox.StubOutWithMock(self.msg_runner, + 'instance_delete_everywhere') + self.msg_runner.instance_delete_everywhere(self.ctxt, + 'fake-instance', + 'fake-type') + self.mox.ReplayAll() + self.cells_manager.instance_delete_everywhere( + self.ctxt, instance='fake-instance', + delete_type='fake-type') + + def test_instance_fault_create_at_top(self): + self.mox.StubOutWithMock(self.msg_runner, + 'instance_fault_create_at_top') + self.msg_runner.instance_fault_create_at_top(self.ctxt, + 'fake-fault') + self.mox.ReplayAll() + self.cells_manager.instance_fault_create_at_top( + self.ctxt, instance_fault='fake-fault') + + def test_bw_usage_update_at_top(self): + self.mox.StubOutWithMock(self.msg_runner, + 'bw_usage_update_at_top') + self.msg_runner.bw_usage_update_at_top(self.ctxt, + 'fake-bw-info') + self.mox.ReplayAll() + self.cells_manager.bw_usage_update_at_top( + self.ctxt, bw_update_info='fake-bw-info') + + def test_heal_instances(self): + self.flags(instance_updated_at_threshold=1000, + instance_update_num_instances=2, + group='cells') + + fake_context = context.RequestContext('fake', 'fake') + stalled_time = timeutils.utcnow() + updated_since = stalled_time - datetime.timedelta(seconds=1000) + + def utcnow(): + return stalled_time + + call_info = {'get_instances': 0, 'sync_instances': []} + + instances = ['instance1', 'instance2', 'instance3'] + + def get_instances_to_sync(context, **kwargs): + self.assertEqual(context, fake_context) + call_info['shuffle'] = kwargs.get('shuffle') + call_info['project_id'] = kwargs.get('project_id') + call_info['updated_since'] = kwargs.get('updated_since') + call_info['get_instances'] += 1 + return iter(instances) + + def instance_get_by_uuid(context, uuid): + return instances[int(uuid[-1]) - 1] + + def sync_instance(context, instance): + self.assertEqual(context, fake_context) + call_info['sync_instances'].append(instance) + + self.stubs.Set(cells_utils, 'get_instances_to_sync', + get_instances_to_sync) + self.stubs.Set(self.cells_manager.db, 'instance_get_by_uuid', + instance_get_by_uuid) + self.stubs.Set(self.cells_manager, '_sync_instance', + sync_instance) + self.stubs.Set(timeutils, 'utcnow', utcnow) + + self.cells_manager._heal_instances(fake_context) + self.assertEqual(call_info['shuffle'], True) + self.assertEqual(call_info['project_id'], None) + self.assertEqual(call_info['updated_since'], updated_since) + self.assertEqual(call_info['get_instances'], 1) + # Only first 2 + self.assertEqual(call_info['sync_instances'], + instances[:2]) + + call_info['sync_instances'] = [] + self.cells_manager._heal_instances(fake_context) + self.assertEqual(call_info['shuffle'], True) + self.assertEqual(call_info['project_id'], None) + self.assertEqual(call_info['updated_since'], updated_since) + self.assertEqual(call_info['get_instances'], 2) + # Now the last 1 and the first 1 + self.assertEqual(call_info['sync_instances'], + [instances[-1], instances[0]]) diff --git a/nova/tests/cells/test_cells_messaging.py b/nova/tests/cells/test_cells_messaging.py new file mode 100644 index 000000000..d728c9474 --- /dev/null +++ b/nova/tests/cells/test_cells_messaging.py @@ -0,0 +1,913 @@ +# Copyright (c) 2012 Rackspace Hosting # 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. +""" +Tests For Cells Messaging module +""" + +from nova.cells import messaging +from nova import context +from nova import exception +from nova.openstack.common import cfg +from nova import test +from nova.tests.cells import fakes + + +CONF = cfg.CONF +CONF.import_opt('host', 'nova.config') +CONF.import_opt('name', 'nova.cells.opts', group='cells') +CONF.import_opt('allowed_rpc_exception_modules', + 'nova.openstack.common.rpc') + + +class CellsMessageClassesTestCase(test.TestCase): + """Test case for the main Cells Message classes.""" + def setUp(self): + super(CellsMessageClassesTestCase, self).setUp() + fakes.init(self) + self.ctxt = context.RequestContext('fake', 'fake') + # Need to be able to deserialize test.TestingException. + allowed_modules = CONF.allowed_rpc_exception_modules + allowed_modules.append('nova.test') + self.flags(allowed_rpc_exception_modules=allowed_modules) + self.our_name = 'api-cell' + self.msg_runner = fakes.get_message_runner(self.our_name) + self.state_manager = self.msg_runner.state_manager + + def test_reverse_path(self): + path = 'a!b!c!d' + expected = 'd!c!b!a' + rev_path = messaging._reverse_path(path) + self.assertEqual(rev_path, expected) + + def test_response_cell_name_from_path(self): + # test array with tuples of inputs/expected outputs + test_paths = [('cell1', 'cell1'), + ('cell1!cell2', 'cell2!cell1'), + ('cell1!cell2!cell3', 'cell3!cell2!cell1')] + + for test_input, expected_output in test_paths: + self.assertEqual(expected_output, + messaging._response_cell_name_from_path(test_input)) + + def test_response_cell_name_from_path_neighbor_only(self): + # test array with tuples of inputs/expected outputs + test_paths = [('cell1', 'cell1'), + ('cell1!cell2', 'cell2!cell1'), + ('cell1!cell2!cell3', 'cell3!cell2')] + + for test_input, expected_output in test_paths: + self.assertEqual(expected_output, + messaging._response_cell_name_from_path(test_input, + neighbor_only=True)) + + def test_targeted_message(self): + self.flags(max_hop_count=99, group='cells') + target_cell = 'api-cell!child-cell2!grandchild-cell1' + method = 'fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + tgt_message = messaging._TargetedMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, direction, + target_cell) + self.assertEqual(self.ctxt, tgt_message.ctxt) + self.assertEqual(method, tgt_message.method_name) + self.assertEqual(method_kwargs, tgt_message.method_kwargs) + self.assertEqual(direction, tgt_message.direction) + self.assertEqual(target_cell, target_cell) + self.assertFalse(tgt_message.fanout) + self.assertFalse(tgt_message.need_response) + self.assertEqual(self.our_name, tgt_message.routing_path) + self.assertEqual(1, tgt_message.hop_count) + self.assertEqual(99, tgt_message.max_hop_count) + self.assertFalse(tgt_message.is_broadcast) + # Correct next hop? + next_hop = tgt_message._get_next_hop() + child_cell = self.state_manager.get_child_cell('child-cell2') + self.assertEqual(child_cell, next_hop) + + def test_create_targeted_message_with_response(self): + self.flags(max_hop_count=99, group='cells') + our_name = 'child-cell1' + target_cell = 'child-cell1!api-cell' + msg_runner = fakes.get_message_runner(our_name) + method = 'fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'up' + tgt_message = messaging._TargetedMessage(msg_runner, + self.ctxt, method, + method_kwargs, direction, + target_cell, + need_response=True) + self.assertEqual(self.ctxt, tgt_message.ctxt) + self.assertEqual(method, tgt_message.method_name) + self.assertEqual(method_kwargs, tgt_message.method_kwargs) + self.assertEqual(direction, tgt_message.direction) + self.assertEqual(target_cell, target_cell) + self.assertFalse(tgt_message.fanout) + self.assertTrue(tgt_message.need_response) + self.assertEqual(our_name, tgt_message.routing_path) + self.assertEqual(1, tgt_message.hop_count) + self.assertEqual(99, tgt_message.max_hop_count) + self.assertFalse(tgt_message.is_broadcast) + # Correct next hop? + next_hop = tgt_message._get_next_hop() + parent_cell = msg_runner.state_manager.get_parent_cell('api-cell') + self.assertEqual(parent_cell, next_hop) + + def test_targeted_message_when_target_is_cell_state(self): + method = 'fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + target_cell = self.state_manager.get_child_cell('child-cell2') + tgt_message = messaging._TargetedMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, direction, + target_cell) + self.assertEqual('api-cell!child-cell2', tgt_message.target_cell) + # Correct next hop? + next_hop = tgt_message._get_next_hop() + self.assertEqual(target_cell, next_hop) + + def test_targeted_message_when_target_cell_state_is_me(self): + method = 'fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + target_cell = self.state_manager.get_my_state() + tgt_message = messaging._TargetedMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, direction, + target_cell) + self.assertEqual('api-cell', tgt_message.target_cell) + # Correct next hop? + next_hop = tgt_message._get_next_hop() + self.assertEqual(target_cell, next_hop) + + def test_create_broadcast_message(self): + self.flags(max_hop_count=99, group='cells') + self.flags(name='api-cell', max_hop_count=99, group='cells') + method = 'fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + bcast_message = messaging._BroadcastMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, direction) + self.assertEqual(self.ctxt, bcast_message.ctxt) + self.assertEqual(method, bcast_message.method_name) + self.assertEqual(method_kwargs, bcast_message.method_kwargs) + self.assertEqual(direction, bcast_message.direction) + self.assertFalse(bcast_message.fanout) + self.assertFalse(bcast_message.need_response) + self.assertEqual(self.our_name, bcast_message.routing_path) + self.assertEqual(1, bcast_message.hop_count) + self.assertEqual(99, bcast_message.max_hop_count) + self.assertTrue(bcast_message.is_broadcast) + # Correct next hops? + next_hops = bcast_message._get_next_hops() + child_cells = self.state_manager.get_child_cells() + self.assertEqual(child_cells, next_hops) + + def test_create_broadcast_message_with_response(self): + self.flags(max_hop_count=99, group='cells') + our_name = 'child-cell1' + msg_runner = fakes.get_message_runner(our_name) + method = 'fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'up' + bcast_message = messaging._BroadcastMessage(msg_runner, self.ctxt, + method, method_kwargs, direction, need_response=True) + self.assertEqual(self.ctxt, bcast_message.ctxt) + self.assertEqual(method, bcast_message.method_name) + self.assertEqual(method_kwargs, bcast_message.method_kwargs) + self.assertEqual(direction, bcast_message.direction) + self.assertFalse(bcast_message.fanout) + self.assertTrue(bcast_message.need_response) + self.assertEqual(our_name, bcast_message.routing_path) + self.assertEqual(1, bcast_message.hop_count) + self.assertEqual(99, bcast_message.max_hop_count) + self.assertTrue(bcast_message.is_broadcast) + # Correct next hops? + next_hops = bcast_message._get_next_hops() + parent_cells = msg_runner.state_manager.get_parent_cells() + self.assertEqual(parent_cells, next_hops) + + def test_self_targeted_message(self): + target_cell = 'api-cell' + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + call_info = {} + + def our_fake_method(message, **kwargs): + call_info['context'] = message.ctxt + call_info['routing_path'] = message.routing_path + call_info['kwargs'] = kwargs + + fakes.stub_tgt_method(self, 'api-cell', 'our_fake_method', + our_fake_method) + + tgt_message = messaging._TargetedMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, direction, + target_cell) + tgt_message.process() + + self.assertEqual(self.ctxt, call_info['context']) + self.assertEqual(method_kwargs, call_info['kwargs']) + self.assertEqual(target_cell, call_info['routing_path']) + + def test_child_targeted_message(self): + target_cell = 'api-cell!child-cell1' + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + call_info = {} + + def our_fake_method(message, **kwargs): + call_info['context'] = message.ctxt + call_info['routing_path'] = message.routing_path + call_info['kwargs'] = kwargs + + fakes.stub_tgt_method(self, 'child-cell1', 'our_fake_method', + our_fake_method) + + tgt_message = messaging._TargetedMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, direction, + target_cell) + tgt_message.process() + + self.assertEqual(self.ctxt, call_info['context']) + self.assertEqual(method_kwargs, call_info['kwargs']) + self.assertEqual(target_cell, call_info['routing_path']) + + def test_grandchild_targeted_message(self): + target_cell = 'api-cell!child-cell2!grandchild-cell1' + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + call_info = {} + + def our_fake_method(message, **kwargs): + call_info['context'] = message.ctxt + call_info['routing_path'] = message.routing_path + call_info['kwargs'] = kwargs + + fakes.stub_tgt_method(self, 'grandchild-cell1', 'our_fake_method', + our_fake_method) + + tgt_message = messaging._TargetedMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, direction, + target_cell) + tgt_message.process() + + self.assertEqual(self.ctxt, call_info['context']) + self.assertEqual(method_kwargs, call_info['kwargs']) + self.assertEqual(target_cell, call_info['routing_path']) + + def test_grandchild_targeted_message_with_response(self): + target_cell = 'api-cell!child-cell2!grandchild-cell1' + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + call_info = {} + + def our_fake_method(message, **kwargs): + call_info['context'] = message.ctxt + call_info['routing_path'] = message.routing_path + call_info['kwargs'] = kwargs + return 'our_fake_response' + + fakes.stub_tgt_method(self, 'grandchild-cell1', 'our_fake_method', + our_fake_method) + + tgt_message = messaging._TargetedMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, direction, + target_cell, + need_response=True) + response = tgt_message.process() + + self.assertEqual(self.ctxt, call_info['context']) + self.assertEqual(method_kwargs, call_info['kwargs']) + self.assertEqual(target_cell, call_info['routing_path']) + self.assertFalse(response.failure) + self.assertTrue(response.value_or_raise(), 'our_fake_response') + + def test_grandchild_targeted_message_with_error(self): + target_cell = 'api-cell!child-cell2!grandchild-cell1' + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + def our_fake_method(message, **kwargs): + raise test.TestingException('this should be returned') + + fakes.stub_tgt_method(self, 'grandchild-cell1', 'our_fake_method', + our_fake_method) + + tgt_message = messaging._TargetedMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, direction, + target_cell, + need_response=True) + response = tgt_message.process() + self.assertTrue(response.failure) + self.assertRaises(test.TestingException, response.value_or_raise) + + def test_grandchild_targeted_message_max_hops(self): + self.flags(max_hop_count=2, group='cells') + target_cell = 'api-cell!child-cell2!grandchild-cell1' + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + def our_fake_method(message, **kwargs): + raise test.TestingException('should not be reached') + + fakes.stub_tgt_method(self, 'grandchild-cell1', 'our_fake_method', + our_fake_method) + + tgt_message = messaging._TargetedMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, direction, + target_cell, + need_response=True) + response = tgt_message.process() + self.assertTrue(response.failure) + self.assertRaises(exception.CellMaxHopCountReached, + response.value_or_raise) + + def test_targeted_message_invalid_cell(self): + target_cell = 'api-cell!child-cell2!grandchild-cell4' + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + tgt_message = messaging._TargetedMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, direction, + target_cell, + need_response=True) + response = tgt_message.process() + self.assertTrue(response.failure) + self.assertRaises(exception.CellRoutingInconsistency, + response.value_or_raise) + + def test_targeted_message_invalid_cell2(self): + target_cell = 'unknown-cell!child-cell2' + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + tgt_message = messaging._TargetedMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, direction, + target_cell, + need_response=True) + response = tgt_message.process() + self.assertTrue(response.failure) + self.assertRaises(exception.CellRoutingInconsistency, + response.value_or_raise) + + def test_broadcast_routing(self): + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + cells = set() + + def our_fake_method(message, **kwargs): + cells.add(message.routing_path) + + fakes.stub_bcast_methods(self, 'our_fake_method', our_fake_method) + + bcast_message = messaging._BroadcastMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, + direction, + run_locally=True) + bcast_message.process() + # fakes creates 8 cells (including ourself). + self.assertEqual(len(cells), 8) + + def test_broadcast_routing_up(self): + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'up' + msg_runner = fakes.get_message_runner('grandchild-cell3') + + cells = set() + + def our_fake_method(message, **kwargs): + cells.add(message.routing_path) + + fakes.stub_bcast_methods(self, 'our_fake_method', our_fake_method) + + bcast_message = messaging._BroadcastMessage(msg_runner, self.ctxt, + method, method_kwargs, + direction, + run_locally=True) + bcast_message.process() + # Paths are reversed, since going 'up' + expected = set(['grandchild-cell3', 'grandchild-cell3!child-cell3', + 'grandchild-cell3!child-cell3!api-cell']) + self.assertEqual(expected, cells) + + def test_broadcast_routing_without_ourselves(self): + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + cells = set() + + def our_fake_method(message, **kwargs): + cells.add(message.routing_path) + + fakes.stub_bcast_methods(self, 'our_fake_method', our_fake_method) + + bcast_message = messaging._BroadcastMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, + direction, + run_locally=False) + bcast_message.process() + # fakes creates 8 cells (including ourself). So we should see + # only 7 here. + self.assertEqual(len(cells), 7) + + def test_broadcast_routing_with_response(self): + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + def our_fake_method(message, **kwargs): + return 'response-%s' % message.routing_path + + fakes.stub_bcast_methods(self, 'our_fake_method', our_fake_method) + + bcast_message = messaging._BroadcastMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, + direction, + run_locally=True, + need_response=True) + responses = bcast_message.process() + self.assertEqual(len(responses), 8) + for response in responses: + self.assertFalse(response.failure) + self.assertEqual('response-%s' % response.cell_name, + response.value_or_raise()) + + def test_broadcast_routing_with_response_max_hops(self): + self.flags(max_hop_count=2, group='cells') + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + def our_fake_method(message, **kwargs): + return 'response-%s' % message.routing_path + + fakes.stub_bcast_methods(self, 'our_fake_method', our_fake_method) + + bcast_message = messaging._BroadcastMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, + direction, + run_locally=True, + need_response=True) + responses = bcast_message.process() + # Should only get responses from our immediate children (and + # ourselves) + self.assertEqual(len(responses), 5) + for response in responses: + self.assertFalse(response.failure) + self.assertEqual('response-%s' % response.cell_name, + response.value_or_raise()) + + def test_broadcast_routing_with_all_erroring(self): + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + def our_fake_method(message, **kwargs): + raise test.TestingException('fake failure') + + fakes.stub_bcast_methods(self, 'our_fake_method', our_fake_method) + + bcast_message = messaging._BroadcastMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, + direction, + run_locally=True, + need_response=True) + responses = bcast_message.process() + self.assertEqual(len(responses), 8) + for response in responses: + self.assertTrue(response.failure) + self.assertRaises(test.TestingException, response.value_or_raise) + + def test_broadcast_routing_with_two_erroring(self): + method = 'our_fake_method' + method_kwargs = dict(arg1=1, arg2=2) + direction = 'down' + + def our_fake_method_failing(message, **kwargs): + raise test.TestingException('fake failure') + + def our_fake_method(message, **kwargs): + return 'response-%s' % message.routing_path + + fakes.stub_bcast_methods(self, 'our_fake_method', our_fake_method) + fakes.stub_bcast_method(self, 'child-cell2', 'our_fake_method', + our_fake_method_failing) + fakes.stub_bcast_method(self, 'grandchild-cell3', 'our_fake_method', + our_fake_method_failing) + + bcast_message = messaging._BroadcastMessage(self.msg_runner, + self.ctxt, method, + method_kwargs, + direction, + run_locally=True, + need_response=True) + responses = bcast_message.process() + self.assertEqual(len(responses), 8) + failure_responses = [resp for resp in responses if resp.failure] + success_responses = [resp for resp in responses if not resp.failure] + self.assertEqual(len(failure_responses), 2) + self.assertEqual(len(success_responses), 6) + + for response in success_responses: + self.assertFalse(response.failure) + self.assertEqual('response-%s' % response.cell_name, + response.value_or_raise()) + + for response in failure_responses: + self.assertIn(response.cell_name, ['api-cell!child-cell2', + 'api-cell!child-cell3!grandchild-cell3']) + self.assertTrue(response.failure) + self.assertRaises(test.TestingException, response.value_or_raise) + + +class CellsTargetedMethodsTestCase(test.TestCase): + """Test case for _TargetedMessageMethods class. Most of these + tests actually test the full path from the MessageRunner through + to the functionality of the message method. Hits 2 birds with 1 + stone, even though it's a little more than a unit test. + """ + def setUp(self): + super(CellsTargetedMethodsTestCase, self).setUp() + fakes.init(self) + self.ctxt = context.RequestContext('fake', 'fake') + self._setup_attrs('api-cell', 'api-cell!child-cell2') + + def _setup_attrs(self, source_cell, target_cell): + self.tgt_cell_name = target_cell + self.src_msg_runner = fakes.get_message_runner(source_cell) + self.src_state_manager = self.src_msg_runner.state_manager + tgt_shortname = target_cell.split('!')[-1] + self.tgt_cell_mgr = fakes.get_cells_manager(tgt_shortname) + self.tgt_msg_runner = self.tgt_cell_mgr.msg_runner + self.tgt_scheduler = self.tgt_msg_runner.scheduler + self.tgt_state_manager = self.tgt_msg_runner.state_manager + methods_cls = self.tgt_msg_runner.methods_by_type['targeted'] + self.tgt_methods_cls = methods_cls + self.tgt_compute_api = methods_cls.compute_api + self.tgt_db_inst = methods_cls.db + + def test_schedule_run_instance(self): + host_sched_kwargs = {'filter_properties': {}, + 'key1': 'value1', + 'key2': 'value2'} + self.mox.StubOutWithMock(self.tgt_scheduler, 'run_instance') + self.tgt_scheduler.run_instance(self.ctxt, host_sched_kwargs) + self.mox.ReplayAll() + self.src_msg_runner.schedule_run_instance(self.ctxt, + self.tgt_cell_name, + host_sched_kwargs) + + def test_call_compute_api_method(self): + + instance_uuid = 'fake_instance_uuid' + method_info = {'method': 'reboot', + 'method_args': (instance_uuid, 2, 3), + 'method_kwargs': {'arg1': 'val1', 'arg2': 'val2'}} + self.mox.StubOutWithMock(self.tgt_compute_api, 'reboot') + self.mox.StubOutWithMock(self.tgt_db_inst, 'instance_get_by_uuid') + + self.tgt_db_inst.instance_get_by_uuid(self.ctxt, + instance_uuid).AndReturn( + 'fake_instance') + self.tgt_compute_api.reboot(self.ctxt, 'fake_instance', 2, 3, + arg1='val1', arg2='val2').AndReturn('fake_result') + self.mox.ReplayAll() + + response = self.src_msg_runner.run_compute_api_method( + self.ctxt, + self.tgt_cell_name, + method_info, + True) + result = response.value_or_raise() + self.assertEqual('fake_result', result) + + def test_call_compute_api_method_unknown_instance(self): + # Unknown instance should send a broadcast up that instance + # is gone. + instance_uuid = 'fake_instance_uuid' + instance = {'uuid': instance_uuid} + method_info = {'method': 'reboot', + 'method_args': (instance_uuid, 2, 3), + 'method_kwargs': {'arg1': 'val1', 'arg2': 'val2'}} + + self.mox.StubOutWithMock(self.tgt_db_inst, 'instance_get_by_uuid') + self.mox.StubOutWithMock(self.tgt_msg_runner, + 'instance_destroy_at_top') + + self.tgt_db_inst.instance_get_by_uuid(self.ctxt, + 'fake_instance_uuid').AndRaise( + exception.InstanceNotFound(instance_id=instance_uuid)) + self.tgt_msg_runner.instance_destroy_at_top(self.ctxt, instance) + + self.mox.ReplayAll() + + response = self.src_msg_runner.run_compute_api_method( + self.ctxt, + self.tgt_cell_name, + method_info, + True) + self.assertRaises(exception.InstanceNotFound, + response.value_or_raise) + + def test_update_capabilities(self): + # Route up to API + self._setup_attrs('child-cell2', 'child-cell2!api-cell') + capabs = {'cap1': set(['val1', 'val2']), + 'cap2': set(['val3'])} + # The list(set([])) seems silly, but we can't assume the order + # of the list... This behavior should match the code we're + # testing... which is check that a set was converted to a list. + expected_capabs = {'cap1': list(set(['val1', 'val2'])), + 'cap2': ['val3']} + self.mox.StubOutWithMock(self.src_state_manager, + 'get_our_capabilities') + self.mox.StubOutWithMock(self.tgt_state_manager, + 'update_cell_capabilities') + self.mox.StubOutWithMock(self.tgt_msg_runner, + 'tell_parents_our_capabilities') + self.src_state_manager.get_our_capabilities().AndReturn(capabs) + self.tgt_state_manager.update_cell_capabilities('child-cell2', + expected_capabs) + self.tgt_msg_runner.tell_parents_our_capabilities(self.ctxt) + + self.mox.ReplayAll() + + self.src_msg_runner.tell_parents_our_capabilities(self.ctxt) + + def test_update_capacities(self): + self._setup_attrs('child-cell2', 'child-cell2!api-cell') + capacs = 'fake_capacs' + self.mox.StubOutWithMock(self.src_state_manager, + 'get_our_capacities') + self.mox.StubOutWithMock(self.tgt_state_manager, + 'update_cell_capacities') + self.mox.StubOutWithMock(self.tgt_msg_runner, + 'tell_parents_our_capacities') + self.src_state_manager.get_our_capacities().AndReturn(capacs) + self.tgt_state_manager.update_cell_capacities('child-cell2', + capacs) + self.tgt_msg_runner.tell_parents_our_capacities(self.ctxt) + + self.mox.ReplayAll() + + self.src_msg_runner.tell_parents_our_capacities(self.ctxt) + + def test_announce_capabilities(self): + self._setup_attrs('api-cell', 'api-cell!child-cell1') + # To make this easier to test, make us only have 1 child cell. + cell_state = self.src_state_manager.child_cells['child-cell1'] + self.src_state_manager.child_cells = {'child-cell1': cell_state} + + self.mox.StubOutWithMock(self.tgt_msg_runner, + 'tell_parents_our_capabilities') + self.tgt_msg_runner.tell_parents_our_capabilities(self.ctxt) + + self.mox.ReplayAll() + + self.src_msg_runner.ask_children_for_capabilities(self.ctxt) + + def test_announce_capacities(self): + self._setup_attrs('api-cell', 'api-cell!child-cell1') + # To make this easier to test, make us only have 1 child cell. + cell_state = self.src_state_manager.child_cells['child-cell1'] + self.src_state_manager.child_cells = {'child-cell1': cell_state} + + self.mox.StubOutWithMock(self.tgt_msg_runner, + 'tell_parents_our_capacities') + self.tgt_msg_runner.tell_parents_our_capacities(self.ctxt) + + self.mox.ReplayAll() + + self.src_msg_runner.ask_children_for_capacities(self.ctxt) + + +class CellsBroadcastMethodsTestCase(test.TestCase): + """Test case for _BroadcastMessageMethods class. Most of these + tests actually test the full path from the MessageRunner through + to the functionality of the message method. Hits 2 birds with 1 + stone, even though it's a little more than a unit test. + """ + + def setUp(self): + super(CellsBroadcastMethodsTestCase, self).setUp() + fakes.init(self) + self.ctxt = context.RequestContext('fake', 'fake') + self._setup_attrs() + + def _setup_attrs(self, up=True): + mid_cell = 'child-cell2' + if up: + src_cell = 'grandchild-cell1' + tgt_cell = 'api-cell' + else: + src_cell = 'api-cell' + tgt_cell = 'grandchild-cell1' + + self.src_msg_runner = fakes.get_message_runner(src_cell) + methods_cls = self.src_msg_runner.methods_by_type['broadcast'] + self.src_methods_cls = methods_cls + self.src_db_inst = methods_cls.db + self.src_compute_api = methods_cls.compute_api + + self.mid_msg_runner = fakes.get_message_runner(mid_cell) + methods_cls = self.mid_msg_runner.methods_by_type['broadcast'] + self.mid_methods_cls = methods_cls + self.mid_db_inst = methods_cls.db + self.mid_compute_api = methods_cls.compute_api + + self.tgt_msg_runner = fakes.get_message_runner(tgt_cell) + methods_cls = self.tgt_msg_runner.methods_by_type['broadcast'] + self.tgt_methods_cls = methods_cls + self.tgt_db_inst = methods_cls.db + self.tgt_compute_api = methods_cls.compute_api + + def test_at_the_top(self): + self.assertTrue(self.tgt_methods_cls._at_the_top()) + self.assertFalse(self.mid_methods_cls._at_the_top()) + self.assertFalse(self.src_methods_cls._at_the_top()) + + def test_instance_update_at_top(self): + fake_info_cache = {'id': 1, + 'instance': 'fake_instance', + 'other': 'moo'} + fake_sys_metadata = [{'id': 1, + 'key': 'key1', + 'value': 'value1'}, + {'id': 2, + 'key': 'key2', + 'value': 'value2'}] + fake_instance = {'id': 2, + 'uuid': 'fake_uuid', + 'security_groups': 'fake', + 'instance_type': 'fake', + 'volumes': 'fake', + 'cell_name': 'fake', + 'name': 'fake', + 'metadata': 'fake', + 'info_cache': fake_info_cache, + 'system_metadata': fake_sys_metadata, + 'other': 'meow'} + expected_sys_metadata = {'key1': 'value1', + 'key2': 'value2'} + expected_info_cache = {'other': 'moo'} + expected_instance = {'system_metadata': expected_sys_metadata, + 'other': 'meow', + 'uuid': 'fake_uuid'} + + # To show these should not be called in src/mid-level cell + self.mox.StubOutWithMock(self.src_db_inst, 'instance_update') + self.mox.StubOutWithMock(self.src_db_inst, + 'instance_info_cache_update') + self.mox.StubOutWithMock(self.mid_db_inst, 'instance_update') + self.mox.StubOutWithMock(self.mid_db_inst, + 'instance_info_cache_update') + + self.mox.StubOutWithMock(self.tgt_db_inst, 'instance_update') + self.mox.StubOutWithMock(self.tgt_db_inst, + 'instance_info_cache_update') + self.tgt_db_inst.instance_update(self.ctxt, 'fake_uuid', + expected_instance, + update_cells=False) + self.tgt_db_inst.instance_info_cache_update(self.ctxt, 'fake_uuid', + expected_info_cache, + update_cells=False) + self.mox.ReplayAll() + + self.src_msg_runner.instance_update_at_top(self.ctxt, fake_instance) + + def test_instance_destroy_at_top(self): + fake_instance = {'uuid': 'fake_uuid'} + + # To show these should not be called in src/mid-level cell + self.mox.StubOutWithMock(self.src_db_inst, 'instance_destroy') + + self.mox.StubOutWithMock(self.tgt_db_inst, 'instance_destroy') + self.tgt_db_inst.instance_destroy(self.ctxt, 'fake_uuid', + update_cells=False) + self.mox.ReplayAll() + + self.src_msg_runner.instance_destroy_at_top(self.ctxt, fake_instance) + + def test_instance_hard_delete_everywhere(self): + # Reset this, as this is a broadcast down. + self._setup_attrs(up=False) + instance = {'uuid': 'meow'} + + # Should not be called in src (API cell) + self.mox.StubOutWithMock(self.src_compute_api, 'delete') + + self.mox.StubOutWithMock(self.mid_compute_api, 'delete') + self.mox.StubOutWithMock(self.tgt_compute_api, 'delete') + + self.mid_compute_api.delete(self.ctxt, instance) + self.tgt_compute_api.delete(self.ctxt, instance) + + self.mox.ReplayAll() + + self.src_msg_runner.instance_delete_everywhere(self.ctxt, + instance, 'hard') + + def test_instance_soft_delete_everywhere(self): + # Reset this, as this is a broadcast down. + self._setup_attrs(up=False) + instance = {'uuid': 'meow'} + + # Should not be called in src (API cell) + self.mox.StubOutWithMock(self.src_compute_api, 'soft_delete') + + self.mox.StubOutWithMock(self.mid_compute_api, 'soft_delete') + self.mox.StubOutWithMock(self.tgt_compute_api, 'soft_delete') + + self.mid_compute_api.soft_delete(self.ctxt, instance) + self.tgt_compute_api.soft_delete(self.ctxt, instance) + + self.mox.ReplayAll() + + self.src_msg_runner.instance_delete_everywhere(self.ctxt, + instance, 'soft') + + def test_instance_fault_create_at_top(self): + fake_instance_fault = {'id': 1, + 'other stuff': 2, + 'more stuff': 3} + expected_instance_fault = {'other stuff': 2, + 'more stuff': 3} + + # Shouldn't be called for these 2 cells + self.mox.StubOutWithMock(self.src_db_inst, 'instance_fault_create') + self.mox.StubOutWithMock(self.mid_db_inst, 'instance_fault_create') + + self.mox.StubOutWithMock(self.tgt_db_inst, 'instance_fault_create') + self.tgt_db_inst.instance_fault_create(self.ctxt, + expected_instance_fault) + self.mox.ReplayAll() + + self.src_msg_runner.instance_fault_create_at_top(self.ctxt, + fake_instance_fault) + + def test_bw_usage_update_at_top(self): + fake_bw_update_info = {'uuid': 'fake_uuid', + 'mac': 'fake_mac', + 'start_period': 'fake_start_period', + 'bw_in': 'fake_bw_in', + 'bw_out': 'fake_bw_out', + 'last_ctr_in': 'fake_last_ctr_in', + 'last_ctr_out': 'fake_last_ctr_out', + 'last_refreshed': 'fake_last_refreshed'} + + # Shouldn't be called for these 2 cells + self.mox.StubOutWithMock(self.src_db_inst, 'bw_usage_update') + self.mox.StubOutWithMock(self.mid_db_inst, 'bw_usage_update') + + self.mox.StubOutWithMock(self.tgt_db_inst, 'bw_usage_update') + self.tgt_db_inst.bw_usage_update(self.ctxt, **fake_bw_update_info) + + self.mox.ReplayAll() + + self.src_msg_runner.bw_usage_update_at_top(self.ctxt, + fake_bw_update_info) diff --git a/nova/tests/cells/test_cells_rpc_driver.py b/nova/tests/cells/test_cells_rpc_driver.py new file mode 100644 index 000000000..a44fe9376 --- /dev/null +++ b/nova/tests/cells/test_cells_rpc_driver.py @@ -0,0 +1,218 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. +""" +Tests For Cells RPC Communication Driver +""" + +from nova.cells import messaging +from nova.cells import rpc_driver +from nova import context +from nova.openstack.common import cfg +from nova.openstack.common import rpc +from nova.openstack.common.rpc import dispatcher as rpc_dispatcher +from nova import test +from nova.tests.cells import fakes + +CONF = cfg.CONF +CONF.import_opt('rpc_driver_queue_base', 'nova.cells.rpc_driver', + group='cells') + + +class CellsRPCDriverTestCase(test.TestCase): + """Test case for Cells communication via RPC.""" + + def setUp(self): + super(CellsRPCDriverTestCase, self).setUp() + fakes.init(self) + self.ctxt = context.RequestContext('fake', 'fake') + self.driver = rpc_driver.CellsRPCDriver() + + def test_start_consumers(self): + self.flags(rpc_driver_queue_base='cells.intercell42', group='cells') + rpc_consumers = [] + rpc_conns = [] + fake_msg_runner = fakes.get_message_runner('api-cell') + call_info = {} + + class FakeInterCellRPCDispatcher(object): + def __init__(_self, msg_runner): + self.assertEqual(fake_msg_runner, msg_runner) + call_info['intercell_dispatcher'] = _self + + class FakeRPCDispatcher(object): + def __init__(_self, proxy_objs): + self.assertEqual([call_info['intercell_dispatcher']], + proxy_objs) + call_info['rpc_dispatcher'] = _self + + class FakeRPCConn(object): + def create_consumer(_self, topic, proxy_obj, **kwargs): + self.assertEqual(call_info['rpc_dispatcher'], proxy_obj) + rpc_consumers.append((topic, kwargs)) + + def consume_in_thread(_self): + pass + + def _fake_create_connection(new): + self.assertTrue(new) + fake_conn = FakeRPCConn() + rpc_conns.append(fake_conn) + return fake_conn + + self.stubs.Set(rpc, 'create_connection', _fake_create_connection) + self.stubs.Set(rpc_driver, 'InterCellRPCDispatcher', + FakeInterCellRPCDispatcher) + self.stubs.Set(rpc_dispatcher, 'RpcDispatcher', FakeRPCDispatcher) + + self.driver.start_consumers(fake_msg_runner) + + for message_type in ['broadcast', 'response', 'targeted']: + topic = 'cells.intercell42.' + message_type + self.assertIn((topic, {'fanout': True}), rpc_consumers) + self.assertIn((topic, {'fanout': False}), rpc_consumers) + self.assertEqual(rpc_conns, self.driver.rpc_connections) + + def test_stop_consumers(self): + call_info = {'closed': []} + + class FakeRPCConn(object): + def close(self): + call_info['closed'].append(self) + + fake_conns = [FakeRPCConn() for x in xrange(5)] + self.driver.rpc_connections = fake_conns + self.driver.stop_consumers() + self.assertEqual(fake_conns, call_info['closed']) + + def test_send_message_to_cell_cast(self): + msg_runner = fakes.get_message_runner('api-cell') + cell_state = fakes.get_cell_state('api-cell', 'child-cell2') + message = messaging._TargetedMessage(msg_runner, + self.ctxt, 'fake', 'fake', 'down', cell_state, fanout=False) + + call_info = {} + + def _fake_make_msg(method, **kwargs): + call_info['rpc_method'] = method + call_info['rpc_kwargs'] = kwargs + return 'fake-message' + + def _fake_cast_to_server(*args, **kwargs): + call_info['cast_args'] = args + call_info['cast_kwargs'] = kwargs + + self.stubs.Set(rpc, 'cast_to_server', _fake_cast_to_server) + self.stubs.Set(self.driver.intercell_rpcapi, 'make_msg', + _fake_make_msg) + self.stubs.Set(self.driver.intercell_rpcapi, 'cast_to_server', + _fake_cast_to_server) + + self.driver.send_message_to_cell(cell_state, message) + expected_server_params = {'hostname': 'rpc_host2', + 'password': 'password2', + 'port': 'rpc_port2', + 'username': 'username2', + 'virtual_host': 'rpc_vhost2'} + expected_cast_args = (self.ctxt, expected_server_params, + 'fake-message') + expected_cast_kwargs = {'topic': 'cells.intercell.targeted'} + expected_rpc_kwargs = {'message': message.to_json()} + self.assertEqual(expected_cast_args, call_info['cast_args']) + self.assertEqual(expected_cast_kwargs, call_info['cast_kwargs']) + self.assertEqual('process_message', call_info['rpc_method']) + self.assertEqual(expected_rpc_kwargs, call_info['rpc_kwargs']) + + def test_send_message_to_cell_fanout_cast(self): + msg_runner = fakes.get_message_runner('api-cell') + cell_state = fakes.get_cell_state('api-cell', 'child-cell2') + message = messaging._TargetedMessage(msg_runner, + self.ctxt, 'fake', 'fake', 'down', cell_state, fanout=True) + + call_info = {} + + def _fake_make_msg(method, **kwargs): + call_info['rpc_method'] = method + call_info['rpc_kwargs'] = kwargs + return 'fake-message' + + def _fake_fanout_cast_to_server(*args, **kwargs): + call_info['cast_args'] = args + call_info['cast_kwargs'] = kwargs + + self.stubs.Set(rpc, 'fanout_cast_to_server', + _fake_fanout_cast_to_server) + self.stubs.Set(self.driver.intercell_rpcapi, 'make_msg', + _fake_make_msg) + self.stubs.Set(self.driver.intercell_rpcapi, + 'fanout_cast_to_server', _fake_fanout_cast_to_server) + + self.driver.send_message_to_cell(cell_state, message) + expected_server_params = {'hostname': 'rpc_host2', + 'password': 'password2', + 'port': 'rpc_port2', + 'username': 'username2', + 'virtual_host': 'rpc_vhost2'} + expected_cast_args = (self.ctxt, expected_server_params, + 'fake-message') + expected_cast_kwargs = {'topic': 'cells.intercell.targeted'} + expected_rpc_kwargs = {'message': message.to_json()} + self.assertEqual(expected_cast_args, call_info['cast_args']) + self.assertEqual(expected_cast_kwargs, call_info['cast_kwargs']) + self.assertEqual('process_message', call_info['rpc_method']) + self.assertEqual(expected_rpc_kwargs, call_info['rpc_kwargs']) + + def test_rpc_topic_uses_message_type(self): + self.flags(rpc_driver_queue_base='cells.intercell42', group='cells') + msg_runner = fakes.get_message_runner('api-cell') + cell_state = fakes.get_cell_state('api-cell', 'child-cell2') + message = messaging._BroadcastMessage(msg_runner, + self.ctxt, 'fake', 'fake', 'down', fanout=True) + message.message_type = 'fake-message-type' + + call_info = {} + + def _fake_fanout_cast_to_server(*args, **kwargs): + call_info['topic'] = kwargs.get('topic') + + self.stubs.Set(self.driver.intercell_rpcapi, + 'fanout_cast_to_server', _fake_fanout_cast_to_server) + + self.driver.send_message_to_cell(cell_state, message) + self.assertEqual('cells.intercell42.fake-message-type', + call_info['topic']) + + def test_process_message(self): + msg_runner = fakes.get_message_runner('api-cell') + dispatcher = rpc_driver.InterCellRPCDispatcher(msg_runner) + message = messaging._BroadcastMessage(msg_runner, + self.ctxt, 'fake', 'fake', 'down', fanout=True) + + call_info = {} + + def _fake_message_from_json(json_message): + call_info['json_message'] = json_message + self.assertEqual(message.to_json(), json_message) + return message + + def _fake_process(): + call_info['process_called'] = True + + self.stubs.Set(msg_runner, 'message_from_json', + _fake_message_from_json) + self.stubs.Set(message, 'process', _fake_process) + + dispatcher.process_message(self.ctxt, message.to_json()) + self.assertEqual(message.to_json(), call_info['json_message']) + self.assertTrue(call_info['process_called']) diff --git a/nova/tests/cells/test_cells_rpcapi.py b/nova/tests/cells/test_cells_rpcapi.py new file mode 100644 index 000000000..b51bfa0c1 --- /dev/null +++ b/nova/tests/cells/test_cells_rpcapi.py @@ -0,0 +1,206 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. +""" +Tests For Cells RPCAPI +""" + +from nova.cells import rpcapi as cells_rpcapi +from nova.openstack.common import cfg +from nova.openstack.common import rpc +from nova import test + +CONF = cfg.CONF +CONF.import_opt('topic', 'nova.cells.opts', group='cells') + + +class CellsAPITestCase(test.TestCase): + """Test case for cells.api interfaces.""" + + def setUp(self): + super(CellsAPITestCase, self).setUp() + self.fake_topic = 'fake_topic' + self.fake_context = 'fake_context' + self.flags(topic=self.fake_topic, enable=True, group='cells') + self.cells_rpcapi = cells_rpcapi.CellsAPI() + + def _stub_rpc_method(self, rpc_method, result): + call_info = {} + + def fake_rpc_method(ctxt, topic, msg, *args, **kwargs): + call_info['context'] = ctxt + call_info['topic'] = topic + call_info['msg'] = msg + return result + + self.stubs.Set(rpc, rpc_method, fake_rpc_method) + return call_info + + def _check_result(self, call_info, method, args, version=None): + if version is None: + version = self.cells_rpcapi.BASE_RPC_API_VERSION + self.assertEqual(self.fake_context, call_info['context']) + self.assertEqual(self.fake_topic, call_info['topic']) + self.assertEqual(method, call_info['msg']['method']) + self.assertEqual(version, call_info['msg']['version']) + self.assertEqual(args, call_info['msg']['args']) + + def test_cast_compute_api_method(self): + fake_cell_name = 'fake_cell_name' + fake_method = 'fake_method' + fake_method_args = (1, 2) + fake_method_kwargs = {'kwarg1': 10, 'kwarg2': 20} + + expected_method_info = {'method': fake_method, + 'method_args': fake_method_args, + 'method_kwargs': fake_method_kwargs} + expected_args = {'method_info': expected_method_info, + 'cell_name': fake_cell_name, + 'call': False} + + call_info = self._stub_rpc_method('cast', None) + + self.cells_rpcapi.cast_compute_api_method(self.fake_context, + fake_cell_name, fake_method, + *fake_method_args, **fake_method_kwargs) + self._check_result(call_info, 'run_compute_api_method', + expected_args) + + def test_call_compute_api_method(self): + fake_cell_name = 'fake_cell_name' + fake_method = 'fake_method' + fake_method_args = (1, 2) + fake_method_kwargs = {'kwarg1': 10, 'kwarg2': 20} + fake_response = 'fake_response' + + expected_method_info = {'method': fake_method, + 'method_args': fake_method_args, + 'method_kwargs': fake_method_kwargs} + expected_args = {'method_info': expected_method_info, + 'cell_name': fake_cell_name, + 'call': True} + + call_info = self._stub_rpc_method('call', fake_response) + + result = self.cells_rpcapi.call_compute_api_method(self.fake_context, + fake_cell_name, fake_method, + *fake_method_args, **fake_method_kwargs) + self._check_result(call_info, 'run_compute_api_method', + expected_args) + self.assertEqual(fake_response, result) + + def test_schedule_run_instance(self): + call_info = self._stub_rpc_method('cast', None) + + self.cells_rpcapi.schedule_run_instance( + self.fake_context, arg1=1, arg2=2, arg3=3) + + expected_args = {'host_sched_kwargs': {'arg1': 1, + 'arg2': 2, + 'arg3': 3}} + self._check_result(call_info, 'schedule_run_instance', + expected_args) + + def test_instance_update_at_top(self): + fake_info_cache = {'id': 1, + 'instance': 'fake_instance', + 'other': 'moo'} + fake_sys_metadata = [{'id': 1, + 'key': 'key1', + 'value': 'value1'}, + {'id': 2, + 'key': 'key2', + 'value': 'value2'}] + fake_instance = {'id': 2, + 'security_groups': 'fake', + 'instance_type': 'fake', + 'volumes': 'fake', + 'cell_name': 'fake', + 'name': 'fake', + 'metadata': 'fake', + 'info_cache': fake_info_cache, + 'system_metadata': fake_sys_metadata, + 'other': 'meow'} + + call_info = self._stub_rpc_method('cast', None) + + self.cells_rpcapi.instance_update_at_top( + self.fake_context, fake_instance) + + expected_args = {'instance': fake_instance} + self._check_result(call_info, 'instance_update_at_top', + expected_args) + + def test_instance_destroy_at_top(self): + fake_instance = {'uuid': 'fake-uuid'} + + call_info = self._stub_rpc_method('cast', None) + + self.cells_rpcapi.instance_destroy_at_top( + self.fake_context, fake_instance) + + expected_args = {'instance': fake_instance} + self._check_result(call_info, 'instance_destroy_at_top', + expected_args) + + def test_instance_delete_everywhere(self): + fake_instance = {'uuid': 'fake-uuid'} + + call_info = self._stub_rpc_method('cast', None) + + self.cells_rpcapi.instance_delete_everywhere( + self.fake_context, fake_instance, + 'fake-type') + + expected_args = {'instance': fake_instance, + 'delete_type': 'fake-type'} + self._check_result(call_info, 'instance_delete_everywhere', + expected_args) + + def test_instance_fault_create_at_top(self): + fake_instance_fault = {'id': 2, + 'other': 'meow'} + + call_info = self._stub_rpc_method('cast', None) + + self.cells_rpcapi.instance_fault_create_at_top( + self.fake_context, fake_instance_fault) + + expected_args = {'instance_fault': fake_instance_fault} + self._check_result(call_info, 'instance_fault_create_at_top', + expected_args) + + def test_bw_usage_update_at_top(self): + update_args = ('fake_uuid', 'fake_mac', 'fake_start_period', + 'fake_bw_in', 'fake_bw_out', 'fake_ctr_in', + 'fake_ctr_out') + update_kwargs = {'last_refreshed': 'fake_refreshed'} + + call_info = self._stub_rpc_method('cast', None) + + self.cells_rpcapi.bw_usage_update_at_top( + self.fake_context, *update_args, **update_kwargs) + + bw_update_info = {'uuid': 'fake_uuid', + 'mac': 'fake_mac', + 'start_period': 'fake_start_period', + 'bw_in': 'fake_bw_in', + 'bw_out': 'fake_bw_out', + 'last_ctr_in': 'fake_ctr_in', + 'last_ctr_out': 'fake_ctr_out', + 'last_refreshed': 'fake_refreshed'} + + expected_args = {'bw_update_info': bw_update_info} + self._check_result(call_info, 'bw_usage_update_at_top', + expected_args) diff --git a/nova/tests/cells/test_cells_scheduler.py b/nova/tests/cells/test_cells_scheduler.py new file mode 100644 index 000000000..66e7e245e --- /dev/null +++ b/nova/tests/cells/test_cells_scheduler.py @@ -0,0 +1,206 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. +""" +Tests For CellsScheduler +""" +import time + +from nova.compute import vm_states +from nova import context +from nova import db +from nova import exception +from nova.openstack.common import cfg +from nova.openstack.common import uuidutils +from nova import test +from nova.tests.cells import fakes + +CONF = cfg.CONF +CONF.import_opt('scheduler_retries', 'nova.cells.scheduler', group='cells') + + +class CellsSchedulerTestCase(test.TestCase): + """Test case for CellsScheduler class""" + + def setUp(self): + super(CellsSchedulerTestCase, self).setUp() + fakes.init(self) + self.msg_runner = fakes.get_message_runner('api-cell') + self.scheduler = self.msg_runner.scheduler + self.state_manager = self.msg_runner.state_manager + self.my_cell_state = self.state_manager.get_my_state() + self.ctxt = context.RequestContext('fake', 'fake') + instance_uuids = [] + for x in xrange(3): + instance_uuids.append(uuidutils.generate_uuid()) + self.instance_uuids = instance_uuids + self.request_spec = {'instance_uuids': instance_uuids, + 'other': 'stuff'} + + def test_create_instances_here(self): + # Just grab the first instance type + inst_type = db.instance_type_get(self.ctxt, 1) + image = {'properties': {}} + instance_props = {'hostname': 'meow', + 'display_name': 'moo', + 'image_ref': 'fake_image_ref', + 'user_id': self.ctxt.user_id, + 'project_id': self.ctxt.project_id} + request_spec = {'instance_type': inst_type, + 'image': image, + 'security_group': ['default'], + 'block_device_mapping': [], + 'instance_properties': instance_props, + 'instance_uuids': self.instance_uuids} + + call_info = {'uuids': []} + + def _fake_instance_update_at_top(_ctxt, instance): + call_info['uuids'].append(instance['uuid']) + + self.stubs.Set(self.msg_runner, 'instance_update_at_top', + _fake_instance_update_at_top) + + self.scheduler._create_instances_here(self.ctxt, request_spec) + self.assertEqual(self.instance_uuids, call_info['uuids']) + + for instance_uuid in self.instance_uuids: + instance = db.instance_get_by_uuid(self.ctxt, instance_uuid) + self.assertEqual('meow', instance['hostname']) + self.assertEqual('moo', instance['display_name']) + self.assertEqual('fake_image_ref', instance['image_ref']) + + def test_run_instance_selects_child_cell(self): + # Make sure there's no capacity info so we're sure to + # select a child cell + our_cell_info = self.state_manager.get_my_state() + our_cell_info.capacities = {} + + call_info = {'times': 0} + + orig_fn = self.msg_runner.schedule_run_instance + + def msg_runner_schedule_run_instance(ctxt, target_cell, + host_sched_kwargs): + # This gets called twice. Once for our running it + # in this cell.. and then it'll get called when the + # child cell is picked. So, first time.. just run it + # like normal. + if not call_info['times']: + call_info['times'] += 1 + return orig_fn(ctxt, target_cell, host_sched_kwargs) + call_info['ctxt'] = ctxt + call_info['target_cell'] = target_cell + call_info['host_sched_kwargs'] = host_sched_kwargs + + self.stubs.Set(self.msg_runner, 'schedule_run_instance', + msg_runner_schedule_run_instance) + + host_sched_kwargs = {'request_spec': self.request_spec} + self.msg_runner.schedule_run_instance(self.ctxt, + self.my_cell_state, host_sched_kwargs) + + self.assertEqual(self.ctxt, call_info['ctxt']) + self.assertEqual(host_sched_kwargs, call_info['host_sched_kwargs']) + child_cells = self.state_manager.get_child_cells() + self.assertIn(call_info['target_cell'], child_cells) + + def test_run_instance_selects_current_cell(self): + # Make sure there's no child cells so that we will be + # selected + self.state_manager.child_cells = {} + + call_info = {} + + def fake_create_instances_here(ctxt, request_spec): + call_info['ctxt'] = ctxt + call_info['request_spec'] = request_spec + + def fake_rpc_run_instance(ctxt, **host_sched_kwargs): + call_info['host_sched_kwargs'] = host_sched_kwargs + + self.stubs.Set(self.scheduler, '_create_instances_here', + fake_create_instances_here) + self.stubs.Set(self.scheduler.scheduler_rpcapi, + 'run_instance', fake_rpc_run_instance) + + host_sched_kwargs = {'request_spec': self.request_spec, + 'other': 'stuff'} + self.msg_runner.schedule_run_instance(self.ctxt, + self.my_cell_state, host_sched_kwargs) + + self.assertEqual(self.ctxt, call_info['ctxt']) + self.assertEqual(self.request_spec, call_info['request_spec']) + self.assertEqual(host_sched_kwargs, call_info['host_sched_kwargs']) + + def test_run_instance_retries_when_no_cells_avail(self): + self.flags(scheduler_retries=7, group='cells') + + host_sched_kwargs = {'request_spec': self.request_spec} + + call_info = {'num_tries': 0, 'errored_uuids': []} + + def fake_run_instance(message, host_sched_kwargs): + call_info['num_tries'] += 1 + raise exception.NoCellsAvailable() + + def fake_sleep(_secs): + return + + def fake_instance_update(ctxt, instance_uuid, values): + self.assertEqual(vm_states.ERROR, values['vm_state']) + call_info['errored_uuids'].append(instance_uuid) + + self.stubs.Set(self.scheduler, '_run_instance', fake_run_instance) + self.stubs.Set(time, 'sleep', fake_sleep) + self.stubs.Set(db, 'instance_update', fake_instance_update) + + self.msg_runner.schedule_run_instance(self.ctxt, + self.my_cell_state, host_sched_kwargs) + + self.assertEqual(8, call_info['num_tries']) + self.assertEqual(self.instance_uuids, call_info['errored_uuids']) + + def test_run_instance_on_random_exception(self): + self.flags(scheduler_retries=7, group='cells') + + host_sched_kwargs = {'request_spec': self.request_spec} + + call_info = {'num_tries': 0, + 'errored_uuids1': [], + 'errored_uuids2': []} + + def fake_run_instance(message, host_sched_kwargs): + call_info['num_tries'] += 1 + raise test.TestingException() + + def fake_instance_update(ctxt, instance_uuid, values): + self.assertEqual(vm_states.ERROR, values['vm_state']) + call_info['errored_uuids1'].append(instance_uuid) + + def fake_instance_update_at_top(ctxt, instance): + self.assertEqual(vm_states.ERROR, instance['vm_state']) + call_info['errored_uuids2'].append(instance['uuid']) + + self.stubs.Set(self.scheduler, '_run_instance', fake_run_instance) + self.stubs.Set(db, 'instance_update', fake_instance_update) + self.stubs.Set(self.msg_runner, 'instance_update_at_top', + fake_instance_update_at_top) + + self.msg_runner.schedule_run_instance(self.ctxt, + self.my_cell_state, host_sched_kwargs) + # Shouldn't retry + self.assertEqual(1, call_info['num_tries']) + self.assertEqual(self.instance_uuids, call_info['errored_uuids1']) + self.assertEqual(self.instance_uuids, call_info['errored_uuids2']) diff --git a/nova/tests/cells/test_cells_utils.py b/nova/tests/cells/test_cells_utils.py new file mode 100644 index 000000000..84f60a796 --- /dev/null +++ b/nova/tests/cells/test_cells_utils.py @@ -0,0 +1,82 @@ +# Copyright (c) 2012 Rackspace Hosting +# 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. +""" +Tests For Cells Utility methods +""" +import inspect +import random + +from nova.cells import utils as cells_utils +from nova import db +from nova import test + + +class CellsUtilsTestCase(test.TestCase): + """Test case for Cells utility methods.""" + def test_get_instances_to_sync(self): + fake_context = 'fake_context' + + call_info = {'get_all': 0, 'shuffle': 0} + + def random_shuffle(_list): + call_info['shuffle'] += 1 + + def instance_get_all_by_filters(context, filters, + sort_key, sort_order): + self.assertEqual(context, fake_context) + self.assertEqual(sort_key, 'deleted') + self.assertEqual(sort_order, 'asc') + call_info['got_filters'] = filters + call_info['get_all'] += 1 + return ['fake_instance1', 'fake_instance2', 'fake_instance3'] + + self.stubs.Set(db, 'instance_get_all_by_filters', + instance_get_all_by_filters) + self.stubs.Set(random, 'shuffle', random_shuffle) + + instances = cells_utils.get_instances_to_sync(fake_context) + self.assertTrue(inspect.isgenerator(instances)) + self.assertTrue(len([x for x in instances]), 3) + self.assertEqual(call_info['get_all'], 1) + self.assertEqual(call_info['got_filters'], {}) + self.assertEqual(call_info['shuffle'], 0) + + instances = cells_utils.get_instances_to_sync(fake_context, + shuffle=True) + self.assertTrue(inspect.isgenerator(instances)) + self.assertTrue(len([x for x in instances]), 3) + self.assertEqual(call_info['get_all'], 2) + self.assertEqual(call_info['got_filters'], {}) + self.assertEqual(call_info['shuffle'], 1) + + instances = cells_utils.get_instances_to_sync(fake_context, + updated_since='fake-updated-since') + self.assertTrue(inspect.isgenerator(instances)) + self.assertTrue(len([x for x in instances]), 3) + self.assertEqual(call_info['get_all'], 3) + self.assertEqual(call_info['got_filters'], + {'changes-since': 'fake-updated-since'}) + self.assertEqual(call_info['shuffle'], 1) + + instances = cells_utils.get_instances_to_sync(fake_context, + project_id='fake-project', + updated_since='fake-updated-since', shuffle=True) + self.assertTrue(inspect.isgenerator(instances)) + self.assertTrue(len([x for x in instances]), 3) + self.assertEqual(call_info['get_all'], 4) + self.assertEqual(call_info['got_filters'], + {'changes-since': 'fake-updated-since', + 'project_id': 'fake-project'}) + self.assertEqual(call_info['shuffle'], 2) diff --git a/nova/tests/cert/test_rpcapi.py b/nova/tests/cert/test_rpcapi.py index 8db8a0f6c..f61d42408 100644 --- a/nova/tests/cert/test_rpcapi.py +++ b/nova/tests/cert/test_rpcapi.py @@ -33,8 +33,12 @@ class CertRpcAPITestCase(test.TestCase): ctxt = context.RequestContext('fake_user', 'fake_project') rpcapi = cert_rpcapi.CertAPI() expected_retval = 'foo' + expected_version = kwargs.pop('version', rpcapi.BASE_RPC_API_VERSION) expected_msg = rpcapi.make_msg(method, **kwargs) - expected_msg['version'] = rpcapi.BASE_RPC_API_VERSION + expected_msg['version'] = expected_version + + if method == 'get_backdoor_port': + del expected_msg['args']['host'] self.call_ctxt = None self.call_topic = None @@ -84,3 +88,7 @@ class CertRpcAPITestCase(test.TestCase): def test_decrypt_text(self): self._test_cert_api('decrypt_text', project_id='fake_project_id', text='blah') + + def test_get_backdoor_port(self): + self._test_cert_api('get_backdoor_port', host='fake_host', + version='1.1') diff --git a/nova/tests/compute/test_compute.py b/nova/tests/compute/test_compute.py index 48c16c603..3bd54cbba 100644 --- a/nova/tests/compute/test_compute.py +++ b/nova/tests/compute/test_compute.py @@ -283,7 +283,7 @@ class ComputeTestCase(BaseTestCase): @compute_manager.wrap_instance_fault def failer(self2, context, instance_uuid): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id=instance_uuid) self.assertRaises(exception.InstanceNotFound, failer, self.compute, self.context, inst_uuid) @@ -618,7 +618,7 @@ class ComputeTestCase(BaseTestCase): instance = self._create_instance() def fake(*args, **kwargs): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id="fake") self.stubs.Set(self.compute.driver, 'spawn', fake) self.mox.StubOutWithMock(self.compute, '_deallocate_network') @@ -723,7 +723,9 @@ class ComputeTestCase(BaseTestCase): self.mox.StubOutWithMock(self.compute, '_get_instance_nw_info') self.compute._get_instance_nw_info( mox.IgnoreArg(), - mox.IgnoreArg()).AndRaise(exception.NetworkNotFound()) + mox.IgnoreArg()).AndRaise( + exception.NetworkNotFound(network_id='fake') + ) self.mox.ReplayAll() self.compute.terminate_instance(self.context, instance=instance) @@ -1724,7 +1726,16 @@ class ComputeTestCase(BaseTestCase): def fake(*args, **kwargs): pass + def fake_migration_update(context, id, values): + # Ensure instance status updates is after the migration finish + migration_ref = db.migration_get(context, id) + instance_uuid = migration_ref['instance_uuid'] + instance = db.instance_get_by_uuid(context, instance_uuid) + self.assertFalse(instance['vm_state'] == vm_states.RESIZED) + self.assertEqual(instance['task_state'], task_states.RESIZE_FINISH) + self.stubs.Set(self.compute.driver, 'finish_migration', fake) + self.stubs.Set(db, 'migration_update', fake_migration_update) reservations = self._ensure_quota_reservations_committed() @@ -2721,10 +2732,10 @@ class ComputeTestCase(BaseTestCase): self.flags(running_deleted_instance_timeout=3600, running_deleted_instance_action='reap') - self.mox.StubOutWithMock(self.compute.db, "instance_get_all_by_host") - self.compute.db.instance_get_all_by_host(admin_context, - self.compute.host - ).AndReturn([instance]) + self.mox.StubOutWithMock(self.compute.conductor_api, + "instance_get_all_by_host") + self.compute.conductor_api.instance_get_all_by_host( + admin_context, self.compute.host).AndReturn([instance]) bdms = [] @@ -2760,9 +2771,10 @@ class ComputeTestCase(BaseTestCase): timeutils.is_older_than('sometimeago', CONF.running_deleted_instance_timeout).AndReturn(True) - self.mox.StubOutWithMock(self.compute.db, "instance_get_all_by_host") - self.compute.db.instance_get_all_by_host('context', - 'host').AndReturn( + self.mox.StubOutWithMock(self.compute.conductor_api, + "instance_get_all_by_host") + self.compute.conductor_api.instance_get_all_by_host('context', + 'host').AndReturn( [instance1, instance2]) self.mox.ReplayAll() @@ -2790,7 +2802,7 @@ class ComputeTestCase(BaseTestCase): def fake_instance_get_by_uuid(context, instance_uuid): if instance_uuid not in instance_map: - raise exception.InstanceNotFound + raise exception.InstanceNotFound(instance_id=instance_uuid) call_info['get_by_uuid'] += 1 return instance_map[instance_uuid] @@ -2803,7 +2815,7 @@ class ComputeTestCase(BaseTestCase): self.assertEqual(instance, call_info['expected_instance']) call_info['get_nw_info'] += 1 - self.stubs.Set(db, 'instance_get_all_by_host', + self.stubs.Set(self.compute.conductor_api, 'instance_get_all_by_host', fake_instance_get_all_by_host) self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) @@ -3685,8 +3697,12 @@ class ComputeAPITestCase(BaseTestCase): {'vm_state': vm_states.SOFT_DELETED, 'task_state': None}) + # Ensure quotas are committed self.mox.StubOutWithMock(nova.quota.QUOTAS, 'commit') nova.quota.QUOTAS.commit(mox.IgnoreArg(), mox.IgnoreArg()) + if self.__class__.__name__ == 'CellsComputeAPITestCase': + # Called a 2nd time (for the child cell) when testing cells + nova.quota.QUOTAS.commit(mox.IgnoreArg(), mox.IgnoreArg()) self.mox.ReplayAll() self.compute_api.restore(self.context, instance) @@ -4024,7 +4040,7 @@ class ComputeAPITestCase(BaseTestCase): """ def fake_show(*args): - raise exception.ImageNotFound + raise exception.ImageNotFound(image_id="fake") self.stubs.Set(fake_image._FakeImageService, 'show', fake_show) @@ -4385,7 +4401,7 @@ class ComputeAPITestCase(BaseTestCase): instance1 = self._create_fake_instance({ 'display_name': 'woot', - 'id': 0, + 'id': 1, 'uuid': '00000000-0000-0000-0000-000000000010'}) instance2 = self._create_fake_instance({ 'display_name': 'woo', diff --git a/nova/tests/compute/test_compute_cells.py b/nova/tests/compute/test_compute_cells.py new file mode 100644 index 000000000..aa4b448d4 --- /dev/null +++ b/nova/tests/compute/test_compute_cells.py @@ -0,0 +1,99 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright (c) 2012 Rackspace Hosting +# 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. +""" +Tests For Compute w/ Cells +""" +from nova.compute import cells_api as compute_cells_api +from nova.openstack.common import log as logging +from nova.tests.compute import test_compute + + +LOG = logging.getLogger('nova.tests.test_compute_cells') + +ORIG_COMPUTE_API = None + + +def stub_call_to_cells(context, instance, method, *args, **kwargs): + fn = getattr(ORIG_COMPUTE_API, method) + return fn(context, instance, *args, **kwargs) + + +def stub_cast_to_cells(context, instance, method, *args, **kwargs): + fn = getattr(ORIG_COMPUTE_API, method) + fn(context, instance, *args, **kwargs) + + +def deploy_stubs(stubs, api): + stubs.Set(api, '_call_to_cells', stub_call_to_cells) + stubs.Set(api, '_cast_to_cells', stub_cast_to_cells) + + +class CellsComputeAPITestCase(test_compute.ComputeAPITestCase): + def setUp(self): + super(CellsComputeAPITestCase, self).setUp() + global ORIG_COMPUTE_API + ORIG_COMPUTE_API = self.compute_api + + def _fake_cell_read_only(*args, **kwargs): + return False + + def _fake_validate_cell(*args, **kwargs): + return + + def _nop_update(context, instance, **kwargs): + return instance + + self.compute_api = compute_cells_api.ComputeCellsAPI() + self.stubs.Set(self.compute_api, '_cell_read_only', + _fake_cell_read_only) + self.stubs.Set(self.compute_api, '_validate_cell', + _fake_validate_cell) + + # NOTE(belliott) Don't update the instance state + # for the tests at the API layer. Let it happen after + # the stub cast to cells so that expected_task_states + # match. + self.stubs.Set(self.compute_api, 'update', _nop_update) + + deploy_stubs(self.stubs, self.compute_api) + + def tearDown(self): + global ORIG_COMPUTE_API + self.compute_api = ORIG_COMPUTE_API + super(CellsComputeAPITestCase, self).tearDown() + + def test_instance_metadata(self): + self.skipTest("Test is incompatible with cells.") + + def test_live_migrate(self): + self.skipTest("Test is incompatible with cells.") + + def test_get_backdoor_port(self): + self.skipTest("Test is incompatible with cells.") + + +class CellsComputePolicyTestCase(test_compute.ComputePolicyTestCase): + def setUp(self): + super(CellsComputePolicyTestCase, self).setUp() + global ORIG_COMPUTE_API + ORIG_COMPUTE_API = self.compute_api + self.compute_api = compute_cells_api.ComputeCellsAPI() + deploy_stubs(self.stubs, self.compute_api) + + def tearDown(self): + global ORIG_COMPUTE_API + self.compute_api = ORIG_COMPUTE_API + super(CellsComputePolicyTestCase, self).tearDown() diff --git a/nova/tests/compute/test_compute_utils.py b/nova/tests/compute/test_compute_utils.py index b3f7ea3df..5acc1cc53 100644 --- a/nova/tests/compute/test_compute_utils.py +++ b/nova/tests/compute/test_compute_utils.py @@ -260,6 +260,7 @@ class UsageInfoTestCase(test.TestCase): 'other_data': 'meow'} db.instance_system_metadata_update(self.context, instance['uuid'], sys_metadata, False) + instance = db.instance_get(self.context, instance_id) compute_utils.notify_usage_exists(self.context, instance) self.assertEquals(len(test_notifier.NOTIFICATIONS), 1) msg = test_notifier.NOTIFICATIONS[0] @@ -378,3 +379,14 @@ class UsageInfoTestCase(test.TestCase): image_ref_url = "%s/images/1" % utils.generate_glance_url() self.assertEquals(payload['image_ref_url'], image_ref_url) self.compute.terminate_instance(self.context, instance) + + +class MetadataToDictTestCase(test.TestCase): + def test_metadata_to_dict(self): + self.assertEqual(compute_utils.metadata_to_dict( + [{'key': 'foo1', 'value': 'bar'}, + {'key': 'foo2', 'value': 'baz'}]), + {'foo1': 'bar', 'foo2': 'baz'}) + + def test_metadata_to_dict_empty(self): + self.assertEqual(compute_utils.metadata_to_dict([]), {}) diff --git a/nova/tests/compute/test_resource_tracker.py b/nova/tests/compute/test_resource_tracker.py index 3f3962c8f..92edd34b5 100644 --- a/nova/tests/compute/test_resource_tracker.py +++ b/nova/tests/compute/test_resource_tracker.py @@ -601,6 +601,15 @@ class InstanceClaimTestCase(BaseTrackerTestCase): self.tracker.update_usage(self.context, instance) self.assertEqual(1, self.tracker.compute_node['vcpus_used']) + def test_skip_deleted_instances(self): + # ensure that the audit process skips instances that have vm_state + # DELETED, but the DB record is not yet deleted. + self._fake_instance(vm_state=vm_states.DELETED, host=self.host) + self.tracker.update_available_resource(self.context) + + self.assertEqual(0, self.tracker.compute_node['memory_mb_used']) + self.assertEqual(0, self.tracker.compute_node['local_gb_used']) + class ResizeClaimTestCase(BaseTrackerTestCase): diff --git a/nova/tests/conductor/test_conductor.py b/nova/tests/conductor/test_conductor.py index 46ad730e6..86f47a79c 100644 --- a/nova/tests/conductor/test_conductor.py +++ b/nova/tests/conductor/test_conductor.py @@ -100,6 +100,17 @@ class _BaseTestCase(object): self.conductor.migration_get(self.context, migration['id'])) + def test_migration_get_unconfirmed_by_dest_compute(self): + self.mox.StubOutWithMock(db, + 'migration_get_unconfirmed_by_dest_compute') + db.migration_get_unconfirmed_by_dest_compute(self.context, + 'fake-window', + 'fake-host') + self.mox.ReplayAll() + self.conductor.migration_get_unconfirmed_by_dest_compute(self.context, + 'fake-window', + 'fake-host') + def test_migration_update(self): migration = db.migration_create(self.context.elevated(), {'instance_uuid': 'fake-uuid', @@ -116,13 +127,6 @@ class _BaseTestCase(object): self.assertEqual(orig_instance['name'], copy_instance['name']) - def test_instance_get_all_by_host(self): - orig_instance = jsonutils.to_primitive(self._create_fake_instance()) - all_instances = self.conductor.instance_get_all_by_host( - self.context, orig_instance['host']) - self.assertEqual(orig_instance['name'], - all_instances[0]['name']) - def _setup_aggregate_with_host(self): aggregate_ref = db.aggregate_create(self.context.elevated(), {'name': 'foo', 'availability_zone': 'foo'}) @@ -268,6 +272,77 @@ class _BaseTestCase(object): 'fake-arch') self.assertEqual(result, 'it worked') + def test_block_device_mapping_get_all_by_instance(self): + fake_inst = {'uuid': 'fake-uuid'} + self.mox.StubOutWithMock(db, + 'block_device_mapping_get_all_by_instance') + db.block_device_mapping_get_all_by_instance( + self.context, fake_inst['uuid']).AndReturn('fake-result') + self.mox.ReplayAll() + result = self.conductor.block_device_mapping_get_all_by_instance( + self.context, fake_inst) + self.assertEqual(result, 'fake-result') + + def test_instance_get_all_hung_in_rebooting(self): + self.mox.StubOutWithMock(db, 'instance_get_all_hung_in_rebooting') + db.instance_get_all_hung_in_rebooting(self.context, 123) + self.mox.ReplayAll() + self.conductor.instance_get_all_hung_in_rebooting(self.context, 123) + + def test_instance_get_active_by_window(self): + self.mox.StubOutWithMock(db, 'instance_get_active_by_window_joined') + db.instance_get_active_by_window_joined(self.context, 'fake-begin', + 'fake-end', 'fake-proj', + 'fake-host') + self.mox.ReplayAll() + self.conductor.instance_get_active_by_window(self.context, + 'fake-begin', 'fake-end', + 'fake-proj', 'fake-host') + + def test_instance_destroy(self): + self.mox.StubOutWithMock(db, 'instance_destroy') + db.instance_destroy(self.context, 'fake-uuid') + self.mox.ReplayAll() + self.conductor.instance_destroy(self.context, {'uuid': 'fake-uuid'}) + + def test_instance_info_cache_delete(self): + self.mox.StubOutWithMock(db, 'instance_info_cache_delete') + db.instance_info_cache_delete(self.context, 'fake-uuid') + self.mox.ReplayAll() + self.conductor.instance_info_cache_delete(self.context, + {'uuid': 'fake-uuid'}) + + def test_instance_type_get(self): + self.mox.StubOutWithMock(db, 'instance_type_get') + db.instance_type_get(self.context, 'fake-id').AndReturn('fake-type') + self.mox.ReplayAll() + result = self.conductor.instance_type_get(self.context, 'fake-id') + self.assertEqual(result, 'fake-type') + + def test_vol_get_usage_by_time(self): + self.mox.StubOutWithMock(db, 'vol_get_usage_by_time') + db.vol_get_usage_by_time(self.context, 'fake-time').AndReturn( + 'fake-usage') + self.mox.ReplayAll() + result = self.conductor.vol_get_usage_by_time(self.context, + 'fake-time') + self.assertEqual(result, 'fake-usage') + + def test_vol_usage_update(self): + self.mox.StubOutWithMock(db, 'vol_usage_update') + db.vol_usage_update(self.context, 'fake-vol', 'rd-req', 'rd-bytes', + 'wr-req', 'wr-bytes', 'fake-id', 'fake-refr', + 'fake-bool') + self.mox.ReplayAll() + self.conductor.vol_usage_update(self.context, 'fake-vol', 'rd-req', + 'rd-bytes', 'wr-req', 'wr-bytes', + {'uuid': 'fake-id'}, 'fake-refr', + 'fake-bool') + + def test_ping(self): + result = self.conductor.ping(self.context, 'foo') + self.assertEqual(result, {'service': 'conductor', 'arg': 'foo'}) + class ConductorTestCase(_BaseTestCase, test.TestCase): """Conductor Manager Tests""" @@ -276,6 +351,91 @@ class ConductorTestCase(_BaseTestCase, test.TestCase): self.conductor = conductor_manager.ConductorManager() self.stub_out_client_exceptions() + def test_block_device_mapping_update_or_create(self): + fake_bdm = {'id': 'fake-id'} + self.mox.StubOutWithMock(db, 'block_device_mapping_create') + self.mox.StubOutWithMock(db, 'block_device_mapping_update') + self.mox.StubOutWithMock(db, 'block_device_mapping_update_or_create') + db.block_device_mapping_create(self.context, fake_bdm) + db.block_device_mapping_update(self.context, fake_bdm['id'], fake_bdm) + db.block_device_mapping_update_or_create(self.context, fake_bdm) + self.mox.ReplayAll() + self.conductor.block_device_mapping_update_or_create(self.context, + fake_bdm, + create=True) + self.conductor.block_device_mapping_update_or_create(self.context, + fake_bdm, + create=False) + self.conductor.block_device_mapping_update_or_create(self.context, + fake_bdm) + + def test_block_device_mapping_destroy(self): + fake_bdm = {'id': 'fake-bdm'} + fake_bdm2 = {'id': 'fake-bdm-2'} + fake_inst = {'uuid': 'fake-uuid'} + self.mox.StubOutWithMock(db, 'block_device_mapping_destroy') + self.mox.StubOutWithMock( + db, 'block_device_mapping_destroy_by_instance_and_device') + self.mox.StubOutWithMock( + db, 'block_device_mapping_destroy_by_instance_and_volume') + db.block_device_mapping_destroy(self.context, 'fake-bdm') + db.block_device_mapping_destroy(self.context, 'fake-bdm-2') + db.block_device_mapping_destroy_by_instance_and_device(self.context, + 'fake-uuid', + 'fake-device') + db.block_device_mapping_destroy_by_instance_and_volume(self.context, + 'fake-uuid', + 'fake-volume') + self.mox.ReplayAll() + self.conductor.block_device_mapping_destroy(self.context, + [fake_bdm, + fake_bdm2]) + self.conductor.block_device_mapping_destroy(self.context, + instance=fake_inst, + device_name='fake-device') + self.conductor.block_device_mapping_destroy(self.context, + instance=fake_inst, + volume_id='fake-volume') + + def test_instance_get_all_by_filters(self): + filters = {'foo': 'bar'} + self.mox.StubOutWithMock(db, 'instance_get_all_by_filters') + db.instance_get_all_by_filters(self.context, filters, + 'fake-key', 'fake-sort') + self.mox.ReplayAll() + self.conductor.instance_get_all_by_filters(self.context, filters, + 'fake-key', 'fake-sort') + + def _test_stubbed(self, name, dbargs, condargs): + self.mox.StubOutWithMock(db, name) + getattr(db, name)(self.context, *dbargs).AndReturn('fake-result') + self.mox.ReplayAll() + result = self.conductor.service_get_all_by(self.context, **condargs) + self.assertEqual(result, 'fake-result') + + def test_service_get_all(self): + self._test_stubbed('service_get_all', (), {}) + + def test_service_get_by_host_and_topic(self): + self._test_stubbed('service_get_by_host_and_topic', + ('host', 'topic'), + dict(topic='topic', host='host')) + + def test_service_get_all_by_topic(self): + self._test_stubbed('service_get_all_by_topic', + ('topic',), + dict(topic='topic')) + + def test_service_get_all_by_host(self): + self._test_stubbed('service_get_all_by_host', + ('host',), + dict(host='host')) + + def test_service_get_all_compute_by_host(self): + self._test_stubbed('service_get_all_compute_by_host', + ('host',), + dict(topic='compute', host='host')) + class ConductorRPCAPITestCase(_BaseTestCase, test.TestCase): """Conductor RPC API Tests""" @@ -285,6 +445,88 @@ class ConductorRPCAPITestCase(_BaseTestCase, test.TestCase): 'conductor', manager='nova.conductor.manager.ConductorManager') self.conductor = conductor_rpcapi.ConductorAPI() + def test_block_device_mapping_update_or_create(self): + fake_bdm = {'id': 'fake-id'} + self.mox.StubOutWithMock(db, 'block_device_mapping_create') + self.mox.StubOutWithMock(db, 'block_device_mapping_update') + self.mox.StubOutWithMock(db, 'block_device_mapping_update_or_create') + db.block_device_mapping_create(self.context, fake_bdm) + db.block_device_mapping_update(self.context, fake_bdm['id'], fake_bdm) + db.block_device_mapping_update_or_create(self.context, fake_bdm) + self.mox.ReplayAll() + self.conductor.block_device_mapping_update_or_create(self.context, + fake_bdm, + create=True) + self.conductor.block_device_mapping_update_or_create(self.context, + fake_bdm, + create=False) + self.conductor.block_device_mapping_update_or_create(self.context, + fake_bdm) + + def test_block_device_mapping_destroy(self): + fake_bdm = {'id': 'fake-bdm'} + fake_inst = {'uuid': 'fake-uuid'} + self.mox.StubOutWithMock(db, 'block_device_mapping_destroy') + self.mox.StubOutWithMock( + db, 'block_device_mapping_destroy_by_instance_and_device') + self.mox.StubOutWithMock( + db, 'block_device_mapping_destroy_by_instance_and_volume') + db.block_device_mapping_destroy(self.context, 'fake-bdm') + db.block_device_mapping_destroy_by_instance_and_device(self.context, + 'fake-uuid', + 'fake-device') + db.block_device_mapping_destroy_by_instance_and_volume(self.context, + 'fake-uuid', + 'fake-volume') + self.mox.ReplayAll() + self.conductor.block_device_mapping_destroy(self.context, + bdms=[fake_bdm]) + self.conductor.block_device_mapping_destroy(self.context, + instance=fake_inst, + device_name='fake-device') + self.conductor.block_device_mapping_destroy(self.context, + instance=fake_inst, + volume_id='fake-volume') + + def test_instance_get_all_by_filters(self): + filters = {'foo': 'bar'} + self.mox.StubOutWithMock(db, 'instance_get_all_by_filters') + db.instance_get_all_by_filters(self.context, filters, + 'fake-key', 'fake-sort') + self.mox.ReplayAll() + self.conductor.instance_get_all_by_filters(self.context, filters, + 'fake-key', 'fake-sort') + + def _test_stubbed(self, name, dbargs, condargs): + self.mox.StubOutWithMock(db, name) + getattr(db, name)(self.context, *dbargs).AndReturn('fake-result') + self.mox.ReplayAll() + result = self.conductor.service_get_all_by(self.context, **condargs) + self.assertEqual(result, 'fake-result') + + def test_service_get_all(self): + self._test_stubbed('service_get_all', (), {}) + + def test_service_get_by_host_and_topic(self): + self._test_stubbed('service_get_by_host_and_topic', + ('host', 'topic'), + dict(topic='topic', host='host')) + + def test_service_get_all_by_topic(self): + self._test_stubbed('service_get_all_by_topic', + ('topic',), + dict(topic='topic')) + + def test_service_get_all_by_host(self): + self._test_stubbed('service_get_all_by_host', + ('host',), + dict(host='host')) + + def test_service_get_all_compute_by_host(self): + self._test_stubbed('service_get_all_compute_by_host', + ('host',), + dict(topic='compute', host='host')) + class ConductorAPITestCase(_BaseTestCase, test.TestCase): """Conductor API Tests""" @@ -313,6 +555,79 @@ class ConductorAPITestCase(_BaseTestCase, test.TestCase): result = self.conductor.bw_usage_get(*get_args) self.assertEqual(result, 'foo') + def test_block_device_mapping_update_or_create(self): + self.mox.StubOutWithMock(db, 'block_device_mapping_create') + self.mox.StubOutWithMock(db, 'block_device_mapping_update') + self.mox.StubOutWithMock(db, 'block_device_mapping_update_or_create') + db.block_device_mapping_create(self.context, 'fake-bdm') + db.block_device_mapping_update(self.context, + 'fake-id', {'id': 'fake-id'}) + db.block_device_mapping_update_or_create(self.context, 'fake-bdm') + + self.mox.ReplayAll() + self.conductor.block_device_mapping_create(self.context, 'fake-bdm') + self.conductor.block_device_mapping_update(self.context, 'fake-id', {}) + self.conductor.block_device_mapping_update_or_create(self.context, + 'fake-bdm') + + def test_block_device_mapping_destroy(self): + fake_bdm = {'id': 'fake-bdm'} + fake_inst = {'uuid': 'fake-uuid'} + self.mox.StubOutWithMock(db, 'block_device_mapping_destroy') + self.mox.StubOutWithMock( + db, 'block_device_mapping_destroy_by_instance_and_device') + self.mox.StubOutWithMock( + db, 'block_device_mapping_destroy_by_instance_and_volume') + db.block_device_mapping_destroy(self.context, 'fake-bdm') + db.block_device_mapping_destroy_by_instance_and_device(self.context, + 'fake-uuid', + 'fake-device') + db.block_device_mapping_destroy_by_instance_and_volume(self.context, + 'fake-uuid', + 'fake-volume') + self.mox.ReplayAll() + self.conductor.block_device_mapping_destroy(self.context, [fake_bdm]) + self.conductor.block_device_mapping_destroy_by_instance_and_device( + self.context, fake_inst, 'fake-device') + self.conductor.block_device_mapping_destroy_by_instance_and_volume( + self.context, fake_inst, 'fake-volume') + + def test_instance_get_all(self): + self.mox.StubOutWithMock(db, 'instance_get_all_by_filters') + db.instance_get_all_by_filters(self.context, {}, 'created_at', 'desc') + db.instance_get_all_by_filters(self.context, {'host': 'fake-host'}, + 'created_at', 'desc') + db.instance_get_all_by_filters(self.context, {'name': 'fake-inst'}, + 'updated_at', 'asc') + self.mox.ReplayAll() + self.conductor.instance_get_all(self.context) + self.conductor.instance_get_all_by_host(self.context, 'fake-host') + self.conductor.instance_get_all_by_filters(self.context, + {'name': 'fake-inst'}, + 'updated_at', 'asc') + + def _test_stubbed(self, name, *args): + self.mox.StubOutWithMock(db, name) + getattr(db, name)(self.context, *args).AndReturn('fake-result') + self.mox.ReplayAll() + result = getattr(self.conductor, name)(self.context, *args) + self.assertEqual(result, 'fake-result') + + def test_service_get_all(self): + self._test_stubbed('service_get_all') + + def test_service_get_by_host_and_topic(self): + self._test_stubbed('service_get_by_host_and_topic', 'host', 'topic') + + def test_service_get_all_by_topic(self): + self._test_stubbed('service_get_all_by_topic', 'topic') + + def test_service_get_all_by_host(self): + self._test_stubbed('service_get_all_by_host', 'host') + + def test_service_get_all_compute_by_host(self): + self._test_stubbed('service_get_all_compute_by_host', 'host') + class ConductorLocalAPITestCase(ConductorAPITestCase): """Conductor LocalAPI Tests""" diff --git a/nova/tests/console/test_console.py b/nova/tests/console/test_console.py index 65db24844..4a1dc8fe6 100644 --- a/nova/tests/console/test_console.py +++ b/nova/tests/console/test_console.py @@ -185,3 +185,13 @@ class ConsoleAPITestCase(test.TestCase): self.mox.ReplayAll() self.console_api.create_console(self.context, self.fake_uuid) + + def test_get_backdoor_port(self): + self.mox.StubOutWithMock(console_rpcapi.ConsoleAPI, + 'get_backdoor_port') + + console_rpcapi.ConsoleAPI.get_backdoor_port(self.context, 'fake_host') + + self.mox.ReplayAll() + + self.console_api.get_backdoor_port(self.context, 'fake_host') diff --git a/nova/tests/console/test_rpcapi.py b/nova/tests/console/test_rpcapi.py index ef4bc4ae7..6e9417ada 100644 --- a/nova/tests/console/test_rpcapi.py +++ b/nova/tests/console/test_rpcapi.py @@ -29,31 +29,43 @@ CONF.import_opt('console_topic', 'nova.config') class ConsoleRpcAPITestCase(test.TestCase): - def _test_console_api(self, method, **kwargs): + def _test_console_api(self, method, rpc_method, **kwargs): ctxt = context.RequestContext('fake_user', 'fake_project') rpcapi = console_rpcapi.ConsoleAPI() + expected_retval = 'foo' if method == 'call' else None + expected_version = kwargs.pop('version', rpcapi.BASE_RPC_API_VERSION) expected_msg = rpcapi.make_msg(method, **kwargs) - expected_msg['version'] = rpcapi.BASE_RPC_API_VERSION + expected_msg['version'] = expected_version - self.cast_ctxt = None - self.cast_topic = None - self.cast_msg = None + if method == 'get_backdoor_port': + del expected_msg['args']['host'] - def _fake_cast(_ctxt, _topic, _msg): - self.cast_ctxt = _ctxt - self.cast_topic = _topic - self.cast_msg = _msg + self.fake_args = None + self.fake_kwargs = None - self.stubs.Set(rpc, 'cast', _fake_cast) + def _fake_rpc_method(*args, **kwargs): + self.fake_args = args + self.fake_kwargs = kwargs + if expected_retval: + return expected_retval - getattr(rpcapi, method)(ctxt, **kwargs) + self.stubs.Set(rpc, rpc_method, _fake_rpc_method) - self.assertEqual(self.cast_ctxt, ctxt) - self.assertEqual(self.cast_topic, CONF.console_topic) - self.assertEqual(self.cast_msg, expected_msg) + retval = getattr(rpcapi, method)(ctxt, **kwargs) + + self.assertEqual(retval, expected_retval) + expected_args = [ctxt, CONF.console_topic, expected_msg] + for arg, expected_arg in zip(self.fake_args, expected_args): + self.assertEqual(arg, expected_arg) def test_add_console(self): - self._test_console_api('add_console', instance_id='i') + self._test_console_api('add_console', instance_id='i', + rpc_method='cast') def test_remove_console(self): - self._test_console_api('remove_console', console_id='i') + self._test_console_api('remove_console', console_id='i', + rpc_method='cast') + + def test_get_backdoor_port(self): + self._test_console_api('get_backdoor_port', host='fake_host', + rpc_method='call', version='1.1') diff --git a/nova/tests/consoleauth/test_consoleauth.py b/nova/tests/consoleauth/test_consoleauth.py index 202e4c7b7..f92a4be1c 100644 --- a/nova/tests/consoleauth/test_consoleauth.py +++ b/nova/tests/consoleauth/test_consoleauth.py @@ -47,3 +47,8 @@ class ConsoleauthTestCase(test.TestCase): self.assertTrue(self.manager.check_token(self.context, token)) timeutils.advance_time_seconds(1) self.assertFalse(self.manager.check_token(self.context, token)) + + def test_get_backdoor_port(self): + self.manager.backdoor_port = 59697 + port = self.manager.get_backdoor_port(self.context) + self.assertEqual(port, self.manager.backdoor_port) diff --git a/nova/tests/consoleauth/test_rpcapi.py b/nova/tests/consoleauth/test_rpcapi.py index 51a8eda14..264c4e10b 100644 --- a/nova/tests/consoleauth/test_rpcapi.py +++ b/nova/tests/consoleauth/test_rpcapi.py @@ -32,8 +32,12 @@ class ConsoleAuthRpcAPITestCase(test.TestCase): ctxt = context.RequestContext('fake_user', 'fake_project') rpcapi = consoleauth_rpcapi.ConsoleAuthAPI() expected_retval = 'foo' + expected_version = kwargs.pop('version', rpcapi.BASE_RPC_API_VERSION) expected_msg = rpcapi.make_msg(method, **kwargs) - expected_msg['version'] = rpcapi.BASE_RPC_API_VERSION + expected_msg['version'] = expected_version + + if method == 'get_backdoor_port': + del expected_msg['args']['host'] self.call_ctxt = None self.call_topic = None @@ -64,3 +68,7 @@ class ConsoleAuthRpcAPITestCase(test.TestCase): def test_check_token(self): self._test_consoleauth_api('check_token', token='t') + + def test_get_backdoor_port(self): + self._test_consoleauth_api('get_backdoor_port', host='fake_host', + version='1.1') diff --git a/nova/tests/fake_network.py b/nova/tests/fake_network.py index f9cd459b1..896b11216 100644 --- a/nova/tests/fake_network.py +++ b/nova/tests/fake_network.py @@ -116,7 +116,7 @@ class FakeNetworkManager(network_manager.NetworkManager): dict(address='10.0.0.2')] def network_get_by_cidr(self, context, cidr): - raise exception.NetworkNotFoundForCidr() + raise exception.NetworkNotFoundForCidr(cidr=cidr) def network_create_safe(self, context, net): fakenet = dict(net) @@ -127,7 +127,7 @@ class FakeNetworkManager(network_manager.NetworkManager): return {'cidr_v6': '2001:db8:69:%x::/64' % network_id} def network_get_by_uuid(self, context, network_uuid): - raise exception.NetworkNotFoundForUUID() + raise exception.NetworkNotFoundForUUID(uuid=network_uuid) def network_get_all(self, context): raise exception.NoNetworksFound() diff --git a/nova/tests/fakeguestfs.py b/nova/tests/fakeguestfs.py index c7ac01c64..33ca49c33 100644 --- a/nova/tests/fakeguestfs.py +++ b/nova/tests/fakeguestfs.py @@ -96,13 +96,13 @@ class GuestFS(object): def stat(self, path): if not path in self.files: - raise Exception("No such file: " + path) + raise RuntimeError("No such file: " + path) return self.files[path]["mode"] def chown(self, uid, gid, path): if not path in self.files: - raise Exception("No such file: " + path) + raise RuntimeError("No such file: " + path) if uid != -1: self.files[path]["uid"] = uid @@ -111,7 +111,7 @@ class GuestFS(object): def chmod(self, mode, path): if not path in self.files: - raise Exception("No such file: " + path) + raise RuntimeError("No such file: " + path) self.files[path]["mode"] = mode @@ -123,7 +123,7 @@ class GuestFS(object): def aug_get(self, cfgpath): if not self.auginit: - raise Exception("Augeus not initialized") + raise RuntimeError("Augeus not initialized") if cfgpath == "/files/etc/passwd/root/uid": return 0 @@ -137,4 +137,4 @@ class GuestFS(object): return 500 elif cfgpath == "/files/etc/group/admins/gid": return 600 - raise Exception("Unknown path %s", cfgpath) + raise RuntimeError("Unknown path %s", cfgpath) diff --git a/nova/tests/integrated/api_samples/OS-FLV-DISABLED/flavor-detail-get-resp.json.tpl b/nova/tests/integrated/api_samples/OS-FLV-DISABLED/flavor-detail-get-resp.json.tpl new file mode 100644 index 000000000..d1f78c08e --- /dev/null +++ b/nova/tests/integrated/api_samples/OS-FLV-DISABLED/flavor-detail-get-resp.json.tpl @@ -0,0 +1,94 @@ +{ + "flavors": [ + { + "OS-FLV-DISABLED:disabled": false, + "disk": 0, + "id": "1", + "links": [ + { + "href": "%(host)s/v2/openstack/flavors/1", + "rel": "self" + }, + { + "href": "%(host)s/openstack/flavors/1", + "rel": "bookmark" + } + ], + "name": "m1.tiny", + "ram": 512, + "vcpus": 1 + }, + { + "OS-FLV-DISABLED:disabled": false, + "disk": 20, + "id": "2", + "links": [ + { + "href": "%(host)s/v2/openstack/flavors/2", + "rel": "self" + }, + { + "href": "%(host)s/openstack/flavors/2", + "rel": "bookmark" + } + ], + "name": "m1.small", + "ram": 2048, + "vcpus": 1 + }, + { + "OS-FLV-DISABLED:disabled": false, + "disk": 40, + "id": "3", + "links": [ + { + "href": "%(host)s/v2/openstack/flavors/3", + "rel": "self" + }, + { + "href": "%(host)s/openstack/flavors/3", + "rel": "bookmark" + } + ], + "name": "m1.medium", + "ram": 4096, + "vcpus": 2 + }, + { + "OS-FLV-DISABLED:disabled": false, + "disk": 80, + "id": "4", + "links": [ + { + "href": "%(host)s/v2/openstack/flavors/4", + "rel": "self" + }, + { + "href": "%(host)s/openstack/flavors/4", + "rel": "bookmark" + } + ], + "name": "m1.large", + "ram": 8192, + "vcpus": 4 + }, + { + "OS-FLV-DISABLED:disabled": false, + "disk": 160, + "id": "5", + "links": [ + { + "href": "%(host)s/v2/openstack/flavors/5", + "rel": "self" + }, + { + "href": "%(host)s/openstack/flavors/5", + "rel": "bookmark" + } + ], + "name": "m1.xlarge", + "ram": 16384, + "vcpus": 8 + } + ] +} diff --git a/nova/tests/integrated/api_samples/OS-FLV-DISABLED/flavor-detail-get-resp.xml.tpl b/nova/tests/integrated/api_samples/OS-FLV-DISABLED/flavor-detail-get-resp.xml.tpl new file mode 100644 index 000000000..8d992e42d --- /dev/null +++ b/nova/tests/integrated/api_samples/OS-FLV-DISABLED/flavor-detail-get-resp.xml.tpl @@ -0,0 +1,23 @@ +<?xml version='1.0' encoding='UTF-8'?> +<flavors xmlns:OS-FLV-DISABLED="http://docs.openstack.org/compute/ext/flavor_disabled/api/v1.1" xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1"> + <flavor disk="0" vcpus="1" ram="512" name="m1.tiny" id="1" OS-FLV-DISABLED:disabled="False"> + <atom:link href="%(host)s/v2/openstack/flavors/1" rel="self"/> + <atom:link href="%(host)s/openstack/flavors/1" rel="bookmark"/> + </flavor> + <flavor disk="20" vcpus="1" ram="2048" name="m1.small" id="2" OS-FLV-DISABLED:disabled="False"> + <atom:link href="%(host)s/v2/openstack/flavors/2" rel="self"/> + <atom:link href="%(host)s/openstack/flavors/2" rel="bookmark"/> + </flavor> + <flavor disk="40" vcpus="2" ram="4096" name="m1.medium" id="3" OS-FLV-DISABLED:disabled="False"> + <atom:link href="%(host)s/v2/openstack/flavors/3" rel="self"/> + <atom:link href="%(host)s/openstack/flavors/3" rel="bookmark"/> + </flavor> + <flavor disk="80" vcpus="4" ram="8192" name="m1.large" id="4" OS-FLV-DISABLED:disabled="False"> + <atom:link href="%(host)s/v2/openstack/flavors/4" rel="self"/> + <atom:link href="%(host)s/openstack/flavors/4" rel="bookmark"/> + </flavor> + <flavor disk="160" vcpus="8" ram="16384" name="m1.xlarge" id="5" OS-FLV-DISABLED:disabled="False"> + <atom:link href="%(host)s/v2/openstack/flavors/5" rel="self"/> + <atom:link href="%(host)s/openstack/flavors/5" rel="bookmark"/> + </flavor> +</flavors> diff --git a/nova/tests/integrated/api_samples/OS-FLV-DISABLED/flavor-show-get-resp.json.tpl b/nova/tests/integrated/api_samples/OS-FLV-DISABLED/flavor-show-get-resp.json.tpl new file mode 100644 index 000000000..cf5fb232a --- /dev/null +++ b/nova/tests/integrated/api_samples/OS-FLV-DISABLED/flavor-show-get-resp.json.tpl @@ -0,0 +1,20 @@ +{ + "flavor": { + "OS-FLV-DISABLED:disabled": false, + "disk": 0, + "id": "%(flavor_id)s", + "links": [ + { + "href": "%(host)s/v2/openstack/flavors/%(flavor_id)s", + "rel": "self" + }, + { + "href": "%(host)s/openstack/flavors/%(flavor_id)s", + "rel": "bookmark" + } + ], + "name": "m1.tiny", + "ram": 512, + "vcpus": 1 + } +} diff --git a/nova/tests/integrated/api_samples/OS-FLV-DISABLED/flavor-show-get-resp.xml.tpl b/nova/tests/integrated/api_samples/OS-FLV-DISABLED/flavor-show-get-resp.xml.tpl new file mode 100644 index 000000000..c7bdd1ca8 --- /dev/null +++ b/nova/tests/integrated/api_samples/OS-FLV-DISABLED/flavor-show-get-resp.xml.tpl @@ -0,0 +1,5 @@ +<?xml version='1.0' encoding='UTF-8'?> +<flavor xmlns:OS-FLV-DISABLED="http://docs.openstack.org/compute/ext/flavor_disabled/api/v1.1" xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" disk="0" vcpus="1" ram="512" name="m1.tiny" id="%(flavor_id)s" OS-FLV-DISABLED:disabled="False"> + <atom:link href="%(host)s/v2/openstack/flavors/%(flavor_id)s" rel="self"/> + <atom:link href="%(host)s/openstack/flavors/%(flavor_id)s" rel="bookmark"/> +</flavor> diff --git a/nova/tests/integrated/api_samples/os-coverage/coverage-stop-post-resp.json.tpl b/nova/tests/integrated/api_samples/os-coverage/coverage-stop-post-resp.json.tpl new file mode 100644 index 000000000..6cdd1f37d --- /dev/null +++ b/nova/tests/integrated/api_samples/os-coverage/coverage-stop-post-resp.json.tpl @@ -0,0 +1,3 @@ +{ + "path" : "%(path)s" +} diff --git a/nova/tests/integrated/api_samples/os-coverage/coverage-stop-post-resp.xml.tpl b/nova/tests/integrated/api_samples/os-coverage/coverage-stop-post-resp.xml.tpl new file mode 100644 index 000000000..65f5e16bc --- /dev/null +++ b/nova/tests/integrated/api_samples/os-coverage/coverage-stop-post-resp.xml.tpl @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8'?> +<path>%(path)s</path> diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index f8bd522fe..e5f3b9ad5 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -360,7 +360,6 @@ class ApiSamplesTrap(ApiSampleTestBase): # removed) soon. do_not_approve_additions = [] do_not_approve_additions.append('NMN') - do_not_approve_additions.append('OS-FLV-DISABLED') do_not_approve_additions.append('os-config-drive') do_not_approve_additions.append('os-create-server-ext') do_not_approve_additions.append('os-flavor-access') @@ -769,10 +768,15 @@ class CoverageExtJsonTests(ApiSampleTestBase): def test_stop_coverage(self): """Stop coverage data collection""" - subs = {} + subs = { + 'path': '/.*', + } response = self._do_post('os-coverage/action', 'coverage-stop-post-req', subs) self.assertEqual(response.status, 200) + subs.update(self._get_regexes()) + return self._verify_response('coverage-stop-post-resp', + subs, response) def test_report_coverage(self): """Generate a coverage report""" @@ -2274,3 +2278,30 @@ class NetworksAssociateJsonTests(ApiSampleTestBase): class NetworksAssociateXmlTests(NetworksAssociateJsonTests): ctype = 'xml' + + +class FlavorDisabledSampleJsonTests(ApiSampleTestBase): + extension_name = ("nova.api.openstack.compute.contrib.flavor_disabled." + "Flavor_disabled") + + def test_show_flavor(self): + """Get api sample to show flavor_disabled attr. of a flavor""" + flavor_id = 1 + response = self._do_get('flavors/%s' % flavor_id) + self.assertEqual(response.status, 200) + subs = self._get_regexes() + subs['flavor_id'] = flavor_id + return self._verify_response('flavor-show-get-resp', subs, + response) + + def test_detail_flavor(self): + """Get api sample to show details of a flavor""" + response = self._do_get('flavors/detail') + self.assertEqual(response.status, 200) + subs = self._get_regexes() + return self._verify_response('flavor-detail-get-resp', subs, + response) + + +class FlavorDisabledSampleXmlTests(FlavorDisabledSampleJsonTests): + ctype = "xml" diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index 471b75308..8ac892b1f 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -145,8 +145,8 @@ class ServersTest(integrated_helpers._IntegratedTestBase): fake_network.set_stub_network_methods(self.stubs) # enforce periodic tasks run in short time to avoid wait for 60s. - self._restart_compute_service( - periodic_interval=0.3, periodic_fuzzy_delay=0) + self._restart_compute_service(periodic_interval_max=0.3, + periodic_fuzzy_delay=0) # Create server server = self._build_minimal_create_server_request() diff --git a/nova/tests/matchers.py b/nova/tests/matchers.py index a421cc056..be65da823 100644 --- a/nova/tests/matchers.py +++ b/nova/tests/matchers.py @@ -198,6 +198,21 @@ class IsSubDictOf(object): return SubDictMismatch(k, sub_value, super_value) +class FunctionCallMatcher(object): + + def __init__(self, expected_func_calls): + self.expected_func_calls = expected_func_calls + self.actual_func_calls = [] + + def call(self, *args, **kwargs): + func_call = {'args': args, 'kwargs': kwargs} + self.actual_func_calls.append(func_call) + + def match(self): + dict_list_matcher = DictListMatches(self.expected_func_calls) + return dict_list_matcher.match(self.actual_func_calls) + + class XMLMismatch(object): """Superclass for XML mismatch.""" diff --git a/nova/tests/network/test_api.py b/nova/tests/network/test_api.py index ef97a4982..3339764b5 100644 --- a/nova/tests/network/test_api.py +++ b/nova/tests/network/test_api.py @@ -152,7 +152,7 @@ class ApiTestCase(test.TestCase): def test_is_multi_host_instance_has_no_fixed_ip(self): def fake_fixed_ip_get_by_instance(ctxt, uuid): - raise exception.FixedIpNotFoundForInstance + raise exception.FixedIpNotFoundForInstance(instance_uuid=uuid) self.stubs.Set(self.network_api.db, 'fixed_ip_get_by_instance', fake_fixed_ip_get_by_instance) instance = {'uuid': FAKE_UUID} diff --git a/nova/tests/network/test_manager.py b/nova/tests/network/test_manager.py index c2aa1dbbb..b45a290c0 100644 --- a/nova/tests/network/test_manager.py +++ b/nova/tests/network/test_manager.py @@ -669,7 +669,7 @@ class VlanNetworkTestCase(test.TestCase): is_admin=False) def fake1(*args, **kwargs): - pass + return '10.0.0.1' # floating ip that's already associated def fake2(*args, **kwargs): @@ -689,6 +689,7 @@ class VlanNetworkTestCase(test.TestCase): def fake4(*args, **kwargs): return {'address': '10.0.0.1', 'pool': 'nova', + 'instance_uuid': FAKEUUID, 'interface': 'eth0', 'network_id': 'blah'} @@ -791,7 +792,7 @@ class VlanNetworkTestCase(test.TestCase): def fixed_ip_get(_context, fixed_ip_id): if fixed_ip_id == 1: return {'address': 'fakefixed'} - raise exception.FixedIpNotFound() + raise exception.FixedIpNotFound(id=fixed_ip_id) self.stubs.Set(self.network.db, 'fixed_ip_get', fixed_ip_get) self.mox.StubOutWithMock(self.network.l3driver, 'add_floating_ip') @@ -840,6 +841,7 @@ class VlanNetworkTestCase(test.TestCase): def fake4(*args, **kwargs): return {'address': '10.0.0.1', 'pool': 'nova', + 'instance_uuid': FAKEUUID, 'interface': 'eth0', 'network_id': 'blah'} @@ -1482,7 +1484,9 @@ class CommonNetworkTestCase(test.TestCase): self.mox.StubOutWithMock(manager.db, 'network_get_by_uuid') manager.db.network_get_by_uuid( mox.IgnoreArg(), - mox.IgnoreArg()).AndRaise(exception.NetworkNotFoundForUUID) + mox.IgnoreArg()).AndRaise( + exception.NetworkNotFoundForUUID(uuid='fake') + ) self.mox.ReplayAll() uuid = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' self.assertRaises(exception.NetworkNotFound, @@ -1517,7 +1521,9 @@ class CommonNetworkTestCase(test.TestCase): self.mox.StubOutWithMock(manager.db, 'network_get_by_uuid') manager.db.network_get_by_uuid( mox.IgnoreArg(), - mox.IgnoreArg()).AndRaise(exception.NetworkNotFoundForUUID) + mox.IgnoreArg()).AndRaise( + exception.NetworkNotFoundForUUID(uuid='fake') + ) self.mox.ReplayAll() uuid = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' self.assertRaises(exception.NetworkNotFound, @@ -1629,6 +1635,115 @@ class FloatingIPTestCase(test.TestCase): shutil.rmtree(self.tempdir) super(FloatingIPTestCase, self).tearDown() + def test_disassociate_floating_ip_multi_host_calls(self): + floating_ip = { + 'fixed_ip_id': 12 + } + + fixed_ip = { + 'network_id': None, + 'instance_uuid': 'instance-uuid' + } + + network = { + 'multi_host': True + } + + instance = { + 'host': 'some-other-host' + } + + ctxt = context.RequestContext('testuser', 'testproject', + is_admin=False) + + self.stubs.Set(self.network.db, + 'floating_ip_get_by_address', + lambda _x, _y: floating_ip) + + self.stubs.Set(self.network, + '_floating_ip_owned_by_project', + lambda _x, _y: True) + + self.stubs.Set(self.network.db, + 'fixed_ip_get', + lambda _x, _y: fixed_ip) + + self.stubs.Set(self.network, + '_get_network_by_id', + lambda _x, _y: network) + + self.stubs.Set(self.network.db, + 'instance_get_by_uuid', + lambda _x, _y: instance) + + self.stubs.Set(self.network.db, + 'service_get_by_host_and_topic', + lambda _x, _y, _z: 'service') + + self.stubs.Set(self.network.servicegroup_api, + 'service_is_up', + lambda _x: True) + + self.mox.StubOutWithMock( + self.network.network_rpcapi, '_disassociate_floating_ip') + + self.network.network_rpcapi._disassociate_floating_ip( + ctxt, 'fl_ip', mox.IgnoreArg(), 'some-other-host', 'instance-uuid') + self.mox.ReplayAll() + + self.network.disassociate_floating_ip(ctxt, 'fl_ip', True) + + def test_associate_floating_ip_multi_host_calls(self): + floating_ip = { + 'fixed_ip_id': None + } + + fixed_ip = { + 'network_id': None, + 'instance_uuid': 'instance-uuid' + } + + network = { + 'multi_host': True + } + + instance = { + 'host': 'some-other-host' + } + + ctxt = context.RequestContext('testuser', 'testproject', + is_admin=False) + + self.stubs.Set(self.network.db, + 'floating_ip_get_by_address', + lambda _x, _y: floating_ip) + + self.stubs.Set(self.network, + '_floating_ip_owned_by_project', + lambda _x, _y: True) + + self.stubs.Set(self.network.db, + 'fixed_ip_get_by_address', + lambda _x, _y: fixed_ip) + + self.stubs.Set(self.network, + '_get_network_by_id', + lambda _x, _y: network) + + self.stubs.Set(self.network.db, + 'instance_get_by_uuid', + lambda _x, _y: instance) + + self.mox.StubOutWithMock( + self.network.network_rpcapi, '_associate_floating_ip') + + self.network.network_rpcapi._associate_floating_ip( + ctxt, 'fl_ip', 'fix_ip', mox.IgnoreArg(), 'some-other-host', + 'instance-uuid') + self.mox.ReplayAll() + + self.network.associate_floating_ip(ctxt, 'fl_ip', 'fix_ip', True) + def test_double_deallocation(self): instance_ref = db.api.instance_create(self.context, {"project_id": self.project_id}) @@ -1940,7 +2055,7 @@ class FloatingIPTestCase(test.TestCase): self.mox.StubOutWithMock(self.network.db, 'floating_ip_get_by_address') self.network.db.floating_ip_get_by_address( self.context, '1.2.3.4').AndRaise( - exception.FloatingIpNotFoundForAddress) + exception.FloatingIpNotFoundForAddress(address='fake')) self.mox.ReplayAll() self.assertRaises(rpc_common.ClientException, self.network.deallocate_floating_ip, @@ -1951,7 +2066,7 @@ class FloatingIPTestCase(test.TestCase): self.mox.StubOutWithMock(self.network.db, 'floating_ip_get_by_address') self.network.db.floating_ip_get_by_address( self.context, '1.2.3.4').AndRaise( - exception.FloatingIpNotFoundForAddress) + exception.FloatingIpNotFoundForAddress(address='fake')) self.mox.ReplayAll() self.assertRaises(rpc_common.ClientException, self.network.associate_floating_ip, @@ -1962,7 +2077,7 @@ class FloatingIPTestCase(test.TestCase): self.mox.StubOutWithMock(self.network.db, 'floating_ip_get_by_address') self.network.db.floating_ip_get_by_address( self.context, '1.2.3.4').AndRaise( - exception.FloatingIpNotFoundForAddress) + exception.FloatingIpNotFoundForAddress(address='fake')) self.mox.ReplayAll() self.assertRaises(rpc_common.ClientException, self.network.disassociate_floating_ip, @@ -1972,7 +2087,7 @@ class FloatingIPTestCase(test.TestCase): """Ensure that FloatingIpNotFoundForAddress is wrapped""" self.mox.StubOutWithMock(self.network.db, 'floating_ip_get') self.network.db.floating_ip_get(self.context, 'fake-id').AndRaise( - exception.FloatingIpNotFound) + exception.FloatingIpNotFound(id='fake')) self.mox.ReplayAll() self.assertRaises(rpc_common.ClientException, self.network.get_floating_ip, diff --git a/nova/tests/network/test_quantumv2.py b/nova/tests/network/test_quantumv2.py index 6cb0728a2..1eab23a03 100644 --- a/nova/tests/network/test_quantumv2.py +++ b/nova/tests/network/test_quantumv2.py @@ -399,7 +399,8 @@ class TestQuantumv2(test.TestCase): else: fixed_ip = fixed_ips.get(net_id) if fixed_ip: - port_req_body['port']['fixed_ip'] = fixed_ip + port_req_body['port']['fixed_ips'] = [{'ip_address': + fixed_ip}] port_req_body['port']['network_id'] = net_id port_req_body['port']['admin_state_up'] = True port_req_body['port']['tenant_id'] = \ diff --git a/nova/tests/network/test_rpcapi.py b/nova/tests/network/test_rpcapi.py index 77a936b63..032996209 100644 --- a/nova/tests/network/test_rpcapi.py +++ b/nova/tests/network/test_rpcapi.py @@ -265,12 +265,13 @@ class NetworkRpcAPITestCase(test.TestCase): def test__associate_floating_ip(self): self._test_network_api('_associate_floating_ip', rpc_method='call', floating_address='fake_addr', fixed_address='fixed_address', - interface='fake_interface', host='fake_host') + interface='fake_interface', host='fake_host', + instance_uuid='fake_uuid', version='1.6') def test__disassociate_floating_ip(self): self._test_network_api('_disassociate_floating_ip', rpc_method='call', address='fake_addr', interface='fake_interface', - host='fake_host') + host='fake_host', instance_uuid='fake_uuid', version='1.6') def test_migrate_instance_start(self): self._test_network_api('migrate_instance_start', rpc_method='call', diff --git a/nova/tests/scheduler/test_filter_scheduler.py b/nova/tests/scheduler/test_filter_scheduler.py index 673e64997..4d7fb02ec 100644 --- a/nova/tests/scheduler/test_filter_scheduler.py +++ b/nova/tests/scheduler/test_filter_scheduler.py @@ -287,7 +287,7 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): hosts = filter_properties['retry']['hosts'] self.assertEqual(1, len(hosts)) - self.assertEqual((host, node), hosts[0]) + self.assertEqual([host, node], hosts[0]) def test_post_select_populate(self): """Test addition of certain filter props after a node is selected""" @@ -300,7 +300,7 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): sched._post_select_populate_filter_properties(filter_properties, host_state) - self.assertEqual(('host', 'node'), + self.assertEqual(['host', 'node'], filter_properties['retry']['hosts'][0]) self.assertEqual({'vcpus': 5}, host_state.limits) @@ -337,5 +337,5 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase): sched.schedule_prep_resize(self.context, image, request_spec, filter_properties, instance, instance_type, reservations) - self.assertEqual([('host', 'node')], + self.assertEqual([['host', 'node']], filter_properties['retry']['hosts']) diff --git a/nova/tests/scheduler/test_host_filters.py b/nova/tests/scheduler/test_host_filters.py index f291e8112..e56278648 100644 --- a/nova/tests/scheduler/test_host_filters.py +++ b/nova/tests/scheduler/test_host_filters.py @@ -1272,8 +1272,8 @@ class HostFiltersTestCase(test.TestCase): filt_cls = self.class_map['RetryFilter']() host = fakes.FakeHostState('host1', 'nodeX', {}) retry = dict(num_attempts=2, - hosts=[('host1', 'node1'), # same host, different node - ('host2', 'node2'), # different host and node + hosts=[['host1', 'node1'], # same host, different node + ['host2', 'node2'], # different host and node ]) filter_properties = dict(retry=retry) self.assertTrue(filt_cls.host_passes(host, filter_properties)) @@ -1283,7 +1283,7 @@ class HostFiltersTestCase(test.TestCase): filt_cls = self.class_map['RetryFilter']() host = fakes.FakeHostState('host1', 'node1', {}) retry = dict(num_attempts=1, - hosts=[('host1', 'node1')]) + hosts=[['host1', 'node1']]) filter_properties = dict(retry=retry) self.assertFalse(filt_cls.host_passes(host, filter_properties)) diff --git a/nova/tests/scheduler/test_rpcapi.py b/nova/tests/scheduler/test_rpcapi.py index b1174559a..da7652a50 100644 --- a/nova/tests/scheduler/test_rpcapi.py +++ b/nova/tests/scheduler/test_rpcapi.py @@ -37,6 +37,9 @@ class SchedulerRpcAPITestCase(test.TestCase): expected_msg = rpcapi.make_msg(method, **kwargs) expected_msg['version'] = expected_version + if method == 'get_backdoor_port': + del expected_msg['args']['host'] + self.fake_args = None self.fake_kwargs = None @@ -84,3 +87,7 @@ class SchedulerRpcAPITestCase(test.TestCase): rpc_method='fanout_cast', service_name='fake_name', host='fake_host', capabilities='fake_capabilities', version='2.4') + + def test_get_backdoor_port(self): + self._test_scheduler_api('get_backdoor_port', rpc_method='call', + host='fake_host', version='2.5') diff --git a/nova/tests/test_cinder.py b/nova/tests/test_cinder.py index 11d29a3ff..dfdd4f3d7 100644 --- a/nova/tests/test_cinder.py +++ b/nova/tests/test_cinder.py @@ -42,6 +42,11 @@ def _stub_volume(**kwargs): volume.update(kwargs) return volume +_image_metadata = { + 'kernel_id': 'fake', + 'ramdisk_id': 'fake' +} + class FakeHTTPClient(cinder.cinder_client.client.HTTPClient): @@ -82,6 +87,13 @@ class FakeHTTPClient(cinder.cinder_client.client.HTTPClient): def get_volumes_nonexisting(self, **kw): raise cinder_exception.NotFound(code=404, message='Resource not found') + def get_volumes_5678(self, **kw): + """Volume with image metadata""" + volume = {'volume': _stub_volume(id='1234', + volume_image_metadata=_image_metadata) + } + return (200, volume) + class FakeCinderClient(cinder.cinder_client.Client): @@ -155,3 +167,9 @@ class CinderTestCase(test.TestCase): def test_get_non_existing_volume(self): self.assertRaises(exception.VolumeNotFound, self.api.get, self.context, 'nonexisting') + + def test_volume_with_image_metadata(self): + volume = self.api.get(self.context, '5678') + self.assert_called('GET', '/volumes/5678') + self.assertTrue('volume_image_metadata' in volume) + self.assertEqual(volume['volume_image_metadata'], _image_metadata) diff --git a/nova/tests/test_configdrive2.py b/nova/tests/test_configdrive2.py index b5f36185d..91c2a4e5e 100644 --- a/nova/tests/test_configdrive2.py +++ b/nova/tests/test_configdrive2.py @@ -45,12 +45,11 @@ class ConfigDriveTestCase(test.TestCase): self.mox.ReplayAll() - c = configdrive.ConfigDriveBuilder() - c._add_file('this/is/a/path/hello', 'This is some content') - (fd, imagefile) = tempfile.mkstemp(prefix='cd_iso_') - os.close(fd) - c._make_iso9660(imagefile) - c.cleanup() + with configdrive.config_drive_helper() as c: + c._add_file('this/is/a/path/hello', 'This is some content') + (fd, imagefile) = tempfile.mkstemp(prefix='cd_iso_') + os.close(fd) + c._make_iso9660(imagefile) # Check cleanup self.assertFalse(os.path.exists(c.tempdir)) @@ -78,12 +77,11 @@ class ConfigDriveTestCase(test.TestCase): self.mox.ReplayAll() - c = configdrive.ConfigDriveBuilder() - c._add_file('this/is/a/path/hello', 'This is some content') - (fd, imagefile) = tempfile.mkstemp(prefix='cd_vfat_') - os.close(fd) - c._make_vfat(imagefile) - c.cleanup() + with configdrive.config_drive_helper() as c: + c._add_file('this/is/a/path/hello', 'This is some content') + (fd, imagefile) = tempfile.mkstemp(prefix='cd_vfat_') + os.close(fd) + c._make_vfat(imagefile) # Check cleanup self.assertFalse(os.path.exists(c.tempdir)) diff --git a/nova/tests/test_db_api.py b/nova/tests/test_db_api.py index ea6e9aea5..af329daf6 100644 --- a/nova/tests/test_db_api.py +++ b/nova/tests/test_db_api.py @@ -246,6 +246,21 @@ class DbApiTestCase(test.TestCase): self.assertEqual(0, len(results)) db.instance_update(ctxt, instance['uuid'], {"task_state": None}) + def test_multi_associate_disassociate(self): + ctxt = context.get_admin_context() + values = {'address': 'floating'} + floating = db.floating_ip_create(ctxt, values) + values = {'address': 'fixed'} + fixed = db.fixed_ip_create(ctxt, values) + res = db.floating_ip_fixed_ip_associate(ctxt, floating, fixed, 'foo') + self.assertEqual(res, fixed) + res = db.floating_ip_fixed_ip_associate(ctxt, floating, fixed, 'foo') + self.assertEqual(res, None) + res = db.floating_ip_disassociate(ctxt, floating) + self.assertEqual(res, fixed) + res = db.floating_ip_disassociate(ctxt, floating) + self.assertEqual(res, None) + def test_network_create_safe(self): ctxt = context.get_admin_context() values = {'host': 'localhost', 'project_id': 'project1'} diff --git a/nova/tests/test_exception.py b/nova/tests/test_exception.py index f7e4bc037..9e34f287c 100644 --- a/nova/tests/test_exception.py +++ b/nova/tests/test_exception.py @@ -117,8 +117,8 @@ class NovaExceptionTestCase(test.TestCase): class FakeNovaException(exception.NovaException): message = "default message: %(mispelled_code)s" - exc = FakeNovaException(code=500) - self.assertEquals(unicode(exc), 'default message: %(mispelled_code)s') + exc = FakeNovaException(code=500, mispelled_code='blah') + self.assertEquals(unicode(exc), 'default message: blah') def test_default_error_code(self): class FakeNovaException(exception.NovaException): diff --git a/nova/tests/test_hypervapi.py b/nova/tests/test_hypervapi.py index 6106503ea..cab877da9 100644 --- a/nova/tests/test_hypervapi.py +++ b/nova/tests/test_hypervapi.py @@ -26,6 +26,7 @@ import sys import uuid from nova.compute import power_state +from nova.compute import task_states from nova import context from nova import db from nova.image import glance @@ -36,6 +37,7 @@ from nova.tests.hyperv import db_fakes from nova.tests.hyperv import hypervutils from nova.tests.hyperv import mockproxy import nova.tests.image.fake as fake_image +from nova.tests import matchers from nova.virt.hyperv import constants from nova.virt.hyperv import driver as driver_hyperv from nova.virt.hyperv import vmutils @@ -201,6 +203,8 @@ class HyperVAPITestCase(basetestcase.BaseTestCase): self._test_spawn_instance(False) def test_spawn_config_drive(self): + self.skip('broken by move to contextlib for configdrive') + self.flags(force_config_drive=True) self.flags(mkisofs_cmd='mkisofs.exe') @@ -212,6 +216,8 @@ class HyperVAPITestCase(basetestcase.BaseTestCase): self.assertEquals(len(vhd_paths), 2) def test_spawn_config_drive_cdrom(self): + self.skip('broken by move to contextlib for configdrive') + self.flags(force_config_drive=True) self.flags(config_drive_cdrom=True) self.flags(mkisofs_cmd='mkisofs.exe') @@ -403,27 +409,55 @@ class HyperVAPITestCase(basetestcase.BaseTestCase): self.assertTrue(self._fetched_image is None) def test_snapshot_with_update_failure(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) + self._spawn_instance(True) self._update_image_raise_exception = True snapshot_name = 'test_snapshot_' + str(uuid.uuid4()) self.assertRaises(vmutils.HyperVException, self._conn.snapshot, - self._context, self._instance_data, snapshot_name) + self._context, self._instance_data, snapshot_name, + func_call_matcher.call) + + # assert states changed in correct order + self.assertIsNone(func_call_matcher.match()) # assert VM snapshots have been removed self.assertEquals(self._hypervutils.get_vm_snapshots_count( self._instance_data["name"]), 0) def test_snapshot(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) + self._spawn_instance(True) snapshot_name = 'test_snapshot_' + str(uuid.uuid4()) - self._conn.snapshot(self._context, self._instance_data, snapshot_name) + self._conn.snapshot(self._context, self._instance_data, snapshot_name, + func_call_matcher.call) self.assertTrue(self._image_metadata and "disk_format" in self._image_metadata and self._image_metadata["disk_format"] == "vhd") + # assert states changed in correct order + self.assertIsNone(func_call_matcher.match()) + # assert VM snapshots have been removed self.assertEquals(self._hypervutils.get_vm_snapshots_count( self._instance_data["name"]), 0) diff --git a/nova/tests/test_imagecache.py b/nova/tests/test_imagecache.py index bfa948ce5..affab4e29 100644 --- a/nova/tests/test_imagecache.py +++ b/nova/tests/test_imagecache.py @@ -26,8 +26,8 @@ import time from nova import test -from nova.compute import manager as compute_manager from nova.compute import vm_states +from nova import conductor from nova import db from nova.openstack.common import cfg from nova.openstack.common import importutils @@ -929,7 +929,7 @@ class ImageCacheManagerTestCase(test.TestCase): def test_compute_manager(self): was = {'called': False} - def fake_get_all(context): + def fake_get_all(context, *args, **kwargs): was['called'] = True return [{'image_ref': '1', 'host': CONF.host, @@ -947,7 +947,9 @@ class ImageCacheManagerTestCase(test.TestCase): with utils.tempdir() as tmpdir: self.flags(instances_path=tmpdir) - self.stubs.Set(db, 'instance_get_all', fake_get_all) + self.stubs.Set(db, 'instance_get_all_by_filters', fake_get_all) compute = importutils.import_object(CONF.compute_manager) + self.flags(use_local=True, group='conductor') + compute.conductor_api = conductor.API() compute._run_image_cache_manager_pass(None) self.assertTrue(was['called']) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index c1016f1f4..6bc18251f 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -32,6 +32,7 @@ from xml.dom import minidom from nova.api.ec2 import cloud from nova.compute import instance_types from nova.compute import power_state +from nova.compute import task_states from nova.compute import vm_mode from nova.compute import vm_states from nova import context @@ -1209,6 +1210,16 @@ class LibvirtConnTestCase(test.TestCase): self.assertEqual(devices, ['vda', 'vdb']) def test_snapshot_in_ami_format(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) + self.flags(libvirt_snapshots_directory='./') # Start test @@ -1238,15 +1249,28 @@ class LibvirtConnTestCase(test.TestCase): self.mox.ReplayAll() conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) - conn.snapshot(self.context, instance_ref, recv_meta['id']) + conn.snapshot(self.context, instance_ref, recv_meta['id'], + func_call_matcher.call) snapshot = image_service.show(context, recv_meta['id']) + self.assertIsNone(func_call_matcher.match()) + self.assertEquals(snapshot['properties']['image_state'], 'available') self.assertEquals(snapshot['properties']['image_state'], 'available') self.assertEquals(snapshot['status'], 'active') self.assertEquals(snapshot['disk_format'], 'ami') self.assertEquals(snapshot['name'], snapshot_name) def test_lxc_snapshot_in_ami_format(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) + self.flags(libvirt_snapshots_directory='./', libvirt_type='lxc') @@ -1277,15 +1301,27 @@ class LibvirtConnTestCase(test.TestCase): self.mox.ReplayAll() conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) - conn.snapshot(self.context, instance_ref, recv_meta['id']) + conn.snapshot(self.context, instance_ref, recv_meta['id'], + func_call_matcher.call) snapshot = image_service.show(context, recv_meta['id']) + self.assertIsNone(func_call_matcher.match()) self.assertEquals(snapshot['properties']['image_state'], 'available') self.assertEquals(snapshot['status'], 'active') self.assertEquals(snapshot['disk_format'], 'ami') self.assertEquals(snapshot['name'], snapshot_name) def test_snapshot_in_raw_format(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) + self.flags(libvirt_snapshots_directory='./') # Start test @@ -1316,15 +1352,27 @@ class LibvirtConnTestCase(test.TestCase): self.mox.ReplayAll() conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) - conn.snapshot(self.context, instance_ref, recv_meta['id']) + conn.snapshot(self.context, instance_ref, recv_meta['id'], + func_call_matcher.call) snapshot = image_service.show(context, recv_meta['id']) + self.assertIsNone(func_call_matcher.match()) self.assertEquals(snapshot['properties']['image_state'], 'available') self.assertEquals(snapshot['status'], 'active') self.assertEquals(snapshot['disk_format'], 'raw') self.assertEquals(snapshot['name'], snapshot_name) def test_lxc_snapshot_in_raw_format(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) + self.flags(libvirt_snapshots_directory='./', libvirt_type='lxc') @@ -1356,15 +1404,27 @@ class LibvirtConnTestCase(test.TestCase): self.mox.ReplayAll() conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) - conn.snapshot(self.context, instance_ref, recv_meta['id']) + conn.snapshot(self.context, instance_ref, recv_meta['id'], + func_call_matcher.call) snapshot = image_service.show(context, recv_meta['id']) + self.assertIsNone(func_call_matcher.match()) self.assertEquals(snapshot['properties']['image_state'], 'available') self.assertEquals(snapshot['status'], 'active') self.assertEquals(snapshot['disk_format'], 'raw') self.assertEquals(snapshot['name'], snapshot_name) def test_snapshot_in_qcow2_format(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) + self.flags(snapshot_image_format='qcow2', libvirt_snapshots_directory='./') @@ -1391,15 +1451,27 @@ class LibvirtConnTestCase(test.TestCase): self.mox.ReplayAll() conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) - conn.snapshot(self.context, instance_ref, recv_meta['id']) + conn.snapshot(self.context, instance_ref, recv_meta['id'], + func_call_matcher.call) snapshot = image_service.show(context, recv_meta['id']) + self.assertIsNone(func_call_matcher.match()) self.assertEquals(snapshot['properties']['image_state'], 'available') self.assertEquals(snapshot['status'], 'active') self.assertEquals(snapshot['disk_format'], 'qcow2') self.assertEquals(snapshot['name'], snapshot_name) def test_lxc_snapshot_in_qcow2_format(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) + self.flags(snapshot_image_format='qcow2', libvirt_snapshots_directory='./', libvirt_type='lxc') @@ -1427,15 +1499,27 @@ class LibvirtConnTestCase(test.TestCase): self.mox.ReplayAll() conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) - conn.snapshot(self.context, instance_ref, recv_meta['id']) + conn.snapshot(self.context, instance_ref, recv_meta['id'], + func_call_matcher.call) snapshot = image_service.show(context, recv_meta['id']) + self.assertIsNone(func_call_matcher.match()) self.assertEquals(snapshot['properties']['image_state'], 'available') self.assertEquals(snapshot['status'], 'active') self.assertEquals(snapshot['disk_format'], 'qcow2') self.assertEquals(snapshot['name'], snapshot_name) def test_snapshot_no_image_architecture(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) + self.flags(libvirt_snapshots_directory='./') # Start test @@ -1465,14 +1549,26 @@ class LibvirtConnTestCase(test.TestCase): self.mox.ReplayAll() conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) - conn.snapshot(self.context, instance_ref, recv_meta['id']) + conn.snapshot(self.context, instance_ref, recv_meta['id'], + func_call_matcher.call) snapshot = image_service.show(context, recv_meta['id']) + self.assertIsNone(func_call_matcher.match()) self.assertEquals(snapshot['properties']['image_state'], 'available') self.assertEquals(snapshot['status'], 'active') self.assertEquals(snapshot['name'], snapshot_name) def test_lxc_snapshot_no_image_architecture(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) + self.flags(libvirt_snapshots_directory='./', libvirt_type='lxc') @@ -1503,14 +1599,26 @@ class LibvirtConnTestCase(test.TestCase): self.mox.ReplayAll() conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) - conn.snapshot(self.context, instance_ref, recv_meta['id']) + conn.snapshot(self.context, instance_ref, recv_meta['id'], + func_call_matcher.call) snapshot = image_service.show(context, recv_meta['id']) + self.assertIsNone(func_call_matcher.match()) self.assertEquals(snapshot['properties']['image_state'], 'available') self.assertEquals(snapshot['status'], 'active') self.assertEquals(snapshot['name'], snapshot_name) def test_snapshot_no_original_image(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) + self.flags(libvirt_snapshots_directory='./') # Start test @@ -1536,14 +1644,26 @@ class LibvirtConnTestCase(test.TestCase): self.mox.ReplayAll() conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) - conn.snapshot(self.context, instance_ref, recv_meta['id']) + conn.snapshot(self.context, instance_ref, recv_meta['id'], + func_call_matcher.call) snapshot = image_service.show(context, recv_meta['id']) + self.assertIsNone(func_call_matcher.match()) self.assertEquals(snapshot['properties']['image_state'], 'available') self.assertEquals(snapshot['status'], 'active') self.assertEquals(snapshot['name'], snapshot_name) def test_lxc_snapshot_no_original_image(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) + self.flags(libvirt_snapshots_directory='./', libvirt_type='lxc') @@ -1570,9 +1690,11 @@ class LibvirtConnTestCase(test.TestCase): self.mox.ReplayAll() conn = libvirt_driver.LibvirtDriver(False) - conn.snapshot(self.context, instance_ref, recv_meta['id']) + conn.snapshot(self.context, instance_ref, recv_meta['id'], + func_call_matcher.call) snapshot = image_service.show(context, recv_meta['id']) + self.assertIsNone(func_call_matcher.match()) self.assertEquals(snapshot['properties']['image_state'], 'available') self.assertEquals(snapshot['status'], 'active') self.assertEquals(snapshot['name'], snapshot_name) @@ -2036,7 +2158,9 @@ class LibvirtConnTestCase(test.TestCase): self.mox.StubOutWithMock(conn, '_compare_cpu') - conn._compare_cpu("asdf").AndRaise(exception.InvalidCPUInfo) + conn._compare_cpu("asdf").AndRaise(exception.InvalidCPUInfo( + reason='foo') + ) self.mox.ReplayAll() self.assertRaises(exception.InvalidCPUInfo, @@ -2435,6 +2559,38 @@ class LibvirtConnTestCase(test.TestCase): shutil.rmtree(os.path.join(CONF.instances_path, CONF.base_dir_name)) + def test_spawn_without_image_meta(self): + self.create_image_called = False + + def fake_none(*args, **kwargs): + return + + def fake_create_image(*args, **kwargs): + self.create_image_called = True + + def fake_get_info(instance): + return {'state': power_state.RUNNING} + + instance_ref = self.test_instance + instance_ref['image_ref'] = 1 + instance = db.instance_create(self.context, instance_ref) + + conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) + self.stubs.Set(conn, 'to_xml', fake_none) + self.stubs.Set(conn, '_create_image', fake_create_image) + self.stubs.Set(conn, '_create_domain_and_network', fake_none) + self.stubs.Set(conn, 'get_info', fake_get_info) + + conn.spawn(self.context, instance, None, [], None) + self.assertFalse(self.create_image_called) + + conn.spawn(self.context, + instance, + {'id': instance['image_ref']}, + [], + None) + self.assertTrue(self.create_image_called) + def test_get_console_output_file(self): fake_libvirt_utils.files['console.log'] = '01234567890' @@ -2596,7 +2752,7 @@ class LibvirtConnTestCase(test.TestCase): def test_immediate_delete(self): def fake_lookup_by_name(instance_name): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id=instance_name) conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) self.stubs.Set(conn, '_lookup_by_name', fake_lookup_by_name) @@ -2699,7 +2855,7 @@ class LibvirtConnTestCase(test.TestCase): return mock def fake_get_info(instance_name): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id=instance_name) conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) self.stubs.Set(conn, '_lookup_by_name', fake_lookup_by_name) @@ -2718,7 +2874,7 @@ class LibvirtConnTestCase(test.TestCase): self.stubs.Set(conn, 'list_instances', list_instances) def get_info(instance_name): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id='fake') self.stubs.Set(conn, 'get_instance_disk_info', get_info) result = conn.get_disk_available_least() @@ -4385,12 +4541,12 @@ class LibvirtVolumeUsageTestCase(test.TestCase): [dict(instance=self.ins_ref, instance_bdms=self.bdms)]) expected_usage = [{'volume': 1, - 'instance_id': 1, + 'instance': self.ins_ref, 'rd_bytes': 688640L, 'wr_req': 0L, 'flush_operations': -1L, 'rd_req': 169L, 'wr_bytes': 0L}, {'volume': 2, - 'instance_id': 1, + 'instance': self.ins_ref, 'rd_bytes': 688640L, 'wr_req': 0L, 'flush_operations': -1L, 'rd_req': 169L, 'wr_bytes': 0L}] diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py index 1f2ea4bc5..25c26ca9c 100644 --- a/nova/tests/test_metadata.py +++ b/nova/tests/test_metadata.py @@ -550,4 +550,5 @@ class MetadataPasswordTestCase(test.TestCase): def test_too_large(self): self.mdinst.password = '' self.assertRaises(webob.exc.HTTPBadRequest, - self._try_set_password, 'a' * 257) + self._try_set_password, + 'a' * (password.MAX_SIZE + 1)) diff --git a/nova/tests/test_misc.py b/nova/tests/test_misc.py index d815678f4..6732c4007 100644 --- a/nova/tests/test_misc.py +++ b/nova/tests/test_misc.py @@ -27,6 +27,8 @@ class ExceptionTestCase(test.TestCase): raise exc() def test_exceptions_raise(self): + # NOTE(dprince): disable format errors since we are not passing kwargs + self.flags(fatal_exception_format_errors=False) for name in dir(exception): exc = getattr(exception, name) if isinstance(exc, type): diff --git a/nova/tests/test_periodic_tasks.py b/nova/tests/test_periodic_tasks.py new file mode 100644 index 000000000..5804ea49b --- /dev/null +++ b/nova/tests/test_periodic_tasks.py @@ -0,0 +1,109 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 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. + + +import fixtures + +from nova import manager +from nova import test + + +class ManagerMetaTestCase(test.TestCase): + """Tests for the meta class which manages the creation of periodic tasks. + """ + + def test_meta(self): + class Manager(object): + __metaclass__ = manager.ManagerMeta + + @manager.periodic_task + def foo(self): + return 'foo' + + @manager.periodic_task(spacing=4) + def bar(self): + return 'bar' + + @manager.periodic_task(enabled=False) + def baz(self): + return 'baz' + + m = Manager() + self.assertEqual(2, len(m._periodic_tasks)) + self.assertEqual(None, m._periodic_spacing['foo']) + self.assertEqual(4, m._periodic_spacing['bar']) + self.assertFalse('baz' in m._periodic_spacing) + + +class Manager(test.TestCase): + """Tests the periodic tasks portion of the manager class.""" + + def test_periodic_tasks_with_idle(self): + class Manager(manager.Manager): + @manager.periodic_task(spacing=200) + def bar(self): + return 'bar' + + m = Manager() + self.assertEqual(1, len(m._periodic_tasks)) + self.assertEqual(200, m._periodic_spacing['bar']) + + # Now a single pass of the periodic tasks + idle = m.periodic_tasks(None) + self.assertAlmostEqual(60, idle, 1) + + def test_periodic_tasks_constant(self): + class Manager(manager.Manager): + @manager.periodic_task(spacing=0) + def bar(self): + return 'bar' + + m = Manager() + idle = m.periodic_tasks(None) + self.assertAlmostEqual(60, idle, 1) + + def test_periodic_tasks_disabled(self): + class Manager(manager.Manager): + @manager.periodic_task(spacing=-1) + def bar(self): + return 'bar' + + m = Manager() + idle = m.periodic_tasks(None) + self.assertAlmostEqual(60, idle, 1) + + def test_external_running_here(self): + self.flags(run_external_periodic_tasks=True) + + class Manager(manager.Manager): + @manager.periodic_task(spacing=200, external_process_ok=True) + def bar(self): + return 'bar' + + m = Manager() + self.assertEqual(1, len(m._periodic_tasks)) + + def test_external_running_elsewhere(self): + self.flags(run_external_periodic_tasks=False) + + class Manager(manager.Manager): + @manager.periodic_task(spacing=200, external_process_ok=True) + def bar(self): + return 'bar' + + m = Manager() + self.assertEqual(0, len(m._periodic_tasks)) diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py index a5a2539cd..50e5d6d8f 100644 --- a/nova/tests/test_quota.py +++ b/nova/tests/test_quota.py @@ -1315,7 +1315,7 @@ class DbQuotaDriverTestCase(test.TestCase): calls.append(('quota_usage_update', context, project_id, resource, kwargs)) if resource == 'nonexist': - raise exception.QuotaUsageNotFound() + raise exception.QuotaUsageNotFound(project_id=project_id) self.stubs.Set(db, 'quota_usage_update', fake_quota_usage_update) ctx = FakeContext('test_project', 'test_class') diff --git a/nova/tests/test_virt_drivers.py b/nova/tests/test_virt_drivers.py index 8e5e258ea..9d9ebcad9 100644 --- a/nova/tests/test_virt_drivers.py +++ b/nova/tests/test_virt_drivers.py @@ -14,7 +14,6 @@ # License for the specific language governing permissions and limitations # under the License. -import __builtin__ import base64 import fixtures import netaddr @@ -115,7 +114,7 @@ class _FakeDriverBackendTestCase(object): # We can't actually make a config drive v2 because ensure_tree has # been faked out - self.stubs.Set(nova.virt.configdrive.ConfigDriveBuilder, + self.stubs.Set(nova.virt.configdrive._ConfigDriveBuilder, 'make_drive', fake_make_drive) def _teardown_fakelibvirt(self): @@ -216,13 +215,15 @@ class _VirtDriverTestCase(_FakeDriverBackendTestCase): img_ref = self.image_service.create(self.ctxt, {'name': 'snap-1'}) self.assertRaises(exception.InstanceNotRunning, self.connection.snapshot, - self.ctxt, instance_ref, img_ref['id']) + self.ctxt, instance_ref, img_ref['id'], + lambda *args, **kwargs: None) @catch_notimplementederror def test_snapshot_running(self): img_ref = self.image_service.create(self.ctxt, {'name': 'snap-1'}) instance_ref, network_info = self._get_running_instance() - self.connection.snapshot(self.ctxt, instance_ref, img_ref['id']) + self.connection.snapshot(self.ctxt, instance_ref, img_ref['id'], + lambda *args, **kwargs: None) @catch_notimplementederror def test_reboot(self): diff --git a/nova/tests/test_vmwareapi.py b/nova/tests/test_vmwareapi.py index 3a404a122..86b3a5730 100644 --- a/nova/tests/test_vmwareapi.py +++ b/nova/tests/test_vmwareapi.py @@ -20,11 +20,13 @@ Test suite for VMWareAPI. """ from nova.compute import power_state +from nova.compute import task_states from nova import context from nova import db from nova import exception from nova import test import nova.tests.image.fake +from nova.tests import matchers from nova.tests.vmwareapi import db_fakes from nova.tests.vmwareapi import stubs from nova.virt.vmwareapi import driver @@ -159,17 +161,29 @@ class VMWareAPIVMTestCase(test.TestCase): self._check_vm_info(info, power_state.RUNNING) def test_snapshot(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) self._create_vm() info = self.conn.get_info({'name': 1}) self._check_vm_info(info, power_state.RUNNING) - self.conn.snapshot(self.context, self.instance, "Test-Snapshot") + self.conn.snapshot(self.context, self.instance, "Test-Snapshot", + func_call_matcher.call) info = self.conn.get_info({'name': 1}) self._check_vm_info(info, power_state.RUNNING) + self.assertIsNone(func_call_matcher.match()) def test_snapshot_non_existent(self): self._create_instance_in_the_db() self.assertRaises(exception.InstanceNotFound, self.conn.snapshot, - self.context, self.instance, "Test-Snapshot") + self.context, self.instance, "Test-Snapshot", + lambda *args, **kwargs: None) def test_reboot(self): self._create_vm() diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index f2799b8f3..8b57dfef4 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -397,6 +397,7 @@ class XenAPIVMTestCase(stubs.XenAPITestBase): self.assertThat(fake_diagnostics, matchers.DictMatches(expected)) def test_instance_snapshot_fails_with_no_primary_vdi(self): + def create_bad_vbd(session, vm_ref, vdi_ref, userdevice, vbd_type='disk', read_only=False, bootable=False, osvol=False): @@ -417,9 +418,20 @@ class XenAPIVMTestCase(stubs.XenAPITestBase): image_id = "my_snapshot_id" self.assertRaises(exception.NovaException, self.conn.snapshot, - self.context, instance, image_id) + self.context, instance, image_id, + lambda *args, **kwargs: None) def test_instance_snapshot(self): + expected_calls = [ + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_PENDING_UPLOAD}}, + {'args': (), + 'kwargs': + {'task_state': task_states.IMAGE_UPLOADING, + 'expected_state': task_states.IMAGE_PENDING_UPLOAD}}] + func_call_matcher = matchers.FunctionCallMatcher(expected_calls) + stubs.stubout_instance_snapshot(self.stubs) stubs.stubout_is_snapshot(self.stubs) # Stubbing out firewall driver as previous stub sets alters @@ -428,7 +440,8 @@ class XenAPIVMTestCase(stubs.XenAPITestBase): instance = self._create_instance() image_id = "my_snapshot_id" - self.conn.snapshot(self.context, instance, image_id) + self.conn.snapshot(self.context, instance, image_id, + func_call_matcher.call) # Ensure VM was torn down vm_labels = [] @@ -447,6 +460,9 @@ class XenAPIVMTestCase(stubs.XenAPITestBase): self.assertEquals(vbd_labels, [instance['name']]) + # Ensure task states changed in correct order + self.assertIsNone(func_call_matcher.match()) + # Ensure VDIs were torn down for vdi_ref in xenapi_fake.get_all('VDI'): vdi_rec = xenapi_fake.get_record('VDI', vdi_ref) @@ -911,7 +927,7 @@ class XenAPIVMTestCase(stubs.XenAPITestBase): def __init__(self): self.finish_revert_migration_called = False - def finish_revert_migration(self, instance): + def finish_revert_migration(self, instance, block_info): self.finish_revert_migration_called = True conn = xenapi_conn.XenAPIDriver(fake.FakeVirtAPI(), False) diff --git a/nova/tests/utils.py b/nova/tests/utils.py index 9fabab593..45d0d295b 100644 --- a/nova/tests/utils.py +++ b/nova/tests/utils.py @@ -81,6 +81,7 @@ def get_test_network_info(count=1): fake_ip = '0.0.0.0/0' fake_ip_2 = '0.0.0.1/0' fake_ip_3 = '0.0.0.1/0' + fake_netmask = '255.255.255.255' fake_vlan = 100 fake_bridge_interface = 'eth0' network = {'bridge': fake, @@ -91,11 +92,13 @@ def get_test_network_info(count=1): 'injected': False} mapping = {'mac': fake, 'dhcp_server': fake, + 'dns': ['fake1', 'fake2'], 'gateway': fake, 'gateway_v6': fake, - 'ips': [{'ip': fake_ip}, {'ip': fake_ip}]} + 'ips': [{'ip': fake_ip, 'netmask': fake_netmask}, + {'ip': fake_ip, 'netmask': fake_netmask}]} if ipv6: - mapping['ip6s'] = [{'ip': fake_ip}, + mapping['ip6s'] = [{'ip': fake_ip, 'netmask': fake_netmask}, {'ip': fake_ip_2}, {'ip': fake_ip_3}] return [(network, mapping) for x in xrange(0, count)] diff --git a/nova/tests/virt/disk/test_api.py b/nova/tests/virt/disk/test_api.py new file mode 100644 index 000000000..15fb2fc2f --- /dev/null +++ b/nova/tests/virt/disk/test_api.py @@ -0,0 +1,60 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 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. + +import tempfile + +import fixtures + +from nova import test +from nova.virt.disk import api + + +class APITestCase(test.TestCase): + + def test_can_resize_need_fs_type_specified(self): + # NOTE(mikal): Bug 1094373 saw a regression where we failed to + # treat a failure to mount as a failure to be able to resize the + # filesystem + def _fake_get_disk_size(path): + return 10 + self.useFixture(fixtures.MonkeyPatch( + 'nova.virt.disk.api.get_disk_size', _fake_get_disk_size)) + + def fake_trycmd(*args, **kwargs): + return '', 'broken' + self.useFixture(fixtures.MonkeyPatch('nova.utils.trycmd', fake_trycmd)) + + def fake_returns_true(*args, **kwargs): + return True + self.useFixture(fixtures.MonkeyPatch( + 'nova.virt.disk.mount.nbd.NbdMount.get_dev', + fake_returns_true)) + self.useFixture(fixtures.MonkeyPatch( + 'nova.virt.disk.mount.nbd.NbdMount.map_dev', + fake_returns_true)) + + # Force the use of localfs, which is what was used during the failure + # reported in the bug + def fake_import_fails(*args, **kwargs): + raise Exception('Failed') + self.useFixture(fixtures.MonkeyPatch( + 'nova.openstack.common.importutils.import_module', + fake_import_fails)) + + imgfile = tempfile.NamedTemporaryFile() + self.addCleanup(imgfile.close) + self.assertFalse(api.can_resize_fs(imgfile, 100, use_cow=True)) diff --git a/nova/tests/virt/disk/test_nbd.py b/nova/tests/virt/disk/test_nbd.py index 16003c9ac..59b0784d9 100644 --- a/nova/tests/virt/disk/test_nbd.py +++ b/nova/tests/virt/disk/test_nbd.py @@ -16,11 +16,12 @@ # under the License. -import fixtures import os +import tempfile -from nova import test +import fixtures +from nova import test from nova.virt.disk.mount import nbd ORIG_EXISTS = os.path.exists @@ -270,3 +271,22 @@ class NbdTestCase(test.TestCase): # No error logged, device consumed self.assertFalse(n.get_dev()) + + def test_do_mount_need_to_specify_fs_type(self): + # NOTE(mikal): Bug 1094373 saw a regression where we failed to + # communicate a failed mount properly. + def fake_trycmd(*args, **kwargs): + return '', 'broken' + self.useFixture(fixtures.MonkeyPatch('nova.utils.trycmd', fake_trycmd)) + + imgfile = tempfile.NamedTemporaryFile() + self.addCleanup(imgfile.close) + tempdir = self.useFixture(fixtures.TempDir()).path + mount = nbd.NbdMount(imgfile.name, tempdir) + + def fake_returns_true(*args, **kwargs): + return True + mount.get_dev = fake_returns_true + mount.map_dev = fake_returns_true + + self.assertFalse(mount.do_mount()) diff --git a/nova/tests/virt/xenapi/test_volumeops.py b/nova/tests/virt/xenapi/test_volumeops.py index bc84386c4..5b5c38139 100644 --- a/nova/tests/virt/xenapi/test_volumeops.py +++ b/nova/tests/virt/xenapi/test_volumeops.py @@ -15,11 +15,12 @@ # under the License. from nova import test +from nova.tests.xenapi import stubs from nova.virt.xenapi import volumeops class VolumeAttachTestCase(test.TestCase): - def test_connect_volume_call(self): + def test_attach_volume_call(self): ops = volumeops.VolumeOps('session') self.mox.StubOutWithMock(ops, 'connect_volume') self.mox.StubOutWithMock(volumeops.vm_utils, 'vm_ref_or_raise') @@ -31,9 +32,79 @@ class VolumeAttachTestCase(test.TestCase): volumeops.volume_utils.get_device_number('mountpoint').AndReturn( 'devnumber') - ops.connect_volume('conn_data', 'devnumber', 'instance_1', 'vmref') + ops.connect_volume( + 'conn_data', 'devnumber', 'instance_1', 'vmref', hotplug=True) self.mox.ReplayAll() ops.attach_volume( dict(driver_volume_type='iscsi', data='conn_data'), 'instance_1', 'mountpoint') + + def test_attach_volume_no_hotplug(self): + ops = volumeops.VolumeOps('session') + self.mox.StubOutWithMock(ops, 'connect_volume') + self.mox.StubOutWithMock(volumeops.vm_utils, 'vm_ref_or_raise') + self.mox.StubOutWithMock(volumeops.volume_utils, 'get_device_number') + + volumeops.vm_utils.vm_ref_or_raise('session', 'instance_1').AndReturn( + 'vmref') + + volumeops.volume_utils.get_device_number('mountpoint').AndReturn( + 'devnumber') + + ops.connect_volume( + 'conn_data', 'devnumber', 'instance_1', 'vmref', hotplug=False) + + self.mox.ReplayAll() + ops.attach_volume( + dict(driver_volume_type='iscsi', data='conn_data'), + 'instance_1', 'mountpoint', hotplug=False) + + def test_connect_volume_no_hotplug(self): + session = stubs.FakeSessionForVolumeTests('fake_uri') + ops = volumeops.VolumeOps(session) + instance_name = 'instance_1' + sr_uuid = '1' + sr_label = 'Disk-for:%s' % instance_name + sr_params = '' + sr_ref = 'sr_ref' + vdi_uuid = '2' + vdi_ref = 'vdi_ref' + vbd_ref = 'vbd_ref' + connection_data = {'vdi_uuid': vdi_uuid} + vm_ref = 'vm_ref' + dev_number = 1 + + called = {'xenapi': False} + + def fake_call_xenapi(self, *args, **kwargs): + # Only used for VBD.plug in this code path. + called['xenapi'] = True + raise Exception() + + self.stubs.Set(ops._session, 'call_xenapi', fake_call_xenapi) + + self.mox.StubOutWithMock(volumeops.volume_utils, 'parse_sr_info') + self.mox.StubOutWithMock(ops, 'introduce_sr') + self.mox.StubOutWithMock(volumeops.volume_utils, 'introduce_vdi') + self.mox.StubOutWithMock(volumeops.vm_utils, 'create_vbd') + + volumeops.volume_utils.parse_sr_info( + connection_data, sr_label).AndReturn( + tuple([sr_uuid, sr_label, sr_params])) + + ops.introduce_sr(sr_uuid, sr_label, sr_params).AndReturn(sr_ref) + + volumeops.volume_utils.introduce_vdi( + session, sr_ref, vdi_uuid, None).AndReturn(vdi_ref) + + volumeops.vm_utils.create_vbd( + session, vm_ref, vdi_ref, dev_number, + bootable=False, osvol=True).AndReturn(vbd_ref) + + self.mox.ReplayAll() + + ops.connect_volume(connection_data, dev_number, instance_name, + vm_ref, hotplug=False) + + self.assertEquals(False, called['xenapi']) diff --git a/nova/utils.py b/nova/utils.py index 26468868a..1056a6e2d 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -556,12 +556,23 @@ class LoopingCallDone(Exception): self.retvalue = retvalue -class LoopingCall(object): +class LoopingCallBase(object): def __init__(self, f=None, *args, **kw): self.args = args self.kw = kw self.f = f self._running = False + self.done = None + + def stop(self): + self._running = False + + def wait(self): + return self.done.wait() + + +class FixedIntervalLoopingCall(LoopingCallBase): + """A looping call which happens at a fixed interval.""" def start(self, interval, initial_delay=None): self._running = True @@ -581,7 +592,7 @@ class LoopingCall(object): self.stop() done.send(e.retvalue) except Exception: - LOG.exception(_('in looping call')) + LOG.exception(_('in fixed duration looping call')) done.send_exception(*sys.exc_info()) return else: @@ -592,11 +603,47 @@ class LoopingCall(object): greenthread.spawn(_inner) return self.done - def stop(self): - self._running = False - def wait(self): - return self.done.wait() +class DynamicLoopingCall(LoopingCallBase): + """A looping call which happens sleeps until the next known event. + + The function called should return how long to sleep for before being + called again. + """ + + def start(self, initial_delay=None, periodic_interval_max=None): + self._running = True + done = event.Event() + + def _inner(): + if initial_delay: + greenthread.sleep(initial_delay) + + try: + while self._running: + idle = self.f(*self.args, **self.kw) + if not self._running: + break + + if not periodic_interval_max is None: + idle = min(idle, periodic_interval_max) + LOG.debug(_('Periodic task processor sleeping for %.02f ' + 'seconds'), idle) + greenthread.sleep(idle) + except LoopingCallDone, e: + self.stop() + done.send(e.retvalue) + except Exception: + LOG.exception(_('in dynamic looping call')) + done.send_exception(*sys.exc_info()) + return + else: + done.send(True) + + self.done = done + + greenthread.spawn(_inner) + return self.done def xhtml_escape(value): diff --git a/nova/virt/baremetal/base.py b/nova/virt/baremetal/base.py index 4b0640885..cf7a33a0a 100644 --- a/nova/virt/baremetal/base.py +++ b/nova/virt/baremetal/base.py @@ -21,27 +21,26 @@ from nova.virt.baremetal import baremetal_states class NodeDriver(object): - def define_vars(self, instance, network_info, block_device_info): - raise NotImplementedError() + def __init__(self): + pass - def create_image(self, var, context, image_meta, node, instance, - injected_files=None, admin_password=None): + def cache_images(self, context, node, instance, **kwargs): raise NotImplementedError() - def destroy_images(self, var, context, node, instance): + def destroy_images(self, context, node, instance): raise NotImplementedError() - def activate_bootloader(self, var, context, node, instance, image_meta): + def activate_bootloader(self, context, node, instance): raise NotImplementedError() - def deactivate_bootloader(self, var, context, node, instance): + def deactivate_bootloader(self, context, node, instance): raise NotImplementedError() - def activate_node(self, var, context, node, instance): + def activate_node(self, context, node, instance): """For operations after power on.""" raise NotImplementedError() - def deactivate_node(self, var, context, node, instance): + def deactivate_node(self, context, node, instance): """For operations before power off.""" raise NotImplementedError() @@ -51,17 +50,21 @@ class NodeDriver(object): class PowerManager(object): - def __init__(self, node): + def __init__(self, **kwargs): + self.state = baremetal_states.DELETED pass def activate_node(self): - return baremetal_states.ACTIVE + self.state = baremetal_states.ACTIVE + return self.state def reboot_node(self): - return baremetal_states.ACTIVE + self.state = baremetal_states.ACTIVE + return self.state def deactivate_node(self): - return baremetal_states.DELETED + self.state = baremetal_states.DELETED + return self.state def is_power_on(self): """Returns True or False according as the node's power state""" diff --git a/nova/virt/baremetal/db/api.py b/nova/virt/baremetal/db/api.py index 3ff533c6c..206a59b4f 100644 --- a/nova/virt/baremetal/db/api.py +++ b/nova/virt/baremetal/db/api.py @@ -98,6 +98,10 @@ def bm_node_update(context, bm_node_id, values): return IMPL.bm_node_update(context, bm_node_id, values) +def bm_node_set_uuid_safe(context, bm_node_id, uuid): + return IMPL.bm_node_set_uuid_safe(context, bm_node_id, uuid) + + def bm_pxe_ip_create(context, address, server_address): return IMPL.bm_pxe_ip_create(context, address, server_address) diff --git a/nova/virt/baremetal/db/sqlalchemy/api.py b/nova/virt/baremetal/db/sqlalchemy/api.py index abdb19fb7..36c66c24f 100644 --- a/nova/virt/baremetal/db/sqlalchemy/api.py +++ b/nova/virt/baremetal/db/sqlalchemy/api.py @@ -151,6 +151,37 @@ def bm_node_update(context, bm_node_id, values): @require_admin_context +def bm_node_set_uuid_safe(context, bm_node_id, values): + """Associate an instance to a node safely + + Associate an instance to a node only if that node is not yet assocated. + Allow the caller to set any other fields they require in the same + operation. For example, this is used to set the node's task_state to + BUILDING at the beginning of driver.spawn(). + + """ + if 'instance_uuid' not in values: + raise exception.NovaException(_( + "instance_uuid must be supplied to bm_node_set_uuid_safe")) + + session = get_session() + with session.begin(): + query = model_query(context, models.BareMetalNode, + session=session, read_deleted="no").\ + filter_by(id=bm_node_id) + + count = query.filter_by(instance_uuid=None).\ + update(values, synchronize_session=False) + if count != 1: + raise exception.NovaException(_( + "Failed to associate instance %(uuid)s to baremetal node " + "%(id)s.") % {'id': bm_node_id, + 'uuid': values['instance_uuid']}) + ref = query.first() + return ref + + +@require_admin_context def bm_node_destroy(context, bm_node_id): model_query(context, models.BareMetalNode).\ filter_by(id=bm_node_id).\ diff --git a/nova/virt/baremetal/db/sqlalchemy/models.py b/nova/virt/baremetal/db/sqlalchemy/models.py index f18c68960..7f9ffb901 100644 --- a/nova/virt/baremetal/db/sqlalchemy/models.py +++ b/nova/virt/baremetal/db/sqlalchemy/models.py @@ -20,8 +20,8 @@ SQLAlchemy models for baremetal data. """ from sqlalchemy import Column, Integer, String -from sqlalchemy import ForeignKey, DateTime, Text, Index from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import ForeignKey, Text from nova.db.sqlalchemy import models diff --git a/nova/virt/baremetal/db/sqlalchemy/session.py b/nova/virt/baremetal/db/sqlalchemy/session.py index 14013669d..d6e2a533d 100644 --- a/nova/virt/baremetal/db/sqlalchemy/session.py +++ b/nova/virt/baremetal/db/sqlalchemy/session.py @@ -52,7 +52,6 @@ def get_session(autocommit=True, expire_on_commit=False): _MAKER = nova_session.get_maker(engine, autocommit, expire_on_commit) session = _MAKER() - session = nova_session.wrap_session(session) return session diff --git a/nova/virt/baremetal/driver.py b/nova/virt/baremetal/driver.py index 120a36c3c..0bc9fec63 100644 --- a/nova/virt/baremetal/driver.py +++ b/nova/virt/baremetal/driver.py @@ -28,7 +28,7 @@ from nova.openstack.common import cfg from nova.openstack.common import importutils from nova.openstack.common import log as logging from nova.virt.baremetal import baremetal_states -from nova.virt.baremetal import db as bmdb +from nova.virt.baremetal import db from nova.virt import driver from nova.virt import firewall from nova.virt.libvirt import imagecache @@ -56,7 +56,7 @@ opts = [ default='nova.virt.baremetal.pxe.PXE', help='Baremetal driver back-end (pxe or tilera)'), cfg.StrOpt('power_manager', - default='nova.virt.baremetal.ipmi.Ipmi', + default='nova.virt.baremetal.ipmi.IPMI', help='Baremetal power management method'), cfg.StrOpt('tftp_root', default='/tftpboot', @@ -79,13 +79,13 @@ DEFAULT_FIREWALL_DRIVER = "%s.%s" % ( def _get_baremetal_nodes(context): - nodes = bmdb.bm_node_get_all(context, service_host=CONF.host) + nodes = db.bm_node_get_all(context, service_host=CONF.host) return nodes def _get_baremetal_node_by_instance_uuid(instance_uuid): ctx = nova_context.get_admin_context() - node = bmdb.bm_node_get_by_instance_uuid(ctx, instance_uuid) + node = db.bm_node_get_by_instance_uuid(ctx, instance_uuid) if node['service_host'] != CONF.host: LOG.error(_("Request for baremetal node %s " "sent to wrong service host") % instance_uuid) @@ -93,19 +93,21 @@ def _get_baremetal_node_by_instance_uuid(instance_uuid): return node -def _update_baremetal_state(context, node, instance, state): - instance_uuid = None - if instance: - instance_uuid = instance['uuid'] - bmdb.bm_node_update(context, node['id'], - {'instance_uuid': instance_uuid, - 'task_state': state, - }) +def _update_state(context, node, instance, state): + """Update the node state in baremetal DB + If instance is not supplied, reset the instance_uuid field for this node. -def get_power_manager(node, **kwargs): + """ + values = {'task_state': state} + if not instance: + values['instance_uuid'] = None + db.bm_node_update(context, node['id'], values) + + +def get_power_manager(**kwargs): cls = importutils.import_class(CONF.baremetal.power_manager) - return cls(node, **kwargs) + return cls(**kwargs) class BareMetalDriver(driver.ComputeDriver): @@ -118,15 +120,15 @@ class BareMetalDriver(driver.ComputeDriver): def __init__(self, virtapi, read_only=False): super(BareMetalDriver, self).__init__(virtapi) - self.baremetal_nodes = importutils.import_object( + self.driver = importutils.import_object( CONF.baremetal.driver) - self._vif_driver = importutils.import_object( + self.vif_driver = importutils.import_object( CONF.baremetal.vif_driver) - self._firewall_driver = firewall.load_driver( + self.firewall_driver = firewall.load_driver( default=DEFAULT_FIREWALL_DRIVER) - self._volume_driver = importutils.import_object( + self.volume_driver = importutils.import_object( CONF.baremetal.volume_driver, virtapi) - self._image_cache_manager = imagecache.ImageCacheManager() + self.image_cache_manager = imagecache.ImageCacheManager() extra_specs = {} extra_specs["baremetal_driver"] = CONF.baremetal.driver @@ -139,9 +141,9 @@ class BareMetalDriver(driver.ComputeDriver): LOG.warning( _('cpu_arch is not found in instance_type_extra_specs')) extra_specs['cpu_arch'] = '' - self._extra_specs = extra_specs + self.extra_specs = extra_specs - self._supported_instances = [ + self.supported_instances = [ (extra_specs['cpu_arch'], 'baremetal', 'baremetal'), ] @@ -174,65 +176,78 @@ class BareMetalDriver(driver.ComputeDriver): def spawn(self, context, instance, image_meta, injected_files, admin_password, network_info=None, block_device_info=None): - nodename = instance.get('node') - if not nodename: - raise exception.NovaException(_("Baremetal node id not supplied" - " to driver")) - node = bmdb.bm_node_get(context, nodename) - if node['instance_uuid']: - raise exception.NovaException(_("Baremetal node %s already" - " in use") % nodename) - - # TODO(deva): split this huge try: block into manageable parts - try: - _update_baremetal_state(context, node, instance, - baremetal_states.BUILDING) - - var = self.baremetal_nodes.define_vars(instance, network_info, - block_device_info) - - self._plug_vifs(instance, network_info, context=context) - self._firewall_driver.setup_basic_filtering(instance, network_info) - self._firewall_driver.prepare_instance_filter(instance, - network_info) + node_id = instance.get('node') + if not node_id: + raise exception.NovaException(_( + "Baremetal node id not supplied to driver")) - self.baremetal_nodes.create_image(var, context, image_meta, node, - instance, - injected_files=injected_files, - admin_password=admin_password) - self.baremetal_nodes.activate_bootloader(var, context, node, - instance, image_meta) - pm = get_power_manager(node) - state = pm.activate_node() + # NOTE(deva): this db method will raise an exception if the node is + # already in use. We call it here to ensure no one else + # allocates this node before we begin provisioning it. + node = db.bm_node_set_uuid_safe(context, node_id, + {'instance_uuid': instance['uuid'], + 'task_state': baremetal_states.BUILDING}) + pm = get_power_manager(node=node, instance=instance) - _update_baremetal_state(context, node, instance, state) + try: + self._plug_vifs(instance, network_info, context=context) - self.baremetal_nodes.activate_node(var, context, node, instance) - self._firewall_driver.apply_instance_filter(instance, network_info) + self.firewall_driver.setup_basic_filtering( + instance, network_info) + self.firewall_driver.prepare_instance_filter( + instance, network_info) + self.firewall_driver.apply_instance_filter( + instance, network_info) - block_device_mapping = driver.block_device_info_get_mapping( - block_device_info) + block_device_mapping = driver.\ + block_device_info_get_mapping(block_device_info) for vol in block_device_mapping: connection_info = vol['connection_info'] mountpoint = vol['mount_device'] - self.attach_volume(connection_info, instance['name'], - mountpoint) - - pm.start_console() + self.attach_volume( + connection_info, instance['name'], mountpoint) + + try: + image_info = self.driver.cache_images( + context, node, instance, + admin_password=admin_password, + image_meta=image_meta, + injected_files=injected_files, + network_info=network_info, + ) + try: + self.driver.activate_bootloader(context, node, instance) + except Exception, e: + self.driver.deactivate_bootloader(context, node, instance) + raise e + except Exception, e: + self.driver.destroy_images(context, node, instance) + raise e except Exception, e: - # TODO(deva): add tooling that can revert a failed spawn - _update_baremetal_state(context, node, instance, - baremetal_states.ERROR) + # TODO(deva): do network and volume cleanup here raise e + else: + # NOTE(deva): pm.activate_node should not raise exceptions. + # We check its success in "finally" block + pm.activate_node() + pm.start_console() + finally: + if pm.state != baremetal_states.ACTIVE: + pm.state = baremetal_states.ERROR + try: + _update_state(context, node, instance, pm.state) + except exception.DBError, e: + LOG.warning(_("Failed to update state record for " + "baremetal node %s") % instance['uuid']) def reboot(self, instance, network_info, reboot_type, block_device_info=None): node = _get_baremetal_node_by_instance_uuid(instance['uuid']) ctx = nova_context.get_admin_context() - pm = get_power_manager(node) + pm = get_power_manager(node=node, instance=instance) state = pm.reboot_node() - _update_baremetal_state(ctx, node, instance, state) + _update_state(ctx, node, instance, state) def destroy(self, instance, network_info, block_device_info=None): ctx = nova_context.get_admin_context() @@ -246,12 +261,9 @@ class BareMetalDriver(driver.ComputeDriver): % instance['uuid']) return - var = self.baremetal_nodes.define_vars(instance, network_info, - block_device_info) - - self.baremetal_nodes.deactivate_node(var, ctx, node, instance) + self.driver.deactivate_node(ctx, node, instance) - pm = get_power_manager(node) + pm = get_power_manager(node=node, instance=instance) pm.stop_console() @@ -267,40 +279,40 @@ class BareMetalDriver(driver.ComputeDriver): mountpoint = vol['mount_device'] self.detach_volume(connection_info, instance['name'], mountpoint) - self.baremetal_nodes.deactivate_bootloader(var, ctx, node, instance) + self.driver.deactivate_bootloader(ctx, node, instance) - self.baremetal_nodes.destroy_images(var, ctx, node, instance) + self.driver.destroy_images(ctx, node, instance) # stop firewall - self._firewall_driver.unfilter_instance(instance, + self.firewall_driver.unfilter_instance(instance, network_info=network_info) self._unplug_vifs(instance, network_info) - _update_baremetal_state(ctx, node, None, state) + _update_state(ctx, node, None, state) def power_off(self, instance): """Power off the specified instance.""" node = _get_baremetal_node_by_instance_uuid(instance['uuid']) - pm = get_power_manager(node) + pm = get_power_manager(node=node, instance=instance) pm.deactivate_node() def power_on(self, instance): """Power on the specified instance""" node = _get_baremetal_node_by_instance_uuid(instance['uuid']) - pm = get_power_manager(node) + pm = get_power_manager(node=node, instance=instance) pm.activate_node() def get_volume_connector(self, instance): - return self._volume_driver.get_volume_connector(instance) + return self.volume_driver.get_volume_connector(instance) def attach_volume(self, connection_info, instance_name, mountpoint): - return self._volume_driver.attach_volume(connection_info, + return self.volume_driver.attach_volume(connection_info, instance_name, mountpoint) @exception.wrap_exception() def detach_volume(self, connection_info, instance_name, mountpoint): - return self._volume_driver.detach_volume(connection_info, + return self.volume_driver.detach_volume(connection_info, instance_name, mountpoint) def get_info(self, instance): @@ -308,7 +320,7 @@ class BareMetalDriver(driver.ComputeDriver): # so we convert from InstanceNotFound inst_uuid = instance.get('uuid') node = _get_baremetal_node_by_instance_uuid(inst_uuid) - pm = get_power_manager(node) + pm = get_power_manager(node=node, instance=instance) ps = power_state.SHUTDOWN if pm.is_power_on(): ps = power_state.RUNNING @@ -319,15 +331,15 @@ class BareMetalDriver(driver.ComputeDriver): 'cpu_time': 0} def refresh_security_group_rules(self, security_group_id): - self._firewall_driver.refresh_security_group_rules(security_group_id) + self.firewall_driver.refresh_security_group_rules(security_group_id) return True def refresh_security_group_members(self, security_group_id): - self._firewall_driver.refresh_security_group_members(security_group_id) + self.firewall_driver.refresh_security_group_members(security_group_id) return True def refresh_provider_fw_rules(self): - self._firewall_driver.refresh_provider_fw_rules() + self.firewall_driver.refresh_provider_fw_rules() def _node_resource(self, node): vcpus_used = 0 @@ -356,27 +368,27 @@ class BareMetalDriver(driver.ComputeDriver): return dic def refresh_instance_security_rules(self, instance): - self._firewall_driver.refresh_instance_security_rules(instance) + self.firewall_driver.refresh_instance_security_rules(instance) def get_available_resource(self, nodename): context = nova_context.get_admin_context() - node = bmdb.bm_node_get(context, nodename) + node = db.bm_node_get(context, nodename) dic = self._node_resource(node) return dic def ensure_filtering_rules_for_instance(self, instance_ref, network_info): - self._firewall_driver.setup_basic_filtering(instance_ref, network_info) - self._firewall_driver.prepare_instance_filter(instance_ref, + self.firewall_driver.setup_basic_filtering(instance_ref, network_info) + self.firewall_driver.prepare_instance_filter(instance_ref, network_info) def unfilter_instance(self, instance_ref, network_info): - self._firewall_driver.unfilter_instance(instance_ref, + self.firewall_driver.unfilter_instance(instance_ref, network_info=network_info) def get_host_stats(self, refresh=False): caps = [] context = nova_context.get_admin_context() - nodes = bmdb.bm_node_get_all(context, + nodes = db.bm_node_get_all(context, service_host=CONF.host) for node in nodes: res = self._node_resource(node) @@ -393,8 +405,8 @@ class BareMetalDriver(driver.ComputeDriver): data['hypervisor_type'] = res['hypervisor_type'] data['hypervisor_version'] = res['hypervisor_version'] data['hypervisor_hostname'] = nodename - data['supported_instances'] = self._supported_instances - data.update(self._extra_specs) + data['supported_instances'] = self.supported_instances + data.update(self.extra_specs) data['host'] = CONF.host data['node'] = nodename # TODO(NTTdocomo): put node's extra specs here @@ -410,24 +422,24 @@ class BareMetalDriver(driver.ComputeDriver): context = nova_context.get_admin_context() node = _get_baremetal_node_by_instance_uuid(instance['uuid']) if node: - pifs = bmdb.bm_interface_get_all_by_bm_node_id(context, node['id']) + pifs = db.bm_interface_get_all_by_bm_node_id(context, node['id']) for pif in pifs: if pif['vif_uuid']: - bmdb.bm_interface_set_vif_uuid(context, pif['id'], None) + db.bm_interface_set_vif_uuid(context, pif['id'], None) for (network, mapping) in network_info: - self._vif_driver.plug(instance, (network, mapping)) + self.vif_driver.plug(instance, (network, mapping)) def _unplug_vifs(self, instance, network_info): for (network, mapping) in network_info: - self._vif_driver.unplug(instance, (network, mapping)) + self.vif_driver.unplug(instance, (network, mapping)) def manage_image_cache(self, context, all_instances): """Manage the local cache of images.""" - self._image_cache_manager.verify_base_images(context, all_instances) + self.image_cache_manager.verify_base_images(context, all_instances) def get_console_output(self, instance): node = _get_baremetal_node_by_instance_uuid(instance['uuid']) - return self.baremetal_nodes.get_console_output(node, instance) + return self.driver.get_console_output(node, instance) def get_available_nodes(self): context = nova_context.get_admin_context() diff --git a/nova/virt/baremetal/fake.py b/nova/virt/baremetal/fake.py index 9df964c39..7a400af6f 100644 --- a/nova/virt/baremetal/fake.py +++ b/nova/virt/baremetal/fake.py @@ -16,37 +16,29 @@ # License for the specific language governing permissions and limitations # under the License. -from nova.virt.baremetal import baremetal_states from nova.virt.baremetal import base +from nova.virt.firewall import NoopFirewallDriver -def get_baremetal_nodes(): - return Fake() +class FakeDriver(base.NodeDriver): - -class Fake(base.NodeDriver): - - def define_vars(self, instance, network_info, block_device_info): - return {} - - def create_image(self, var, context, image_meta, node, instance, - injected_files=None, admin_password=None): + def cache_images(self, context, node, instance, **kwargs): pass - def destroy_images(self, var, context, node, instance): + def destroy_images(self, context, node, instance): pass - def activate_bootloader(self, var, context, node, instance, image_meta): + def activate_bootloader(self, context, node, instance): pass - def deactivate_bootloader(self, var, context, node, instance): + def deactivate_bootloader(self, context, node, instance): pass - def activate_node(self, var, context, node, instance): + def activate_node(self, context, node, instance): """For operations after power on.""" pass - def deactivate_node(self, var, context, node, instance): + def deactivate_node(self, context, node, instance): """For operations before power off.""" pass @@ -56,20 +48,37 @@ class Fake(base.NodeDriver): class FakePowerManager(base.PowerManager): - def activate_node(self): - return baremetal_states.ACTIVE + def __init__(self, **kwargs): + super(FakePowerManager, self).__init__(**kwargs) + + +class FakeFirewallDriver(NoopFirewallDriver): + + def __init__(self): + super(FakeFirewallDriver, self).__init__() + + +class FakeVifDriver(object): + + def __init__(self): + super(FakeVifDriver, self).__init__() + + def plug(self, instance, vif): + pass + + def unplug(self, instance, vif): + pass - def reboot_node(self): - return baremetal_states.ACTIVE - def deactivate_node(self): - return baremetal_states.DELETED +class FakeVolumeDriver(object): - def is_power_on(self): - return True + def __init__(self, virtapi): + super(FakeVolumeDriver, self).__init__() + self.virtapi = virtapi + self._initiator = "fake_initiator" - def start_console(self): + def attach_volume(self, connection_info, instance_name, mountpoint): pass - def stop_console(self): + def detach_volume(self, connection_info, instance_name, mountpoint): pass diff --git a/nova/virt/baremetal/ipmi.py b/nova/virt/baremetal/ipmi.py new file mode 100644 index 000000000..1111a1e40 --- /dev/null +++ b/nova/virt/baremetal/ipmi.py @@ -0,0 +1,256 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# coding=utf-8 + +# Copyright 2012 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2012 NTT DOCOMO, 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. + +""" +Baremetal IPMI power manager. +""" + +import os +import stat +import tempfile + +from nova.exception import InvalidParameterValue +from nova.openstack.common import cfg +from nova.openstack.common import log as logging +from nova import utils +from nova.virt.baremetal import baremetal_states +from nova.virt.baremetal import base +from nova.virt.baremetal import utils as bm_utils + +opts = [ + cfg.StrOpt('terminal', + default='shellinaboxd', + help='path to baremetal terminal program'), + cfg.StrOpt('terminal_cert_dir', + default=None, + help='path to baremetal terminal SSL cert(PEM)'), + cfg.StrOpt('terminal_pid_dir', + default='$state_path/baremetal/console', + help='path to directory stores pidfiles of baremetal_terminal'), + cfg.IntOpt('ipmi_power_retry', + default=5, + help='maximal number of retries for IPMI operations'), + ] + +baremetal_group = cfg.OptGroup(name='baremetal', + title='Baremetal Options') + +CONF = cfg.CONF +CONF.register_group(baremetal_group) +CONF.register_opts(opts, baremetal_group) + +LOG = logging.getLogger(__name__) + + +def _make_password_file(password): + fd, path = tempfile.mkstemp() + os.fchmod(fd, stat.S_IRUSR | stat.S_IWUSR) + with os.fdopen(fd, "w") as f: + f.write(password) + return path + + +def _get_console_pid_path(node_id): + name = "%s.pid" % node_id + path = os.path.join(CONF.baremetal.terminal_pid_dir, name) + return path + + +def _get_console_pid(node_id): + pid_path = _get_console_pid_path(node_id) + if os.path.exists(pid_path): + with open(pid_path, 'r') as f: + pid_str = f.read() + try: + return int(pid_str) + except ValueError: + LOG.warn(_("pid file %s does not contain any pid"), pid_path) + return None + + +class IPMI(base.PowerManager): + """IPMI Power Driver for Baremetal Nova Compute + + This PowerManager class provides mechanism for controlling the power state + of physical hardware via IPMI calls. It also provides serial console access + where available. + + """ + + def __init__(self, node, **kwargs): + self.state = None + self.retries = None + self.node_id = node['id'] + self.address = node['pm_address'] + self.user = node['pm_user'] + self.password = node['pm_password'] + self.port = node['terminal_port'] + + if self.node_id == None: + raise InvalidParameterValue(_("Node id not supplied to IPMI")) + if self.address == None: + raise InvalidParameterValue(_("Address not supplied to IPMI")) + if self.user == None: + raise InvalidParameterValue(_("User not supplied to IPMI")) + if self.password == None: + raise InvalidParameterValue(_("Password not supplied to IPMI")) + + def _exec_ipmitool(self, command): + args = ['ipmitool', + '-I', + 'lanplus', + '-H', + self.address, + '-U', + self.user, + '-f'] + pwfile = _make_password_file(self.password) + try: + args.append(pwfile) + args.extend(command.split(" ")) + out, err = utils.execute(*args, attempts=3) + LOG.debug(_("ipmitool stdout: '%(out)s', stderr: '%(err)%s'"), + locals()) + return out, err + finally: + bm_utils.unlink_without_raise(pwfile) + + def _is_power(self, state): + out_err = self._exec_ipmitool("power status") + return out_err[0] == ("Chassis Power is %s\n" % state) + + def _power_on(self): + """Turn the power to this node ON""" + + def _wait_for_power_on(): + """Called at an interval until the node's power is on""" + + if self._is_power("on"): + self.state = baremetal_states.ACTIVE + raise utils.LoopingCallDone() + if self.retries > CONF.baremetal.ipmi_power_retry: + self.state = baremetal_states.ERROR + raise utils.LoopingCallDone() + try: + self.retries += 1 + self._exec_ipmitool("power on") + except Exception: + LOG.exception(_("IPMI power on failed")) + + self.retries = 0 + timer = utils.FixedIntervalLoopingCall(_wait_for_power_on) + timer.start(interval=0.5).wait() + + def _power_off(self): + """Turn the power to this node OFF""" + + def _wait_for_power_off(): + """Called at an interval until the node's power is off""" + + if self._is_power("off"): + self.state = baremetal_states.DELETED + raise utils.LoopingCallDone() + if self.retries > CONF.baremetal.ipmi_power_retry: + self.state = baremetal_states.ERROR + raise utils.LoopingCallDone() + try: + self.retries += 1 + self._exec_ipmitool("power off") + except Exception: + LOG.exception(_("IPMI power off failed")) + + self.retries = 0 + timer = utils.FixedIntervalLoopingCall(_wait_for_power_off) + timer.start(interval=0.5).wait() + + def _set_pxe_for_next_boot(self): + try: + self._exec_ipmitool("chassis bootdev pxe") + except Exception: + LOG.exception(_("IPMI set next bootdev failed")) + + def activate_node(self): + """Turns the power to node ON""" + if self._is_power("on") and self.state == baremetal_states.ACTIVE: + LOG.warning(_("Activate node called, but node %s " + "is already active") % self.address) + self._set_pxe_for_next_boot() + self._power_on() + return self.state + + def reboot_node(self): + """Cycles the power to a node""" + self._power_off() + self._set_pxe_for_next_boot() + self._power_on() + return self.state + + def deactivate_node(self): + """Turns the power to node OFF, regardless of current state""" + self._power_off() + return self.state + + def is_power_on(self): + return self._is_power("on") + + def start_console(self): + if not self.port: + return + args = [] + args.append(CONF.baremetal.terminal) + if CONF.baremetal.terminal_cert_dir: + args.append("-c") + args.append(CONF.baremetal.terminal_cert_dir) + else: + args.append("-t") + args.append("-p") + args.append(str(self.port)) + args.append("--background=%s" % _get_console_pid_path(self.node_id)) + args.append("-s") + + try: + pwfile = _make_password_file(self.password) + ipmi_args = "/:%(uid)s:%(gid)s:HOME:ipmitool -H %(address)s" \ + " -I lanplus -U %(user)s -f %(pwfile)s sol activate" \ + % {'uid': os.getuid(), + 'gid': os.getgid(), + 'address': self.address, + 'user': self.user, + 'pwfile': pwfile, + } + + args.append(ipmi_args) + # Run shellinaboxd without pipes. Otherwise utils.execute() waits + # infinitely since shellinaboxd does not close passed fds. + x = ["'" + arg.replace("'", "'\\''") + "'" for arg in args] + x.append('</dev/null') + x.append('>/dev/null') + x.append('2>&1') + utils.execute(' '.join(x), shell=True) + finally: + bm_utils.unlink_without_raise(pwfile) + + def stop_console(self): + console_pid = _get_console_pid(self.node_id) + if console_pid: + # Allow exitcode 99 (RC_UNAUTHORIZED) + utils.execute('kill', '-TERM', str(console_pid), + run_as_root=True, + check_exit_code=[0, 99]) + bm_utils.unlink_without_raise(_get_console_pid_path(self.node_id)) diff --git a/nova/virt/baremetal/net-dhcp.ubuntu.template b/nova/virt/baremetal/net-dhcp.ubuntu.template new file mode 100644 index 000000000..e8824a88d --- /dev/null +++ b/nova/virt/baremetal/net-dhcp.ubuntu.template @@ -0,0 +1,21 @@ +# Injected by Nova on instance boot +# +# This file describes the network interfaces available on your system +# and how to activate them. For more information, see interfaces(5). + +# The loopback network interface +auto lo +iface lo inet loopback + +#for $ifc in $interfaces +auto ${ifc.name} +iface ${ifc.name} inet dhcp +#if $ifc.hwaddress + hwaddress ether ${ifc.hwaddress} +#end if + +#if $use_ipv6 +iface ${ifc.name} inet6 dhcp +#end if + +#end for diff --git a/nova/virt/baremetal/interfaces.template b/nova/virt/baremetal/net-static.ubuntu.template index 94776ed49..f14f0ce8c 100644 --- a/nova/virt/baremetal/interfaces.template +++ b/nova/virt/baremetal/net-static.ubuntu.template @@ -12,7 +12,6 @@ auto ${ifc.name} iface ${ifc.name} inet static address ${ifc.address} netmask ${ifc.netmask} - broadcast ${ifc.broadcast} gateway ${ifc.gateway} #if $ifc.dns dns-nameservers ${ifc.dns} diff --git a/nova/virt/baremetal/pxe.py b/nova/virt/baremetal/pxe.py new file mode 100644 index 000000000..4bb61ad39 --- /dev/null +++ b/nova/virt/baremetal/pxe.py @@ -0,0 +1,460 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2012 NTT DOCOMO, 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. + +""" +Class for PXE bare-metal nodes. +""" + +import os +import shutil + +from nova.compute import instance_types +from nova import exception +from nova.openstack.common import cfg +from nova.openstack.common import fileutils +from nova.openstack.common import log as logging +from nova import utils +from nova.virt.baremetal import base +from nova.virt.baremetal import db +from nova.virt.baremetal import utils as bm_utils +from nova.virt.disk import api as disk + + +pxe_opts = [ + cfg.StrOpt('dnsmasq_pid_dir', + default='$state_path/baremetal/dnsmasq', + help='path to directory stores pidfiles of dnsmasq'), + cfg.StrOpt('dnsmasq_lease_dir', + default='$state_path/baremetal/dnsmasq', + help='path to directory stores leasefiles of dnsmasq'), + cfg.StrOpt('deploy_kernel', + help='Default kernel image ID used in deployment phase'), + cfg.StrOpt('deploy_ramdisk', + help='Default ramdisk image ID used in deployment phase'), + cfg.StrOpt('net_config_template', + default='$pybasedir/nova/virt/baremetal/' + 'net-dhcp.ubuntu.template', + help='Template file for injected network config'), + cfg.StrOpt('pxe_append_params', + help='additional append parameters for baremetal PXE boot'), + cfg.StrOpt('pxe_config_template', + default='$pybasedir/nova/virt/baremetal/pxe_config.template', + help='Template file for PXE configuration'), + cfg.StrOpt('pxe_interface', + default='eth0'), + cfg.StrOpt('pxe_path', + default='/usr/lib/syslinux/pxelinux.0', + help='path to pxelinux.0'), + ] + +LOG = logging.getLogger(__name__) + +baremetal_group = cfg.OptGroup(name='baremetal', + title='Baremetal Options') + +CONF = cfg.CONF +CONF.register_group(baremetal_group) +CONF.register_opts(pxe_opts, baremetal_group) + + +CHEETAH = None + + +def _get_cheetah(): + global CHEETAH + if CHEETAH is None: + from Cheetah.Template import Template as CHEETAH + return CHEETAH + + +def build_pxe_config(deployment_id, deployment_key, deployment_iscsi_iqn, + deployment_aki_path, deployment_ari_path, + aki_path, ari_path): + """Build the PXE config file for a node + + This method builds the PXE boot configuration file for a node, + given all the required parameters. + + The resulting file has both a "deploy" and "boot" label, which correspond + to the two phases of booting. This may be extended later. + + """ + LOG.debug(_("Building PXE config for deployment %s.") % deployment_id) + pxe_options = { + 'deployment_id': deployment_id, + 'deployment_key': deployment_key, + 'deployment_iscsi_iqn': deployment_iscsi_iqn, + 'deployment_aki_path': deployment_aki_path, + 'deployment_ari_path': deployment_ari_path, + 'aki_path': aki_path, + 'ari_path': ari_path, + 'pxe_append_params': CONF.baremetal.pxe_append_params, + } + cheetah = _get_cheetah() + pxe_config = str(cheetah( + open(CONF.baremetal.pxe_config_template).read(), + searchList=[{'pxe_options': pxe_options, + 'ROOT': '${ROOT}', + }])) + return pxe_config + + +def build_network_config(network_info): + # TODO(deva): fix assumption that device names begin with "eth" + # and fix assumption about ordering + try: + assert isinstance(network_info, list) + except AssertionError: + network_info = [network_info] + interfaces = [] + for id, (network, mapping) in enumerate(network_info): + address_v6 = None + gateway_v6 = None + netmask_v6 = None + if CONF.use_ipv6: + address_v6 = mapping['ip6s'][0]['ip'] + netmask_v6 = mapping['ip6s'][0]['netmask'] + gateway_v6 = mapping['gateway_v6'] + interface = { + 'name': 'eth%d' % id, + 'hwaddress': mapping['mac'], + 'address': mapping['ips'][0]['ip'], + 'gateway': mapping['gateway'], + 'netmask': mapping['ips'][0]['netmask'], + 'dns': ' '.join(mapping['dns']), + 'address_v6': address_v6, + 'gateway_v6': gateway_v6, + 'netmask_v6': netmask_v6, + } + interfaces.append(interface) + + cheetah = _get_cheetah() + network_config = str(cheetah( + open(CONF.baremetal.net_config_template).read(), + searchList=[ + {'interfaces': interfaces, + 'use_ipv6': CONF.use_ipv6, + } + ])) + return network_config + + +def get_deploy_aki_id(instance): + return instance.get('extra_specs', {}).\ + get('deploy_kernel_id', CONF.baremetal.deploy_kernel) + + +def get_deploy_ari_id(instance): + return instance.get('extra_specs', {}).\ + get('deploy_ramdisk_id', CONF.baremetal.deploy_ramdisk) + + +def get_image_dir_path(instance): + """Generate the dir for an instances disk""" + return os.path.join(CONF.instances_path, instance['name']) + + +def get_image_file_path(instance): + """Generate the full path for an instances disk""" + return os.path.join(CONF.instances_path, instance['name'], 'disk') + + +def get_pxe_config_file_path(instance): + """Generate the path for an instances PXE config file""" + return os.path.join(CONF.baremetal.tftp_root, instance['uuid'], 'config') + + +def get_partition_sizes(instance): + type_id = instance['instance_type_id'] + root_mb = instance['root_gb'] * 1024 + + # NOTE(deva): is there a way to get swap_mb directly from instance? + swap_mb = instance_types.get_instance_type(type_id)['swap'] + + # NOTE(deva): For simpler code paths on the deployment side, + # we always create a swap partition. If the flavor + # does not specify any swap, we default to 1MB + if swap_mb < 1: + swap_mb = 1 + + return (root_mb, swap_mb) + + +def get_pxe_mac_path(mac): + """Convert a MAC address into a PXE config file name""" + return os.path.join( + CONF.baremetal.tftp_root, + 'pxelinux.cfg', + "01-" + mac.replace(":", "-").lower() + ) + + +def get_tftp_image_info(instance): + """Generate the paths for tftp files for this instance + + Raises NovaException if + - instance does not contain kernel_id or ramdisk_id + - deploy_kernel_id or deploy_ramdisk_id can not be read from + instance['extra_specs'] and defaults are not set + + """ + image_info = { + 'kernel': [None, None], + 'ramdisk': [None, None], + 'deploy_kernel': [None, None], + 'deploy_ramdisk': [None, None], + } + try: + image_info['kernel'][0] = str(instance['kernel_id']) + image_info['ramdisk'][0] = str(instance['ramdisk_id']) + image_info['deploy_kernel'][0] = get_deploy_aki_id(instance) + image_info['deploy_ramdisk'][0] = get_deploy_ari_id(instance) + except KeyError as e: + pass + + missing_labels = [] + for label in image_info.keys(): + (uuid, path) = image_info[label] + if uuid is None: + missing_labels.append(label) + else: + image_info[label][1] = os.path.join(CONF.baremetal.tftp_root, + instance['uuid'], label) + if missing_labels: + raise exception.NovaException(_( + "Can not activate PXE bootloader. The following boot parameters " + "were not passed to baremetal driver: %s") % missing_labels) + return image_info + + +class PXE(base.NodeDriver): + """PXE bare metal driver""" + + def __init__(self): + super(PXE, self).__init__() + + def _collect_mac_addresses(self, context, node): + macs = [] + macs.append(db.bm_node_get(context, node['id'])['prov_mac_address']) + for nic in db.bm_interface_get_all_by_bm_node_id(context, node['id']): + if nic['address']: + macs.append(nic['address']) + macs.sort() + return macs + + def _generate_udev_rules(self, context, node): + # TODO(deva): fix assumption that device names begin with "eth" + # and fix assumption of ordering + macs = self._collect_mac_addresses(context, node) + rules = '' + for (i, mac) in enumerate(macs): + rules += 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ' \ + 'ATTR{address}=="%(mac)s", ATTR{dev_id}=="0x0", ' \ + 'ATTR{type}=="1", KERNEL=="eth*", NAME="%(name)s"\n' \ + % {'mac': mac.lower(), + 'name': 'eth%d' % i, + } + return rules + + def _cache_tftp_images(self, context, instance, image_info): + """Fetch the necessary kernels and ramdisks for the instance.""" + fileutils.ensure_tree( + os.path.join(CONF.baremetal.tftp_root, instance['uuid'])) + + LOG.debug(_("Fetching kernel and ramdisk for instance %s") % + instance['name']) + for label in image_info.keys(): + (uuid, path) = image_info[label] + bm_utils.cache_image( + context=context, + target=path, + image_id=uuid, + user_id=instance['user_id'], + project_id=instance['project_id'], + ) + + def _cache_image(self, context, instance, image_meta): + """Fetch the instance's image from Glance + + This method pulls the relevant AMI and associated kernel and ramdisk, + and the deploy kernel and ramdisk from Glance, and writes them + to the appropriate places on local disk. + + Both sets of kernel and ramdisk are needed for PXE booting, so these + are stored under CONF.baremetal.tftp_root. + + At present, the AMI is cached and certain files are injected. + Debian/ubuntu-specific assumptions are made regarding the injected + files. In a future revision, this functionality will be replaced by a + more scalable and os-agnostic approach: the deployment ramdisk will + fetch from Glance directly, and write its own last-mile configuration. + + """ + fileutils.ensure_tree(get_image_dir_path(instance)) + image_path = get_image_file_path(instance) + + LOG.debug(_("Fetching image %(ami)s for instance %(name)s") % + {'ami': image_meta['id'], 'name': instance['name']}) + bm_utils.cache_image(context=context, + target=image_path, + image_id=image_meta['id'], + user_id=instance['user_id'], + project_id=instance['project_id'] + ) + + return [image_meta['id'], image_path] + + def _inject_into_image(self, context, node, instance, network_info, + injected_files=None, admin_password=None): + """Inject last-mile configuration into instances image + + Much of this method is a hack around DHCP and cloud-init + not working together with baremetal provisioning yet. + + """ + # NOTE(deva): We assume that if we're not using a kernel, + # then the target partition is the first partition + partition = None + if not instance['kernel_id']: + partition = "1" + + ssh_key = None + if 'key_data' in instance and instance['key_data']: + ssh_key = str(instance['key_data']) + + if injected_files is None: + injected_files = [] + + net_config = build_network_config(network_info) + udev_rules = self._generate_udev_rules(context, node) + injected_files.append( + ('/etc/udev/rules.d/70-persistent-net.rules', udev_rules)) + + if instance['hostname']: + injected_files.append(('/etc/hostname', instance['hostname'])) + + LOG.debug(_("Injecting files into image for instance %(name)s") % + {'name': instance['name']}) + + bm_utils.inject_into_image( + image=get_image_file_path(instance), + key=ssh_key, + net=net_config, + metadata=instance['metadata'], + admin_password=admin_password, + files=injected_files, + partition=partition, + ) + + def cache_images(self, context, node, instance, + admin_password, image_meta, injected_files, network_info): + """Prepare all the images for this instance""" + tftp_image_info = get_tftp_image_info(instance) + self._cache_tftp_images(context, instance, tftp_image_info) + + self._cache_image(context, instance, image_meta) + self._inject_into_image(context, node, instance, network_info, + injected_files, admin_password) + + def destroy_images(self, context, node, instance): + """Delete instance's image file""" + bm_utils.unlink_without_raise(get_image_file_path(instance)) + bm_utils.unlink_without_raise(get_image_dir_path(instance)) + + def activate_bootloader(self, context, node, instance): + """Configure PXE boot loader for an instance + + Kernel and ramdisk images are downloaded by cache_tftp_images, + and stored in /tftpboot/{uuid}/ + + This method writes the instances config file, and then creates + symlinks for each MAC address in the instance. + + By default, the complete layout looks like this: + + /tftpboot/ + ./{uuid}/ + kernel + ramdisk + deploy_kernel + deploy_ramdisk + config + ./pxelinux.cfg/ + {mac} -> ../{uuid}/config + + """ + image_info = get_tftp_image_info(instance) + (root_mb, swap_mb) = get_partition_sizes(instance) + pxe_config_file_path = get_pxe_config_file_path(instance) + image_file_path = get_image_file_path(instance) + + deployment_key = bm_utils.random_alnum(32) + deployment_iscsi_iqn = "iqn-%s" % instance['uuid'] + deployment_id = db.bm_deployment_create( + context, + deployment_key, + image_file_path, + pxe_config_file_path, + root_mb, + swap_mb + ) + pxe_config = build_pxe_config( + deployment_id, + deployment_key, + deployment_iscsi_iqn, + image_info['deploy_kernel'][1], + image_info['deploy_ramdisk'][1], + image_info['kernel'][1], + image_info['ramdisk'][1], + ) + bm_utils.write_to_file(pxe_config_file_path, pxe_config) + + macs = self._collect_mac_addresses(context, node) + for mac in macs: + mac_path = get_pxe_mac_path(mac) + bm_utils.unlink_without_raise(mac_path) + bm_utils.create_link_without_raise(pxe_config_file_path, mac_path) + + def deactivate_bootloader(self, context, node, instance): + """Delete PXE bootloader images and config""" + try: + image_info = get_tftp_image_info(instance) + except exception.NovaException: + pass + else: + for label in image_info.keys(): + (uuid, path) = image_info[label] + bm_utils.unlink_without_raise(path) + + bm_utils.unlink_without_raise(get_pxe_config_file_path(instance)) + try: + macs = self._collect_mac_addresses(context, node) + except exception.DBError: + pass + else: + for mac in macs: + bm_utils.unlink_without_raise(get_pxe_mac_path(mac)) + + bm_utils.unlink_without_raise( + os.path.join(CONF.baremetal.tftp_root, instance['uuid'])) + + def activate_node(self, context, node, instance): + pass + + def deactivate_node(self, context, node, instance): + pass diff --git a/nova/virt/baremetal/pxe_config.template b/nova/virt/baremetal/pxe_config.template new file mode 100644 index 000000000..f2fcc9b14 --- /dev/null +++ b/nova/virt/baremetal/pxe_config.template @@ -0,0 +1,11 @@ +default deploy + +label deploy +kernel ${pxe_options.deployment_aki_path} +append initrd=${pxe_options.deployment_ari_path} selinux=0 disk=cciss/c0d0,sda,hda,vda iscsi_target_iqn=${pxe_options.deployment_iscsi_iqn} deployment_id=${pxe_options.deployment_id} deployment_key=${pxe_options.deployment_key} ${pxe_options.pxe_append_params} +ipappend 3 + + +label boot +kernel ${pxe_options.aki_path} +append initrd=${pxe_options.ari_path} root=${ROOT} ro ${pxe_options.pxe_append_params} diff --git a/nova/virt/baremetal/utils.py b/nova/virt/baremetal/utils.py index e34ca60f3..0842ae201 100644 --- a/nova/virt/baremetal/utils.py +++ b/nova/virt/baremetal/utils.py @@ -18,9 +18,9 @@ import os from nova.openstack.common import log as logging +from nova.virt.disk import api as disk_api from nova.virt.libvirt import utils as libvirt_utils - LOG = logging.getLogger(__name__) @@ -30,8 +30,38 @@ def cache_image(context, target, image_id, user_id, project_id): user_id, project_id) +def inject_into_image(image, key, net, metadata, admin_password, + files, partition, use_cow=False): + try: + disk_api.inject_data(image, key, net, metadata, admin_password, + files, partition, use_cow) + except Exception as e: + LOG.warn(_("Failed to inject data into image %(image)s. " + "Error: %(e)s") % locals()) + + def unlink_without_raise(path): try: - libvirt_utils.file_delete(path) + os.unlink(path) + except OSError: + LOG.exception(_("Failed to unlink %s") % path) + + +def write_to_file(path, contents): + with open(path, 'w') as f: + f.write(contents) + + +def create_link_without_raise(source, link): + try: + os.symlink(source, link) except OSError: - LOG.exception(_("failed to unlink %s") % path) + LOG.exception(_("Failed to create symlink from %(source)s to %(link)s") + % locals()) + + +def random_alnum(count): + import random + import string + chars = string.ascii_uppercase + string.digits + return "".join(random.choice(chars) for _ in range(count)) diff --git a/nova/virt/baremetal/volume_driver.py b/nova/virt/baremetal/volume_driver.py index 6708b33ab..7f59ec517 100644 --- a/nova/virt/baremetal/volume_driver.py +++ b/nova/virt/baremetal/volume_driver.py @@ -25,7 +25,6 @@ from nova.openstack.common import importutils from nova.openstack.common import log as logging from nova import utils from nova.virt.baremetal import db as bmdb -from nova.virt import driver from nova.virt.libvirt import utils as libvirt_utils opts = [ diff --git a/nova/virt/configdrive.py b/nova/virt/configdrive.py index 2a1001577..321bf8389 100644 --- a/nova/virt/configdrive.py +++ b/nova/virt/configdrive.py @@ -17,6 +17,7 @@ """Config Drive v2 helper.""" +import contextlib import os import shutil import tempfile @@ -54,7 +55,18 @@ CONF = cfg.CONF CONF.register_opts(configdrive_opts) -class ConfigDriveBuilder(object): +@contextlib.contextmanager +def config_drive_helper(instance_md=None): + cdb = _ConfigDriveBuilder(instance_md=instance_md) + try: + yield cdb + finally: + cdb.cleanup() + + +class _ConfigDriveBuilder(object): + """Don't use this directly, use the fancy pants contextlib helper above!""" + def __init__(self, instance_md=None): self.imagefile = None diff --git a/nova/virt/disk/api.py b/nova/virt/disk/api.py index 758299f16..9b490cc1a 100644 --- a/nova/virt/disk/api.py +++ b/nova/virt/disk/api.py @@ -124,30 +124,44 @@ def extend(image, size): def can_resize_fs(image, size, use_cow=False): """Check whether we can resize contained file system.""" + LOG.debug(_('Checking if we can resize image %(image)s. ' + 'size=%(size)s, CoW=%(use_cow)s'), locals()) + # Check that we're increasing the size virt_size = get_disk_size(image) if virt_size >= size: + LOG.debug(_('Cannot resize filesystem %s to a smaller size.'), + image) return False # Check the image is unpartitioned if use_cow: - # Try to mount an unpartitioned qcow2 image try: - inject_data(image, use_cow=True) - except exception.NovaException: + fs = vfs.VFS.instance_for_image(image, 'qcow2', None) + fs.setup() + fs.teardown() + except exception.NovaException, e: + LOG.debug(_('Unable to mount image %(image)s with ' + 'error %(error)s. Cannot resize.'), + {'image': image, + 'error': e}) return False else: # For raw, we can directly inspect the file system try: utils.execute('e2label', image) - except exception.ProcessExecutionError: + except exception.ProcessExecutionError, e: + LOG.debug(_('Unable to determine label for image %(image)s with ' + 'error %(errror)s. Cannot resize.'), + {'image': image, + 'error': e}) return False return True def bind(src, target, instance_name): - """Bind device to a filesytem""" + """Bind device to a filesystem""" if src: utils.execute('touch', target, run_as_root=True) utils.execute('mount', '-o', 'bind', src, target, @@ -252,8 +266,7 @@ class _DiskImage(object): # Public module functions -def inject_data(image, - key=None, net=None, metadata=None, admin_password=None, +def inject_data(image, key=None, net=None, metadata=None, admin_password=None, files=None, partition=None, use_cow=False): """Injects a ssh key and optionally net data into a disk image. diff --git a/nova/virt/disk/mount/api.py b/nova/virt/disk/mount/api.py index ac396ff80..8d17d66c6 100644 --- a/nova/virt/disk/mount/api.py +++ b/nova/virt/disk/mount/api.py @@ -109,7 +109,7 @@ class Mount(object): """Some implementations need to retry their get_dev.""" # NOTE(mikal): This method helps implement retries. The implementation # simply calls _get_dev_retry_helper from their get_dev, and implements - # _inner_get_dev with their device acquistion logic. The NBD + # _inner_get_dev with their device acquisition logic. The NBD # implementation has an example. start_time = time.time() device = self._inner_get_dev() diff --git a/nova/virt/disk/vfs/api.py b/nova/virt/disk/vfs/api.py index 5a3f748e7..445752d9c 100644 --- a/nova/virt/disk/vfs/api.py +++ b/nova/virt/disk/vfs/api.py @@ -91,7 +91,7 @@ class VFS(object): """ Replace the entire contents of the file identified - by @path, wth @content, creating the file if it does + by @path, with @content, creating the file if it does not already exist """ def replace_file(self, path, content): diff --git a/nova/virt/disk/vfs/guestfs.py b/nova/virt/disk/vfs/guestfs.py index 5fe599b89..acea8afdf 100644 --- a/nova/virt/disk/vfs/guestfs.py +++ b/nova/virt/disk/vfs/guestfs.py @@ -89,36 +89,54 @@ class VFSGuestFS(vfs.VFS): self.handle.mount_options("", mount[1], mount[0]) def setup(self): - try: - LOG.debug(_("Setting up appliance for %(imgfile)s %(imgfmt)s") % - {'imgfile': self.imgfile, 'imgfmt': self.imgfmt}) - self.handle = guestfs.GuestFS() + LOG.debug(_("Setting up appliance for %(imgfile)s %(imgfmt)s") % + {'imgfile': self.imgfile, 'imgfmt': self.imgfmt}) + self.handle = guestfs.GuestFS() + try: self.handle.add_drive_opts(self.imgfile, format=self.imgfmt) self.handle.launch() self.setup_os() self.handle.aug_init("/", 0) - except Exception, e: + except RuntimeError, e: + # dereference object and implicitly close() + self.handle = None + raise exception.NovaException( + _("Error mounting %(imgfile)s with libguestfs (%(e)s)") % + {'imgfile': self.imgfile, 'e': e}) + except Exception: self.handle = None raise def teardown(self): LOG.debug(_("Tearing down appliance")) + try: - self.handle.aug_close() - except Exception, e: - LOG.debug(_("Failed to close augeas %s"), str(e)) - try: - self.handle.shutdown() - except Exception, e: - LOG.debug(_("Failed to shutdown appliance %s"), str(e)) - try: - self.handle.close() - except Exception, e: - LOG.debug(_("Failed to close guest handle %s"), str(e)) - self.handle = None + try: + self.handle.aug_close() + except RuntimeError, e: + LOG.warn(_("Failed to close augeas %s"), e) + + try: + self.handle.shutdown() + except AttributeError: + # Older libguestfs versions haven't an explicit shutdown + pass + except RuntimeError, e: + LOG.warn(_("Failed to shutdown appliance %s"), e) + + try: + self.handle.close() + except AttributeError: + # Older libguestfs versions haven't an explicit close + pass + except RuntimeError, e: + LOG.warn(_("Failed to close guest handle %s"), e) + finally: + # dereference object and implicitly close() + self.handle = None @staticmethod def _canonicalize_path(path): @@ -152,7 +170,7 @@ class VFSGuestFS(vfs.VFS): try: self.handle.stat(path) return True - except Exception, e: + except RuntimeError: return False def set_permissions(self, path, mode): diff --git a/nova/virt/disk/vfs/localfs.py b/nova/virt/disk/vfs/localfs.py index 3686994fa..9efa6798b 100644 --- a/nova/virt/disk/vfs/localfs.py +++ b/nova/virt/disk/vfs/localfs.py @@ -74,8 +74,7 @@ class VFSLocalFS(vfs.VFS): self.imgdir, self.partition) if not mount.do_mount(): - raise Exception(_("Failed to mount image: %s") % - mount.error) + raise exception.NovaException(mount.error) self.mount = mount except Exception, e: LOG.debug(_("Failed to mount image %(ex)s)") % diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 9291ac6f8..7d627e80c 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -280,7 +280,7 @@ class ComputeDriver(object): """ raise NotImplementedError() - def snapshot(self, context, instance, image_id): + def snapshot(self, context, instance, image_id, update_task_state): """ Snapshots the specified instance. diff --git a/nova/virt/fake.py b/nova/virt/fake.py index c9cd41680..5d3b3c926 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -26,6 +26,7 @@ semantics of real hypervisor connections. """ from nova.compute import power_state +from nova.compute import task_states from nova import db from nova import exception from nova.openstack.common import log as logging @@ -122,9 +123,10 @@ class FakeDriver(driver.ComputeDriver): fake_instance = FakeInstance(name, state) self.instances[name] = fake_instance - def snapshot(self, context, instance, name): + def snapshot(self, context, instance, name, update_task_state): if not instance['name'] in self.instances: raise exception.InstanceNotRunning(instance_id=instance['uuid']) + update_task_state(task_state=task_states.IMAGE_UPLOADING) def reboot(self, instance, network_info, reboot_type, block_device_info=None): diff --git a/nova/virt/firewall.py b/nova/virt/firewall.py index 8776e59f8..d7a5cbc31 100644 --- a/nova/virt/firewall.py +++ b/nova/virt/firewall.py @@ -53,7 +53,7 @@ class FirewallDriver(object): """ Firewall Driver base class. Defines methods that any driver providing security groups - and provider fireall functionality should implement. + and provider firewall functionality should implement. """ def __init__(self, virtapi): self._virtapi = virtapi diff --git a/nova/virt/hyperv/driver.py b/nova/virt/hyperv/driver.py index 4359b1007..2b57ba0b1 100644 --- a/nova/virt/hyperv/driver.py +++ b/nova/virt/hyperv/driver.py @@ -128,8 +128,8 @@ class HyperVDriver(driver.ComputeDriver): def host_power_action(self, host, action): return self._hostops.host_power_action(host, action) - def snapshot(self, context, instance, name): - self._snapshotops.snapshot(context, instance, name) + def snapshot(self, context, instance, name, update_task_state): + self._snapshotops.snapshot(context, instance, name, update_task_state) def pause(self, instance): self._vmops.pause(instance) diff --git a/nova/virt/hyperv/snapshotops.py b/nova/virt/hyperv/snapshotops.py index 5dc19ebb1..cdc6e45a4 100644 --- a/nova/virt/hyperv/snapshotops.py +++ b/nova/virt/hyperv/snapshotops.py @@ -22,6 +22,7 @@ import os import shutil import sys +from nova.compute import task_states from nova import exception from nova.image import glance from nova.openstack.common import cfg @@ -45,7 +46,7 @@ class SnapshotOps(baseops.BaseOps): super(SnapshotOps, self).__init__() self._vmutils = vmutils.VMUtils() - def snapshot(self, context, instance, name): + def snapshot(self, context, instance, name, update_task_state): """Create snapshot from a running VM instance.""" instance_name = instance["name"] vm = self._vmutils.lookup(self._conn, instance_name) @@ -70,6 +71,8 @@ class SnapshotOps(baseops.BaseOps): raise vmutils.HyperVException( _('Failed to create snapshot for VM %s') % instance_name) + else: + update_task_state(task_state=task_states.IMAGE_PENDING_UPLOAD) export_folder = None f = None @@ -164,6 +167,8 @@ class SnapshotOps(baseops.BaseOps): _("Updating Glance image %(image_id)s with content from " "merged disk %(image_vhd_path)s"), locals()) + update_task_state(task_state=task_states.IMAGE_UPLOADING, + expected_state=task_states.IMAGE_PENDING_UPLOAD) glance_image_service.update(context, image_id, image_metadata, f) LOG.debug(_("Snapshot image %(image_id)s updated for VM " diff --git a/nova/virt/hyperv/vmops.py b/nova/virt/hyperv/vmops.py index fea1034f4..46fbd6cbc 100644 --- a/nova/virt/hyperv/vmops.py +++ b/nova/virt/hyperv/vmops.py @@ -78,8 +78,7 @@ class VMOps(baseops.BaseOps): def get_info(self, instance): """Get information about the VM""" LOG.debug(_("get_info called for instance"), instance=instance) - instance_name = instance["name"] - return self._get_info(instance_name) + return self._get_info(instance['name']) def _get_info(self, instance_name): vm = self._vmutils.lookup(self._conn, instance_name) @@ -120,10 +119,9 @@ class VMOps(baseops.BaseOps): def spawn(self, context, instance, image_meta, injected_files, admin_password, network_info, block_device_info=None): """ Create a new VM and start it.""" - instance_name = instance["name"] - vm = self._vmutils.lookup(self._conn, instance_name) + vm = self._vmutils.lookup(self._conn, instance['name']) if vm is not None: - raise exception.InstanceExists(name=instance_name) + raise exception.InstanceExists(name=instance['name']) ebs_root = self._volumeops.volume_in_mapping( self._volumeops.get_default_root_device(), @@ -132,7 +130,7 @@ class VMOps(baseops.BaseOps): #If is not a boot from volume spawn if not (ebs_root): #Fetch the file, assume it is a VHD file. - vhdfile = self._vmutils.get_vhd_path(instance_name) + vhdfile = self._vmutils.get_vhd_path(instance['name']) try: self._cache_image(fn=self._vmutils.fetch_image, context=context, @@ -154,7 +152,7 @@ class VMOps(baseops.BaseOps): constants.IDE_DISK) else: self._volumeops.attach_boot_volume(block_device_info, - instance_name) + instance['name']) #A SCSI controller for volumes connection is created self._create_scsi_controller(instance['name']) @@ -167,9 +165,9 @@ class VMOps(baseops.BaseOps): self._create_config_drive(instance, injected_files, admin_password) - LOG.debug(_('Starting VM %s '), instance_name) + LOG.debug(_('Starting VM %s '), instance['name']) self._set_vm_state(instance['name'], 'Enabled') - LOG.info(_('Started VM %s '), instance_name) + LOG.info(_('Started VM %s '), instance['name']) except Exception as exn: LOG.exception(_('spawn vm failed: %s'), exn) self.destroy(instance) @@ -194,15 +192,13 @@ class VMOps(baseops.BaseOps): LOG.info(_('Creating config drive at %(path)s'), {'path': configdrive_path_iso}, instance=instance) - cdb = configdrive.ConfigDriveBuilder(instance_md=inst_md) - try: - cdb.make_drive(configdrive_path_iso) - except exception.ProcessExecutionError, e: - LOG.error(_('Creating config drive failed with error: %s'), - e, instance=instance) - raise - finally: - cdb.cleanup() + with configdrive.config_drive_helper(instance_md=inst_md) as cdb: + try: + cdb.make_drive(configdrive_path_iso) + except exception.ProcessExecutionError, e: + LOG.error(_('Creating config drive failed with error: %s'), + e, instance=instance) + raise if not CONF.config_drive_cdrom: drive_type = constants.IDE_DISK @@ -227,11 +223,10 @@ class VMOps(baseops.BaseOps): def _create_vm(self, instance): """Create a VM but don't start it. """ - instance_name = instance["name"] vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] vs_gs_data = self._conn.Msvm_VirtualSystemGlobalSettingData.new() - vs_gs_data.ElementName = instance_name + vs_gs_data.ElementName = instance["name"] (job, ret_val) = vs_man_svc.DefineVirtualSystem( [], None, vs_gs_data.GetText_(1))[1:] if ret_val == constants.WMI_JOB_STATUS_STARTED: @@ -241,10 +236,10 @@ class VMOps(baseops.BaseOps): if not success: raise vmutils.HyperVException(_('Failed to create VM %s') % - instance_name) + instance["name"]) - LOG.debug(_('Created VM %s...'), instance_name) - vm = self._conn.Msvm_ComputerSystem(ElementName=instance_name)[0] + LOG.debug(_('Created VM %s...'), instance["name"]) + vm = self._conn.Msvm_ComputerSystem(ElementName=instance["name"])[0] vmsettings = vm.associators( wmi_result_class='Msvm_VirtualSystemSettingData') @@ -260,7 +255,7 @@ class VMOps(baseops.BaseOps): (job, ret_val) = vs_man_svc.ModifyVirtualSystemResources( vm.path_(), [memsetting.GetText_(1)]) - LOG.debug(_('Set memory for vm %s...'), instance_name) + LOG.debug(_('Set memory for vm %s...'), instance["name"]) procsetting = vmsetting.associators( wmi_result_class='Msvm_ProcessorSettingData')[0] vcpus = long(instance['vcpus']) @@ -273,7 +268,7 @@ class VMOps(baseops.BaseOps): (job, ret_val) = vs_man_svc.ModifyVirtualSystemResources( vm.path_(), [procsetting.GetText_(1)]) - LOG.debug(_('Set vcpus for vm %s...'), instance_name) + LOG.debug(_('Set vcpus for vm %s...'), instance["name"]) def _create_scsi_controller(self, vm_name): """ Create an iscsi controller ready to mount volumes """ @@ -447,24 +442,22 @@ class VMOps(baseops.BaseOps): .associators(wmi_result_class='Msvm_VirtualSwitch')[0] def reboot(self, instance, network_info, reboot_type): - instance_name = instance["name"] """Reboot the specified instance.""" - vm = self._vmutils.lookup(self._conn, instance_name) + vm = self._vmutils.lookup(self._conn, instance['name']) if vm is None: raise exception.InstanceNotFound(instance_id=instance["id"]) - self._set_vm_state(instance_name, 'Reboot') + self._set_vm_state(instance['name'], 'Reboot') def destroy(self, instance, network_info=None, cleanup=True): """Destroy the VM. Also destroy the associated VHD disk files""" - instance_name = instance["name"] - LOG.debug(_("Got request to destroy vm %s"), instance_name) - vm = self._vmutils.lookup(self._conn, instance_name) + LOG.debug(_("Got request to destroy vm %s"), instance['name']) + vm = self._vmutils.lookup(self._conn, instance['name']) if vm is None: return - vm = self._conn.Msvm_ComputerSystem(ElementName=instance_name)[0] + vm = self._conn.Msvm_ComputerSystem(ElementName=instance['name'])[0] vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0] #Stop the VM first. - self._set_vm_state(instance_name, 'Disabled') + self._set_vm_state(instance['name'], 'Disabled') vmsettings = vm.associators( wmi_result_class='Msvm_VirtualSystemSettingData') rasds = vmsettings[0].associators( @@ -492,7 +485,7 @@ class VMOps(baseops.BaseOps): success = True if not success: raise vmutils.HyperVException(_('Failed to destroy vm %s') % - instance_name) + instance['name']) #Disconnect volumes for volume_drive in volumes_drives_list: self._volumeops.disconnect_volume(volume_drive) @@ -501,8 +494,8 @@ class VMOps(baseops.BaseOps): vhdfile = self._conn_cimv2.query( "Select * from CIM_DataFile where Name = '" + disk.replace("'", "''") + "'")[0] - LOG.debug(_("Del: disk %(vhdfile)s vm %(instance_name)s") - % locals()) + LOG.debug(_("Del: disk %(vhdfile)s vm %(name)s") + % {'vhdfile': vhdfile, 'name': instance['name']}) vhdfile.Delete() def pause(self, instance): diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index cfed962d0..e3d95c62e 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -57,6 +57,7 @@ from xml.dom import minidom from nova.api.metadata import base as instance_metadata from nova import block_device from nova.compute import power_state +from nova.compute import task_states from nova.compute import vm_mode from nova import context as nova_context from nova import exception @@ -512,7 +513,7 @@ class LibvirtDriver(driver.ComputeDriver): instance=instance) raise utils.LoopingCallDone() - timer = utils.LoopingCall(_wait_for_destroy) + timer = utils.FixedIntervalLoopingCall(_wait_for_destroy) timer.start(interval=0.5).wait() def destroy(self, instance, network_info, block_device_info=None): @@ -736,7 +737,7 @@ class LibvirtDriver(driver.ComputeDriver): mount_device) @exception.wrap_exception() - def snapshot(self, context, instance, image_href): + def snapshot(self, context, instance, image_href, update_task_state): """Create snapshot from a running VM instance. This command only works with qemu 0.14+ @@ -804,6 +805,7 @@ class LibvirtDriver(driver.ComputeDriver): image_type=source_format) snapshot.create() + update_task_state(task_state=task_states.IMAGE_PENDING_UPLOAD) # Export the snapshot to a raw image snapshot_directory = CONF.libvirt_snapshots_directory @@ -821,6 +823,9 @@ class LibvirtDriver(driver.ComputeDriver): self._create_domain(domain=virt_dom) # Upload that image to the image service + + update_task_state(task_state=task_states.IMAGE_UPLOADING, + expected_state=task_states.IMAGE_PENDING_UPLOAD) with libvirt_utils.file_open(out_path) as image_file: image_service.update(context, image_href, @@ -872,8 +877,9 @@ class LibvirtDriver(driver.ComputeDriver): power_state.CRASHED]: LOG.info(_("Instance shutdown successfully."), instance=instance) - self._create_domain(domain=dom, inst_name=instance['name']) - timer = utils.LoopingCall(self._wait_for_running, instance) + self._create_domain(domain=dom) + timer = utils.FixedIntervalLoopingCall(self._wait_for_running, + instance) timer.start(interval=0.5).wait() return True greenthread.sleep(1) @@ -910,7 +916,7 @@ class LibvirtDriver(driver.ComputeDriver): instance=instance) raise utils.LoopingCallDone() - timer = utils.LoopingCall(_wait_for_reboot) + timer = utils.FixedIntervalLoopingCall(_wait_for_reboot) timer.start(interval=0.5).wait() @exception.wrap_exception() @@ -934,8 +940,9 @@ class LibvirtDriver(driver.ComputeDriver): def power_on(self, instance): """Power on the specified instance""" dom = self._lookup_by_name(instance['name']) - self._create_domain(domain=dom, inst_name=instance['name']) - timer = utils.LoopingCall(self._wait_for_running, instance) + self._create_domain(domain=dom) + timer = utils.FixedIntervalLoopingCall(self._wait_for_running, + instance) timer.start(interval=0.5).wait() @exception.wrap_exception() @@ -1028,7 +1035,9 @@ class LibvirtDriver(driver.ComputeDriver): admin_password, network_info=None, block_device_info=None): xml = self.to_xml(instance, network_info, image_meta, block_device_info=block_device_info) - self._create_image(context, instance, xml, network_info=network_info, + if image_meta: + self._create_image(context, instance, xml, + network_info=network_info, block_device_info=block_device_info, files=injected_files, admin_pass=admin_password) @@ -1045,7 +1054,7 @@ class LibvirtDriver(driver.ComputeDriver): instance=instance) raise utils.LoopingCallDone() - timer = utils.LoopingCall(_wait_for_boot) + timer = utils.FixedIntervalLoopingCall(_wait_for_boot) timer.start(interval=0.5).wait() def _flush_libvirt_console(self, pty): @@ -1207,8 +1216,7 @@ class LibvirtDriver(driver.ComputeDriver): @staticmethod def _get_console_log_path(instance_name): - return os.path.join(CONF.instances_path, instance_name, - 'console.log') + return os.path.join(CONF.instances_path, instance_name, 'console.log') def _chown_console_log_for_instance(self, instance_name): console_log = self._get_console_log_path(instance_name) @@ -1244,7 +1252,8 @@ class LibvirtDriver(driver.ComputeDriver): self._chown_console_log_for_instance(instance['name']) # NOTE(vish): No need add the suffix to console.log - libvirt_utils.write_to_file(basepath('console.log', ''), '', 007) + libvirt_utils.write_to_file( + self._get_console_log_path(instance['name']), '', 007) if not disk_images: disk_images = {'image_id': instance['image_ref'], @@ -1364,20 +1373,17 @@ class LibvirtDriver(driver.ComputeDriver): inst_md = instance_metadata.InstanceMetadata(instance, content=files, extra_md=extra_md) - cdb = configdrive.ConfigDriveBuilder(instance_md=inst_md) - try: + with configdrive.config_drive_helper(instance_md=inst_md) as cdb: configdrive_path = basepath(fname='disk.config') LOG.info(_('Creating config drive at %(path)s'), {'path': configdrive_path}, instance=instance) - cdb.make_drive(configdrive_path) - except exception.ProcessExecutionError, e: - LOG.error(_('Creating config drive failed with error: %s'), - e, instance=instance) - raise - - finally: - cdb.cleanup() + try: + cdb.make_drive(configdrive_path) + except exception.ProcessExecutionError, e: + LOG.error(_('Creating config drive failed with error: %s'), + e, instance=instance) + raise elif any((key, net, metadata, admin_pass, files)): # If we're not using config_drive, inject into root fs @@ -1788,9 +1794,8 @@ class LibvirtDriver(driver.ComputeDriver): # to configure two separate consoles. consolelog = vconfig.LibvirtConfigGuestSerial() consolelog.type = "file" - consolelog.source_path = os.path.join(CONF.instances_path, - instance['name'], - "console.log") + consolelog.source_path = self._get_console_log_path( + instance['name']) guest.add_device(consolelog) consolepty = vconfig.LibvirtConfigGuestSerial() @@ -2200,7 +2205,7 @@ class LibvirtDriver(driver.ComputeDriver): if vol_stats: rd_req, rd_bytes, wr_req, wr_bytes, flush_ops = vol_stats vol_usage.append(dict(volume=bdm['volume_id'], - instance_id=instance['id'], + instance=instance, rd_req=rd_req, rd_bytes=rd_bytes, wr_req=wr_req, @@ -2561,7 +2566,7 @@ class LibvirtDriver(driver.ComputeDriver): recover_method(ctxt, instance_ref, dest, block_migration) # Waiting for completion of live_migration. - timer = utils.LoopingCall(f=None) + timer = utils.FixedIntervalLoopingCall(f=None) def wait_for_live_migration(): """waiting for live migration completion""" @@ -2616,7 +2621,7 @@ class LibvirtDriver(driver.ComputeDriver): os.mkdir(instance_dir) # Touch the console.log file, required by libvirt. - console_file = os.path.join(instance_dir, 'console.log') + console_file = self._get_console_log_path(instance_ref['name']) libvirt_utils.file_open(console_file, 'a').close() # if image has kernel and ramdisk, just download @@ -2970,7 +2975,8 @@ class LibvirtDriver(driver.ComputeDriver): block_device_info=None) self._create_domain_and_network(xml, instance, network_info, block_device_info) - timer = utils.LoopingCall(self._wait_for_running, instance) + timer = utils.FixedIntervalLoopingCall(self._wait_for_running, + instance) timer.start(interval=0.5).wait() @exception.wrap_exception() @@ -2988,7 +2994,8 @@ class LibvirtDriver(driver.ComputeDriver): self._create_domain_and_network(xml, instance, network_info, block_device_info) - timer = utils.LoopingCall(self._wait_for_running, instance) + timer = utils.FixedIntervalLoopingCall(self._wait_for_running, + instance) timer.start(interval=0.5).wait() def confirm_migration(self, migration, instance, network_info): diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py index ef9f9e6af..a3071e0c9 100644 --- a/nova/virt/libvirt/vif.py +++ b/nova/virt/libvirt/vif.py @@ -25,7 +25,6 @@ from nova.openstack.common import cfg from nova.openstack.common import log as logging from nova import utils from nova.virt import netutils -from nova.virt import vif from nova.virt.libvirt import config as vconfig @@ -48,7 +47,7 @@ CONF.import_opt('use_ipv6', 'nova.config') LINUX_DEV_LEN = 14 -class LibvirtBaseVIFDriver(vif.VIFDriver): +class LibvirtBaseVIFDriver(object): def get_config(self, instance, network, mapping): conf = vconfig.LibvirtConfigGuestInterface() diff --git a/nova/virt/netutils.py b/nova/virt/netutils.py index 3196271a4..727b7aac6 100644 --- a/nova/virt/netutils.py +++ b/nova/virt/netutils.py @@ -18,7 +18,7 @@ # under the License. -"""Network-releated utilities for supporting libvirt connection code.""" +"""Network-related utilities for supporting libvirt connection code.""" import netaddr diff --git a/nova/virt/vmwareapi/driver.py b/nova/virt/vmwareapi/driver.py index f6aa91e85..50fc3e922 100644 --- a/nova/virt/vmwareapi/driver.py +++ b/nova/virt/vmwareapi/driver.py @@ -75,9 +75,6 @@ vmwareapi_opts = [ 'socket error, etc. ' 'Used only if compute_driver is ' 'vmwareapi.VMWareESXDriver.'), - cfg.StrOpt('vmwareapi_vlan_interface', - default='vmnic0', - help='Physical ethernet adapter name for vlan networking'), ] CONF = cfg.CONF @@ -130,9 +127,9 @@ class VMWareESXDriver(driver.ComputeDriver): """Create VM instance.""" self._vmops.spawn(context, instance, image_meta, network_info) - def snapshot(self, context, instance, name): + def snapshot(self, context, instance, name, update_task_state): """Create snapshot from a running VM instance.""" - self._vmops.snapshot(context, instance, name) + self._vmops.snapshot(context, instance, name, update_task_state) def reboot(self, instance, network_info, reboot_type, block_device_info=None): @@ -357,8 +354,8 @@ class VMWareAPISession(object): The task is polled until it completes. """ done = event.Event() - loop = utils.LoopingCall(self._poll_task, instance_uuid, task_ref, - done) + loop = utils.FixedIntervalLoopingCall(self._poll_task, instance_uuid, + task_ref, done) loop.start(CONF.vmwareapi_task_poll_interval) ret_val = done.wait() loop.stop() diff --git a/nova/virt/vmwareapi/vif.py b/nova/virt/vmwareapi/vif.py index dc9c6dead..4d53e266d 100644 --- a/nova/virt/vmwareapi/vif.py +++ b/nova/virt/vmwareapi/vif.py @@ -20,70 +20,61 @@ from nova import exception from nova.openstack.common import cfg from nova.openstack.common import log as logging -from nova.virt import vif from nova.virt.vmwareapi import network_utils LOG = logging.getLogger(__name__) CONF = cfg.CONF -CONF.set_default('vmwareapi_vlan_interface', 'vmnic0') +vmwareapi_vif_opts = [ + cfg.StrOpt('vmwareapi_vlan_interface', + default='vmnic0', + help='Physical ethernet adapter name for vlan networking'), +] -class VMWareVlanBridgeDriver(vif.VIFDriver): - """VIF Driver to setup bridge/VLAN networking using VMWare API.""" +CONF.register_opts(vmwareapi_vif_opts) - def plug(self, instance, vif): - """Plug the VIF to specified instance using information passed. - Currently we are plugging the VIF(s) during instance creation itself. - We can use this method when we add support to add additional NIC to - an existing instance.""" - pass - def ensure_vlan_bridge(self, session, network): - """Create a vlan and bridge unless they already exist.""" - vlan_num = network['vlan'] - bridge = network['bridge'] - vlan_interface = CONF.vmwareapi_vlan_interface +def ensure_vlan_bridge(self, session, network): + """Create a vlan and bridge unless they already exist.""" + vlan_num = network['vlan'] + bridge = network['bridge'] + vlan_interface = CONF.vmwareapi_vlan_interface - # Check if the vlan_interface physical network adapter exists on the - # host. - if not network_utils.check_if_vlan_interface_exists(session, - vlan_interface): - raise exception.NetworkAdapterNotFound(adapter=vlan_interface) - - # Get the vSwitch associated with the Physical Adapter - vswitch_associated = network_utils.get_vswitch_for_vlan_interface( - session, vlan_interface) - if vswitch_associated is None: - raise exception.SwitchNotFoundForNetworkAdapter( - adapter=vlan_interface) - # Check whether bridge already exists and retrieve the the ref of the - # network whose name_label is "bridge" - network_ref = network_utils.get_network_with_the_name(session, bridge) - if network_ref is None: - # Create a port group on the vSwitch associated with the - # vlan_interface corresponding physical network adapter on the ESX - # host. - network_utils.create_port_group(session, bridge, - vswitch_associated, vlan_num) - else: - # Get the vlan id and vswitch corresponding to the port group - _get_pg_info = network_utils.get_vlanid_and_vswitch_for_portgroup - pg_vlanid, pg_vswitch = _get_pg_info(session, bridge) + # Check if the vlan_interface physical network adapter exists on the + # host. + if not network_utils.check_if_vlan_interface_exists(session, + vlan_interface): + raise exception.NetworkAdapterNotFound(adapter=vlan_interface) - # Check if the vswitch associated is proper - if pg_vswitch != vswitch_associated: - raise exception.InvalidVLANPortGroup( - bridge=bridge, expected=vswitch_associated, - actual=pg_vswitch) + # Get the vSwitch associated with the Physical Adapter + vswitch_associated = network_utils.get_vswitch_for_vlan_interface( + session, vlan_interface) + if vswitch_associated is None: + raise exception.SwitchNotFoundForNetworkAdapter( + adapter=vlan_interface) + # Check whether bridge already exists and retrieve the the ref of the + # network whose name_label is "bridge" + network_ref = network_utils.get_network_with_the_name(session, bridge) + if network_ref is None: + # Create a port group on the vSwitch associated with the + # vlan_interface corresponding physical network adapter on the ESX + # host. + network_utils.create_port_group(session, bridge, + vswitch_associated, vlan_num) + else: + # Get the vlan id and vswitch corresponding to the port group + _get_pg_info = network_utils.get_vlanid_and_vswitch_for_portgroup + pg_vlanid, pg_vswitch = _get_pg_info(session, bridge) - # Check if the vlan id is proper for the port group - if pg_vlanid != vlan_num: - raise exception.InvalidVLANTag(bridge=bridge, tag=vlan_num, - pgroup=pg_vlanid) + # Check if the vswitch associated is proper + if pg_vswitch != vswitch_associated: + raise exception.InvalidVLANPortGroup( + bridge=bridge, expected=vswitch_associated, + actual=pg_vswitch) - def unplug(self, instance, vif): - """Cleanup operations like deleting port group if no instance - is associated with it.""" - pass + # Check if the vlan id is proper for the port group + if pg_vlanid != vlan_num: + raise exception.InvalidVLANTag(bridge=bridge, tag=vlan_num, + pgroup=pg_vlanid) diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py index 97270fc06..b5b5d1fff 100644 --- a/nova/virt/vmwareapi/vmops.py +++ b/nova/virt/vmwareapi/vmops.py @@ -27,22 +27,19 @@ import urllib2 import uuid from nova.compute import power_state +from nova.compute import task_states from nova import exception from nova.openstack.common import cfg from nova.openstack.common import importutils from nova.openstack.common import log as logging from nova.virt.vmwareapi import network_utils +from nova.virt.vmwareapi import vif as vmwarevif from nova.virt.vmwareapi import vim_util from nova.virt.vmwareapi import vm_util from nova.virt.vmwareapi import vmware_images -vmware_vif_driver_opt = cfg.StrOpt('vmware_vif_driver', - default='nova.virt.vmwareapi.vif.VMWareVlanBridgeDriver', - help='The VMWare VIF driver to configure the VIFs.') - CONF = cfg.CONF -CONF.register_opt(vmware_vif_driver_opt) LOG = logging.getLogger(__name__) @@ -58,7 +55,6 @@ class VMWareVMOps(object): def __init__(self, session): """Initializer.""" self._session = session - self._vif_driver = importutils.import_object(CONF.vmware_vif_driver) def list_instances(self): """Lists the VM instances that are registered with the ESX host.""" @@ -173,8 +169,8 @@ class VMWareVMOps(object): mac_address = mapping['mac'] network_name = network['bridge'] if mapping.get('should_create_vlan'): - network_ref = self._vif_driver.ensure_vlan_bridge( - self._session, network) + network_ref = vmwarevif.ensure_vlan_bridge( + self._session, network) else: network_ref = _check_if_network_bridge_exists(network_name) vif_infos.append({'network_name': network_name, @@ -338,7 +334,7 @@ class VMWareVMOps(object): LOG.debug(_("Powered on the VM instance"), instance=instance) _power_on_vm() - def snapshot(self, context, instance, snapshot_name): + def snapshot(self, context, instance, snapshot_name, update_task_state): """Create snapshot from a running VM instance. Steps followed are: @@ -395,6 +391,7 @@ class VMWareVMOps(object): instance=instance) _create_vm_snapshot() + update_task_state(task_state=task_states.IMAGE_PENDING_UPLOAD) def _check_if_tmp_folder_exists(): # Copy the contents of the VM that were there just before the @@ -473,6 +470,8 @@ class VMWareVMOps(object): LOG.debug(_("Uploaded image %s") % snapshot_name, instance=instance) + update_task_state(task_state=task_states.IMAGE_UPLOADING, + expected_state=task_states.IMAGE_PENDING_UPLOAD) _upload_vmdk_to_image_repository() def _clean_temp_data(): @@ -823,10 +822,8 @@ class VMWareVMOps(object): def plug_vifs(self, instance, network_info): """Plug VIFs into networks.""" - for (network, mapping) in network_info: - self._vif_driver.plug(instance, (network, mapping)) + pass def unplug_vifs(self, instance, network_info): """Unplug VIFs from networks.""" - for (network, mapping) in network_info: - self._vif_driver.unplug(instance, (network, mapping)) + pass diff --git a/nova/virt/xenapi/driver.py b/nova/virt/xenapi/driver.py index 7bf27b7e0..d3047d364 100644 --- a/nova/virt/xenapi/driver.py +++ b/nova/virt/xenapi/driver.py @@ -178,29 +178,19 @@ class XenAPIDriver(driver.ComputeDriver): block_device_info=None): """Finish reverting a resize, powering back on the instance""" # NOTE(vish): Xen currently does not use network info. - self._vmops.finish_revert_migration(instance) - self._attach_mapped_block_devices(instance, block_device_info) + self._vmops.finish_revert_migration(instance, block_device_info) def finish_migration(self, context, migration, instance, disk_info, network_info, image_meta, resize_instance=False, block_device_info=None): """Completes a resize, turning on the migrated instance""" self._vmops.finish_migration(context, migration, instance, disk_info, - network_info, image_meta, resize_instance) - self._attach_mapped_block_devices(instance, block_device_info) + network_info, image_meta, resize_instance, + block_device_info) - def _attach_mapped_block_devices(self, instance, block_device_info): - block_device_mapping = driver.block_device_info_get_mapping( - block_device_info) - for vol in block_device_mapping: - connection_info = vol['connection_info'] - mount_device = vol['mount_device'].rpartition("/")[2] - self.attach_volume(connection_info, - instance['name'], mount_device) - - def snapshot(self, context, instance, image_id): + def snapshot(self, context, instance, image_id, update_task_state): """ Create snapshot from a running VM instance """ - self._vmops.snapshot(context, instance, image_id) + self._vmops.snapshot(context, instance, image_id, update_task_state) def reboot(self, instance, network_info, reboot_type, block_device_info=None): diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py index 9af8a9f41..666e46754 100644 --- a/nova/virt/xenapi/fake.py +++ b/nova/virt/xenapi/fake.py @@ -727,6 +727,8 @@ class SessionBase(object): return lambda *params: self._create(name, params) elif self._is_destroy(name): return lambda *params: self._destroy(name, params) + elif name == 'XenAPI': + return FakeXenAPI() else: return None @@ -890,6 +892,11 @@ class SessionBase(object): return result +class FakeXenAPI(object): + def __init__(self): + self.Failure = Failure + + # Based upon _Method from xmlrpclib. class _Dispatcher: def __init__(self, send, name): diff --git a/nova/virt/xenapi/vif.py b/nova/virt/xenapi/vif.py index ba036a28e..9da105e81 100644 --- a/nova/virt/xenapi/vif.py +++ b/nova/virt/xenapi/vif.py @@ -21,7 +21,6 @@ from nova.openstack.common import cfg from nova.openstack.common import log as logging -from nova.virt import vif from nova.virt.xenapi import network_utils from nova.virt.xenapi import vm_utils @@ -35,7 +34,7 @@ CONF.register_opt(xenapi_ovs_integration_bridge_opt) LOG = logging.getLogger(__name__) -class XenVIFDriver(vif.VIFDriver): +class XenVIFDriver(object): def __init__(self, xenapi_session): self._session = xenapi_session diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index ee36cea0b..adb43a743 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -36,6 +36,7 @@ from eventlet import greenthread from nova import block_device from nova.compute import power_state +from nova.compute import task_states from nova import exception from nova.image import glance from nova.openstack.common import cfg @@ -604,7 +605,11 @@ def get_vdi_for_vm_safely(session, vm_ref): @contextlib.contextmanager -def snapshot_attached_here(session, instance, vm_ref, label): +def snapshot_attached_here(session, instance, vm_ref, label, *args): + update_task_state = None + if len(args) == 1: + update_task_state = args[0] + """Snapshot the root disk only. Return a list of uuids for the vhds in the chain. """ @@ -616,6 +621,8 @@ def snapshot_attached_here(session, instance, vm_ref, label): sr_ref = vm_vdi_rec["SR"] snapshot_ref = session.call_xenapi("VDI.snapshot", vm_vdi_ref, {}) + if update_task_state is not None: + update_task_state(task_state=task_states.IMAGE_PENDING_UPLOAD) try: snapshot_rec = session.call_xenapi("VDI.get_record", snapshot_ref) _wait_for_vhd_coalesce(session, instance, sr_ref, vm_vdi_ref, diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 8d4687fe8..fbf3e0599 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -28,6 +28,7 @@ import netaddr from nova.compute import api as compute from nova.compute import power_state +from nova.compute import task_states from nova.compute import vm_mode from nova.compute import vm_states from nova import context as nova_context @@ -39,11 +40,13 @@ from nova.openstack.common import jsonutils from nova.openstack.common import log as logging from nova.openstack.common import timeutils from nova import utils +from nova.virt import driver as virt_driver from nova.virt import firewall from nova.virt.xenapi import agent as xapi_agent from nova.virt.xenapi import pool_states from nova.virt.xenapi import vm_utils from nova.virt.xenapi import volume_utils +from nova.virt.xenapi import volumeops LOG = logging.getLogger(__name__) @@ -147,6 +150,7 @@ class VMOps(object): self.compute_api = compute.API() self._session = session self._virtapi = virtapi + self._volumeops = volumeops.VolumeOps(self._session) self.firewall_driver = firewall.load_driver( DEFAULT_FIREWALL_DRIVER, self._virtapi, @@ -179,7 +183,20 @@ class VMOps(object): vm_ref = vm_utils.lookup(self._session, name_label) return self._destroy(instance, vm_ref, network_info) - def finish_revert_migration(self, instance): + def _attach_mapped_block_devices(self, instance, block_device_info): + # We are attaching these volumes before start (no hotplugging) + # because some guests (windows) don't load PV drivers quickly + block_device_mapping = virt_driver.block_device_info_get_mapping( + block_device_info) + for vol in block_device_mapping: + connection_info = vol['connection_info'] + mount_device = vol['mount_device'].rpartition("/")[2] + self._volumeops.attach_volume(connection_info, + instance['name'], + mount_device, + hotplug=False) + + def finish_revert_migration(self, instance, block_device_info=None): # NOTE(sirp): the original vm was suffixed with '-orig'; find it using # the old suffix, remove the suffix, then power it back on. name_label = self._get_orig_vm_name_label(instance) @@ -190,6 +207,8 @@ class VMOps(object): name_label = instance['name'] vm_utils.set_vm_name_label(self._session, vm_ref, name_label) + self._attach_mapped_block_devices(instance, block_device_info) + self._start(instance, vm_ref) def finish_migration(self, context, migration, instance, disk_info, @@ -221,6 +240,9 @@ class VMOps(object): {'root': root_vdi}, disk_image_type, network_info, kernel_file, ramdisk_file) + + self._attach_mapped_block_devices(instance, block_device_info) + # 5. Start VM self._start(instance, vm_ref=vm_ref) self._update_instance_progress(context, instance, @@ -605,7 +627,7 @@ class VMOps(object): vm, "start") - def snapshot(self, context, instance, image_id): + def snapshot(self, context, instance, image_id, update_task_state): """Create snapshot from a running VM instance. :param context: request context @@ -633,7 +655,10 @@ class VMOps(object): label = "%s-snapshot" % instance['name'] with vm_utils.snapshot_attached_here( - self._session, instance, vm_ref, label) as vdi_uuids: + self._session, instance, vm_ref, label, + update_task_state) as vdi_uuids: + update_task_state(task_state=task_states.IMAGE_UPLOADING, + expected_state=task_states.IMAGE_PENDING_UPLOAD) vm_utils.upload_image( context, self._session, instance, vdi_uuids, image_id) diff --git a/nova/virt/xenapi/volumeops.py b/nova/virt/xenapi/volumeops.py index d17adeba6..056313478 100644 --- a/nova/virt/xenapi/volumeops.py +++ b/nova/virt/xenapi/volumeops.py @@ -105,7 +105,8 @@ class VolumeOps(object): LOG.exception(exc) raise exception.NovaException(_('Could not forget SR')) - def attach_volume(self, connection_info, instance_name, mountpoint): + def attach_volume(self, connection_info, instance_name, mountpoint, + hotplug=True): """Attach volume storage to VM instance""" vm_ref = vm_utils.vm_ref_or_raise(self._session, instance_name) @@ -121,14 +122,14 @@ class VolumeOps(object): connection_data = connection_info['data'] dev_number = volume_utils.get_device_number(mountpoint) - self.connect_volume( - connection_data, dev_number, instance_name, vm_ref) + self.connect_volume(connection_data, dev_number, instance_name, + vm_ref, hotplug=hotplug) LOG.info(_('Mountpoint %(mountpoint)s attached to' ' instance %(instance_name)s') % locals()) def connect_volume(self, connection_data, dev_number, instance_name, - vm_ref): + vm_ref, hotplug=True): description = 'Disk-for:%s' % instance_name uuid, label, sr_params = volume_utils.parse_sr_info(connection_data, @@ -172,13 +173,14 @@ class VolumeOps(object): raise Exception(_('Unable to use SR %(sr_ref)s for' ' instance %(instance_name)s') % locals()) - try: - self._session.call_xenapi("VBD.plug", vbd_ref) - except self._session.XenAPI.Failure, exc: - LOG.exception(exc) - self.forget_sr(uuid) - raise Exception(_('Unable to attach volume to instance %s') - % instance_name) + if hotplug: + try: + self._session.call_xenapi("VBD.plug", vbd_ref) + except self._session.XenAPI.Failure, exc: + LOG.exception(exc) + self.forget_sr(uuid) + raise Exception(_('Unable to attach volume to instance %s') + % instance_name) def detach_volume(self, connection_info, instance_name, mountpoint): """Detach volume storage to VM instance""" diff --git a/nova/volume/cinder.py b/nova/volume/cinder.py index 1b8305505..04c151d1e 100644 --- a/nova/volume/cinder.py +++ b/nova/volume/cinder.py @@ -20,6 +20,7 @@ Handles all requests relating to volumes + cinder. """ +from copy import deepcopy import sys from cinderclient import exceptions as cinder_exception @@ -117,6 +118,9 @@ def _untranslate_volume_summary_view(context, vol): item['value'] = value d['volume_metadata'].append(item) + if hasattr(vol, 'volume_image_metadata'): + d['volume_image_metadata'] = deepcopy(vol.volume_image_metadata) + return d @@ -50,6 +50,7 @@ setuptools.setup(name='nova', 'bin/nova-api-metadata', 'bin/nova-api-os-compute', 'bin/nova-rpc-zmq-receiver', + 'bin/nova-cells', 'bin/nova-cert', 'bin/nova-clear-rabbit-queues', 'bin/nova-compute', |