diff options
-rw-r--r-- | etc/api-paste.ini | 18 | ||||
-rw-r--r-- | nova/api/openstack/__init__.py | 33 | ||||
-rw-r--r-- | nova/api/openstack/auth.py | 2 | ||||
-rw-r--r-- | nova/api/openstack/common.py | 17 | ||||
-rw-r--r-- | nova/api/openstack/servers.py | 65 | ||||
-rw-r--r-- | nova/api/openstack/views/addresses.py | 16 | ||||
-rw-r--r-- | nova/api/openstack/views/flavors.py | 19 | ||||
-rw-r--r-- | nova/api/openstack/views/images.py | 19 | ||||
-rw-r--r-- | nova/api/openstack/views/servers.py | 67 | ||||
-rw-r--r-- | nova/compute/manager.py | 74 | ||||
-rw-r--r-- | nova/compute/power_state.py | 18 | ||||
-rw-r--r-- | nova/tests/api/openstack/fakes.py | 18 | ||||
-rw-r--r-- | nova/tests/api/openstack/test_auth.py | 6 | ||||
-rw-r--r-- | nova/tests/api/openstack/test_servers.py | 74 | ||||
-rw-r--r-- | nova/tests/test_compute.py | 35 | ||||
-rw-r--r-- | nova/tests/test_virt.py | 15 | ||||
-rw-r--r-- | nova/utils.py | 9 | ||||
-rw-r--r-- | nova/virt/connection.py | 4 | ||||
-rw-r--r-- | nova/virt/driver.py | 234 | ||||
-rw-r--r-- | nova/virt/fake.py | 39 | ||||
-rw-r--r-- | nova/virt/hyperv.py | 19 | ||||
-rw-r--r-- | nova/virt/libvirt_conn.py | 38 | ||||
-rw-r--r-- | nova/virt/xenapi/vmops.py | 21 | ||||
-rw-r--r-- | nova/virt/xenapi_conn.py | 7 |
24 files changed, 695 insertions, 172 deletions
diff --git a/etc/api-paste.ini b/etc/api-paste.ini index d95350fc7..169163289 100644 --- a/etc/api-paste.ini +++ b/etc/api-paste.ini @@ -67,11 +67,14 @@ paste.app_factory = nova.api.ec2.metadatarequesthandler:MetadataRequestHandler.f [composite:osapi] use = egg:Paste#urlmap /: osversions -/v1.0: openstackapi -/v1.1: openstackapi +/v1.0: openstackapi10 +/v1.1: openstackapi11 -[pipeline:openstackapi] -pipeline = faultwrap auth ratelimit osapiapp +[pipeline:openstackapi10] +pipeline = faultwrap auth ratelimit osapiapp10 + +[pipeline:openstackapi11] +pipeline = faultwrap auth ratelimit osapiapp11 [filter:faultwrap] paste.filter_factory = nova.api.openstack:FaultWrapper.factory @@ -82,8 +85,11 @@ paste.filter_factory = nova.api.openstack.auth:AuthMiddleware.factory [filter:ratelimit] paste.filter_factory = nova.api.openstack.limits:RateLimitingMiddleware.factory -[app:osapiapp] -paste.app_factory = nova.api.openstack:APIRouter.factory +[app:osapiapp10] +paste.app_factory = nova.api.openstack:APIRouterV10.factory + +[app:osapiapp11] +paste.app_factory = nova.api.openstack:APIRouterV11.factory [pipeline:osversions] pipeline = faultwrap osversionapp diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index b4c352b08..143b1d2b2 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -72,9 +72,14 @@ class APIRouter(wsgi.Router): return cls() def __init__(self): + self.server_members = {} mapper = routes.Mapper() + self._setup_routes(mapper) + super(APIRouter, self).__init__(mapper) - server_members = {'action': 'POST'} + def _setup_routes(self, mapper): + server_members = self.server_members + server_members['action'] = 'POST' if FLAGS.allow_admin_api: LOG.debug(_("Including admin operations in API.")) @@ -99,10 +104,6 @@ class APIRouter(wsgi.Router): controller=accounts.Controller(), collection={'detail': 'GET'}) - mapper.resource("server", "servers", controller=servers.Controller(), - collection={'detail': 'GET'}, - member=server_members) - mapper.resource("backup_schedule", "backup_schedule", controller=backup_schedules.Controller(), parent_resource=dict(member_name='server', @@ -126,7 +127,27 @@ class APIRouter(wsgi.Router): _limits = limits.LimitsController() mapper.resource("limit", "limits", controller=_limits) - super(APIRouter, self).__init__(mapper) + +class APIRouterV10(APIRouter): + """Define routes specific to OpenStack API V1.0.""" + + def _setup_routes(self, mapper): + super(APIRouterV10, self)._setup_routes(mapper) + mapper.resource("server", "servers", + controller=servers.ControllerV10(), + collection={'detail': 'GET'}, + member=self.server_members) + + +class APIRouterV11(APIRouter): + """Define routes specific to OpenStack API V1.1.""" + + def _setup_routes(self, mapper): + super(APIRouterV11, self)._setup_routes(mapper) + mapper.resource("server", "servers", + controller=servers.ControllerV11(), + collection={'detail': 'GET'}, + member=self.server_members) class Versions(wsgi.Application): diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 5aa5e099b..f3a9bdeca 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -69,8 +69,6 @@ class AuthMiddleware(wsgi.Middleware): return faults.Fault(webob.exc.HTTPUnauthorized()) req.environ['nova.context'] = context.RequestContext(user, account) - version = req.path.split('/')[1].replace('v', '') - req.environ['api.version'] = version return self.application def has_authentication(self, req): diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index d6679de01..bff050347 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -15,7 +15,9 @@ # License for the specific language governing permissions and limitations # under the License. -import webob.exc +from urlparse import urlparse + +import webob from nova import exception @@ -76,5 +78,14 @@ def get_image_id_from_image_hash(image_service, context, image_hash): raise exception.NotFound(image_hash) -def get_api_version(req): - return req.environ.get('api.version') +def get_id_from_href(href): + """Return the id portion of a url as an int. + + Given: http://www.foo.com/bar/123?q=4 + Returns: 123 + + """ + try: + return int(urlparse(href).path.split('/')[-1]) + except: + raise webob.exc.HTTPBadRequest(_('could not parse id from href')) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index d392ab57f..443196146 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -30,8 +30,9 @@ from nova import wsgi from nova import utils from nova.api.openstack import common from nova.api.openstack import faults -from nova.api.openstack.views import servers as servers_views -from nova.api.openstack.views import addresses as addresses_views +import nova.api.openstack.views.addresses +import nova.api.openstack.views.flavors +import nova.api.openstack.views.servers from nova.auth import manager as auth_manager from nova.compute import instance_types from nova.compute import power_state @@ -64,7 +65,7 @@ class Controller(wsgi.Controller): except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) - builder = addresses_views.get_view_builder(req) + builder = self._get_addresses_view_builder(req) return builder.build(instance) def index(self, req): @@ -82,7 +83,7 @@ class Controller(wsgi.Controller): """ instance_list = self.compute_api.get_all(req.environ['nova.context']) limited_list = common.limited(instance_list, req) - builder = servers_views.get_view_builder(req) + builder = self._get_view_builder(req) servers = [builder.build(inst, is_detail)['server'] for inst in limited_list] return dict(servers=servers) @@ -91,7 +92,7 @@ class Controller(wsgi.Controller): """ Returns server details by server id """ try: instance = self.compute_api.get(req.environ['nova.context'], id) - builder = servers_views.get_view_builder(req) + builder = self._get_view_builder(req) return builder.build(instance, is_detail=True) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -120,8 +121,9 @@ class Controller(wsgi.Controller): key_name = key_pair['name'] key_data = key_pair['public_key'] + requested_image_id = self._image_id_from_req_data(env) image_id = common.get_image_id_from_image_hash(self._image_service, - context, env['server']['imageId']) + context, requested_image_id) kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image( req, image_id) @@ -140,10 +142,11 @@ class Controller(wsgi.Controller): if personality: injected_files = self._get_injected_files(personality) + flavor_id = self._flavor_id_from_req_data(env) try: - instances = self.compute_api.create( + (inst,) = self.compute_api.create( context, - instance_types.get_by_flavor_id(env['server']['flavorId']), + instance_types.get_by_flavor_id(flavor_id), image_id, kernel_id=kernel_id, ramdisk_id=ramdisk_id, @@ -156,8 +159,11 @@ class Controller(wsgi.Controller): except QuotaError as error: self._handle_quota_errors(error) - builder = servers_views.get_view_builder(req) - server = builder.build(instances[0], is_detail=False) + inst['instance_type'] = flavor_id + inst['image_id'] = requested_image_id + + builder = self._get_view_builder(req) + server = builder.build(inst, is_detail=True) password = "%s%s" % (server['server']['name'][:4], utils.generate_password(12)) server['server']['adminPass'] = password @@ -512,6 +518,45 @@ class Controller(wsgi.Controller): return kernel_id, ramdisk_id +class ControllerV10(Controller): + def _image_id_from_req_data(self, data): + return data['server']['imageId'] + + def _flavor_id_from_req_data(self, data): + return data['server']['flavorId'] + + def _get_view_builder(self, req): + addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV10() + return nova.api.openstack.views.servers.ViewBuilderV10( + addresses_builder) + + def _get_addresses_view_builder(self, req): + return nova.api.openstack.views.addresses.ViewBuilderV10(req) + + +class ControllerV11(Controller): + def _image_id_from_req_data(self, data): + href = data['server']['imageRef'] + return common.get_id_from_href(href) + + def _flavor_id_from_req_data(self, data): + href = data['server']['flavorRef'] + return common.get_id_from_href(href) + + def _get_view_builder(self, req): + base_url = req.application_url + flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11( + base_url) + image_builder = nova.api.openstack.views.images.ViewBuilderV11( + base_url) + addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11() + return nova.api.openstack.views.servers.ViewBuilderV11( + addresses_builder, flavor_builder, image_builder) + + def _get_addresses_view_builder(self, req): + return nova.api.openstack.views.addresses.ViewBuilderV11(req) + + class ServerCreateRequestXMLDeserializer(object): """ Deserializer to handle xml-formatted server create requests. diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py index 9d392aace..90c77855b 100644 --- a/nova/api/openstack/views/addresses.py +++ b/nova/api/openstack/views/addresses.py @@ -19,18 +19,6 @@ from nova import utils from nova.api.openstack import common -def get_view_builder(req): - ''' - A factory method that returns the correct builder based on the version of - the api requested. - ''' - version = common.get_api_version(req) - if version == '1.1': - return ViewBuilder_1_1() - else: - return ViewBuilder_1_0() - - class ViewBuilder(object): ''' Models a server addresses response as a python dictionary.''' @@ -38,14 +26,14 @@ class ViewBuilder(object): raise NotImplementedError() -class ViewBuilder_1_0(ViewBuilder): +class ViewBuilderV10(ViewBuilder): def build(self, inst): private_ips = utils.get_from_path(inst, 'fixed_ip/address') public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') return dict(public=public_ips, private=private_ips) -class ViewBuilder_1_1(ViewBuilder): +class ViewBuilderV11(ViewBuilder): def build(self, inst): private_ips = utils.get_from_path(inst, 'fixed_ip/address') private_ips = [dict(version=4, addr=a) for a in private_ips] diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py index dd2e75a7a..18bd779c0 100644 --- a/nova/api/openstack/views/flavors.py +++ b/nova/api/openstack/views/flavors.py @@ -18,19 +18,6 @@ from nova.api.openstack import common -def get_view_builder(req): - ''' - A factory method that returns the correct builder based on the version of - the api requested. - ''' - version = common.get_api_version(req) - base_url = req.application_url - if version == '1.1': - return ViewBuilder_1_1(base_url) - else: - return ViewBuilder_1_0() - - class ViewBuilder(object): def __init__(self): pass @@ -39,13 +26,9 @@ class ViewBuilder(object): raise NotImplementedError() -class ViewBuilder_1_1(ViewBuilder): +class ViewBuilderV11(ViewBuilder): def __init__(self, base_url): self.base_url = base_url def generate_href(self, flavor_id): return "%s/flavors/%s" % (self.base_url, flavor_id) - - -class ViewBuilder_1_0(ViewBuilder): - pass diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 2369a8f9d..a6c6ad7d1 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -18,19 +18,6 @@ from nova.api.openstack import common -def get_view_builder(req): - ''' - A factory method that returns the correct builder based on the version of - the api requested. - ''' - version = common.get_api_version(req) - base_url = req.application_url - if version == '1.1': - return ViewBuilder_1_1(base_url) - else: - return ViewBuilder_1_0() - - class ViewBuilder(object): def __init__(self): pass @@ -39,13 +26,9 @@ class ViewBuilder(object): raise NotImplementedError() -class ViewBuilder_1_1(ViewBuilder): +class ViewBuilderV11(ViewBuilder): def __init__(self, base_url): self.base_url = base_url def generate_href(self, image_id): return "%s/images/%s" % (self.base_url, image_id) - - -class ViewBuilder_1_0(ViewBuilder): - pass diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 68f712e56..f93435198 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -27,45 +27,30 @@ from nova.api.openstack.views import images as images_view from nova import utils -def get_view_builder(req): - ''' - A factory method that returns the correct builder based on the version of - the api requested. - ''' - version = common.get_api_version(req) - addresses_builder = addresses_view.get_view_builder(req) - if version == '1.1': - flavor_builder = flavors_view.get_view_builder(req) - image_builder = images_view.get_view_builder(req) - return ViewBuilder_1_1(addresses_builder, flavor_builder, - image_builder) - else: - return ViewBuilder_1_0(addresses_builder) - - class ViewBuilder(object): - ''' - Models a server response as a python dictionary. + """Model a server response as a python dictionary. + + Public methods: build Abstract methods: _build_image, _build_flavor - ''' + + """ def __init__(self, addresses_builder): self.addresses_builder = addresses_builder def build(self, inst, is_detail): - """ - Coerces into dictionary format, mapping everything to - Rackspace-like attributes for return - """ + """Return a dict that represenst a server.""" if is_detail: return self._build_detail(inst) else: return self._build_simple(inst) def _build_simple(self, inst): - return dict(server=dict(id=inst['id'], name=inst['display_name'])) + """Return a simple model of a server.""" + return dict(server=dict(id=inst['id'], name=inst['display_name'])) def _build_detail(self, inst): + """Returns a detailed model of a server.""" power_mapping = { None: 'build', power_state.NOSTATE: 'build', @@ -77,32 +62,26 @@ class ViewBuilder(object): power_state.SHUTOFF: 'active', power_state.CRASHED: 'error', power_state.FAILED: 'error'} - inst_dict = {} - - #mapped_keys = dict(status='state', imageId='image_id', - # flavorId='instance_type', name='display_name', id='id') - mapped_keys = dict(status='state', name='display_name', id='id') - - for k, v in mapped_keys.iteritems(): - inst_dict[k] = inst[v] + inst_dict = { + 'id': int(inst['id']), + 'name': inst['display_name'], + 'addresses': self.addresses_builder.build(inst), + 'status': power_mapping[inst.get('state')]} ctxt = nova.context.get_admin_context() - inst_dict['status'] = power_mapping[inst_dict['status']] compute_api = nova.compute.API() if compute_api.has_finished_migration(ctxt, inst['id']): inst_dict['status'] = 'resize-confirm' - inst_dict['addresses'] = self.addresses_builder.build(inst) - # Return the metadata as a dictionary metadata = {} - for item in inst['metadata']: + for item in inst.get('metadata', []): metadata[item['key']] = item['value'] inst_dict['metadata'] = metadata inst_dict['hostId'] = '' - if inst['host']: + if inst.get('host'): inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() self._build_image(inst_dict, inst) @@ -111,21 +90,27 @@ class ViewBuilder(object): return dict(server=inst_dict) def _build_image(self, response, inst): + """Return the image sub-resource of a server.""" raise NotImplementedError() def _build_flavor(self, response, inst): + """Return the flavor sub-resource of a server.""" raise NotImplementedError() -class ViewBuilder_1_0(ViewBuilder): +class ViewBuilderV10(ViewBuilder): + """Model an Openstack API V1.0 server response.""" + def _build_image(self, response, inst): - response["imageId"] = inst["image_id"] + response['imageId'] = inst['image_id'] def _build_flavor(self, response, inst): - response["flavorId"] = inst["instance_type"] + response['flavorId'] = inst['instance_type'] + +class ViewBuilderV11(ViewBuilder): + """Model an Openstack API V1.0 server response.""" -class ViewBuilder_1_1(ViewBuilder): def __init__(self, addresses_builder, flavor_builder, image_builder): ViewBuilder.__init__(self, addresses_builder) self.flavor_builder = flavor_builder diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 432270467..7316d1510 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -2,6 +2,7 @@ # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Justin Santa Barbara # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -41,9 +42,10 @@ import string import socket import sys import tempfile -import time import functools +from eventlet import greenthread + from nova import exception from nova import flags from nova import log as logging @@ -51,6 +53,7 @@ from nova import manager from nova import rpc from nova import utils from nova.compute import power_state +from nova.virt import driver FLAGS = flags.FLAGS flags.DEFINE_string('instances_path', '$state_path/instances', @@ -120,9 +123,11 @@ class ComputeManager(manager.Manager): compute_driver = FLAGS.compute_driver try: - self.driver = utils.import_object(compute_driver) - except ImportError: - LOG.error("Unable to load the virtualization driver.") + self.driver = utils.check_isinstance( + utils.import_object(compute_driver), + driver.ComputeDriver) + except ImportError as e: + LOG.error(_("Unable to load the virtualization driver: %s") % (e)) sys.exit(1) self.network_manager = utils.import_object(FLAGS.network_manager) @@ -840,7 +845,7 @@ class ComputeManager(manager.Manager): return self.driver.update_available_resource(context, self.host) - def pre_live_migration(self, context, instance_id): + def pre_live_migration(self, context, instance_id, time=None): """Preparations for live migration at dest host. :param context: security context @@ -848,6 +853,9 @@ class ComputeManager(manager.Manager): """ + if not time: + time = greenthread + # Getting instance info instance_ref = self.db.instance_get(context, instance_id) ec2_id = instance_ref['hostname'] @@ -1016,3 +1024,59 @@ class ComputeManager(manager.Manager): for volume in instance_ref['volumes']: self.db.volume_update(ctxt, volume['id'], {'status': 'in-use'}) + + def periodic_tasks(self, context=None): + """Tasks to be run at a periodic interval.""" + error_list = super(ComputeManager, self).periodic_tasks(context) + if error_list is None: + error_list = [] + + try: + self._poll_instance_states(context) + except Exception as ex: + LOG.warning(_("Error during instance poll: %s"), + unicode(ex)) + error_list.append(ex) + return error_list + + def _poll_instance_states(self, context): + vm_instances = self.driver.list_instances_detail() + vm_instances = dict((vm.name, vm) for vm in vm_instances) + + # Keep a list of VMs not in the DB, cross them off as we find them + vms_not_found_in_db = list(vm_instances.keys()) + + db_instances = self.db.instance_get_all_by_host(context, self.host) + + for db_instance in db_instances: + name = db_instance['name'] + vm_instance = vm_instances.get(name) + if vm_instance is None: + LOG.info(_("Found instance '%(name)s' in DB but no VM. " + "Setting state to shutoff.") % locals()) + vm_state = power_state.SHUTOFF + else: + vm_state = vm_instance.state + vms_not_found_in_db.remove(name) + + db_state = db_instance['state'] + if vm_state != db_state: + LOG.info(_("DB/VM state mismatch. Changing state from " + "'%(db_state)s' to '%(vm_state)s'") % locals()) + self.db.instance_set_state(context, + db_instance['id'], + vm_state) + + if vm_state == power_state.SHUTOFF: + # TODO(soren): This is what the compute manager does when you + # terminate an instance. At some point I figure we'll have a + # "terminated" state and some sort of cleanup job that runs + # occasionally, cleaning them out. + self.db.instance_destroy(context, db_instance['id']) + + # Are there VMs not in the DB? + for vm_not_found_in_db in vms_not_found_in_db: + name = vm_not_found_in_db + # TODO(justinsb): What to do here? Adopt it? Shut it down? + LOG.warning(_("Found VM not in DB: '%(name)s'. Ignoring") + % locals()) diff --git a/nova/compute/power_state.py b/nova/compute/power_state.py index adfc2dff0..ef013b2ef 100644 --- a/nova/compute/power_state.py +++ b/nova/compute/power_state.py @@ -2,6 +2,7 @@ # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Justin Santa Barbara # All Rights Reserved. # Copyright (c) 2010 Citrix Systems, Inc. # @@ -19,6 +20,7 @@ """The various power states that a VM can be in.""" +#NOTE(justinsb): These are the virDomainState values from libvirt NOSTATE = 0x00 RUNNING = 0x01 BLOCKED = 0x02 @@ -29,9 +31,10 @@ CRASHED = 0x06 SUSPENDED = 0x07 FAILED = 0x08 - -def name(code): - d = { +# TODO(justinsb): Power state really needs to be a proper class, +# so that we're not locked into the libvirt status codes and can put mapping +# logic here rather than spread throughout the code +_STATE_MAP = { NOSTATE: 'pending', RUNNING: 'running', BLOCKED: 'blocked', @@ -41,4 +44,11 @@ def name(code): CRASHED: 'crashed', SUSPENDED: 'suspended', FAILED: 'failed to spawn'} - return d[code] + + +def name(code): + return _STATE_MAP[code] + + +def valid_states(): + return _STATE_MAP.keys() diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index d931b6efb..56143114d 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -73,14 +73,18 @@ def fake_wsgi(self, req): return self.application -def wsgi_app(inner_application=None): - if not inner_application: - inner_application = openstack.APIRouter() +def wsgi_app(inner_app10=None, inner_app11=None): + if not inner_app10: + inner_app10 = openstack.APIRouterV10() + if not inner_app11: + inner_app11 = openstack.APIRouterV11() mapper = urlmap.URLMap() - api = openstack.FaultWrapper(auth.AuthMiddleware( - limits.RateLimitingMiddleware(inner_application))) - mapper['/v1.0'] = api - mapper['/v1.1'] = api + api10 = openstack.FaultWrapper(auth.AuthMiddleware( + limits.RateLimitingMiddleware(inner_app10))) + api11 = openstack.FaultWrapper(auth.AuthMiddleware( + limits.RateLimitingMiddleware(inner_app11))) + mapper['/v1.0'] = api10 + mapper['/v1.1'] = api11 mapper['/'] = openstack.FaultWrapper(openstack.Versions()) return mapper diff --git a/nova/tests/api/openstack/test_auth.py b/nova/tests/api/openstack/test_auth.py index 21596fb25..8f189c744 100644 --- a/nova/tests/api/openstack/test_auth.py +++ b/nova/tests/api/openstack/test_auth.py @@ -83,8 +83,7 @@ class Test(test.TestCase): self.assertEqual(result.headers['X-Storage-Url'], "") token = result.headers['X-Auth-Token'] - self.stubs.Set(nova.api.openstack, 'APIRouter', - fakes.FakeRouter) + self.stubs.Set(nova.api.openstack, 'APIRouterV10', fakes.FakeRouter) req = webob.Request.blank('/v1.0/fake') req.headers['X-Auth-Token'] = token result = req.get_response(fakes.wsgi_app()) @@ -201,8 +200,7 @@ class TestLimiter(test.TestCase): self.assertEqual(len(result.headers['X-Auth-Token']), 40) token = result.headers['X-Auth-Token'] - self.stubs.Set(nova.api.openstack, 'APIRouter', - fakes.FakeRouter) + self.stubs.Set(nova.api.openstack, 'APIRouterV10', fakes.FakeRouter) req = webob.Request.blank('/v1.0/fake') req.method = 'POST' req.headers['X-Auth-Token'] = token diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 3a804c649..9462bd40b 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -161,7 +161,7 @@ class ServersTest(test.TestCase): req = webob.Request.blank('/v1.0/servers/1') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) - self.assertEqual(res_dict['server']['id'], '1') + self.assertEqual(res_dict['server']['id'], 1) self.assertEqual(res_dict['server']['name'], 'server1') def test_get_server_by_id_with_addresses(self): @@ -172,7 +172,7 @@ class ServersTest(test.TestCase): req = webob.Request.blank('/v1.0/servers/1') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) - self.assertEqual(res_dict['server']['id'], '1') + self.assertEqual(res_dict['server']['id'], 1) self.assertEqual(res_dict['server']['name'], 'server1') addresses = res_dict['server']['addresses'] self.assertEqual(len(addresses["public"]), len(public)) @@ -180,7 +180,7 @@ class ServersTest(test.TestCase): self.assertEqual(len(addresses["private"]), 1) self.assertEqual(addresses["private"][0], private) - def test_get_server_by_id_with_addresses_v1_1(self): + def test_get_server_by_id_with_addresses_v11(self): private = "192.168.0.3" public = ["1.2.3.4"] new_return_server = return_server_with_addresses(private, public) @@ -189,7 +189,7 @@ class ServersTest(test.TestCase): req.environ['api.version'] = '1.1' res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) - self.assertEqual(res_dict['server']['id'], '1') + self.assertEqual(res_dict['server']['id'], 1) self.assertEqual(res_dict['server']['name'], 'server1') addresses = res_dict['server']['addresses'] self.assertEqual(len(addresses["public"]), len(public)) @@ -239,7 +239,7 @@ class ServersTest(test.TestCase): servers = json.loads(res.body)['servers'] self.assertEqual([s['id'] for s in servers], [1, 2]) - def _test_create_instance_helper(self): + def _setup_for_create_instance(self): """Shared implementation for tests below that create instance""" def instance_create(context, inst): return {'id': '1', 'display_name': 'server_test'} @@ -276,14 +276,17 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.api.openstack.common, "get_image_id_from_image_hash", image_id_from_hash) + def _test_create_instance_helper(self): + self._setup_for_create_instance() + body = dict(server=dict( - name='server_test', imageId=2, flavorId=2, + name='server_test', imageId=3, flavorId=2, metadata={'hello': 'world', 'open': 'stack'}, personality={})) req = webob.Request.blank('/v1.0/servers') req.method = 'POST' req.body = json.dumps(body) - req.headers["Content-Type"] = "application/json" + req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) @@ -291,8 +294,9 @@ class ServersTest(test.TestCase): self.assertEqual('serv', server['adminPass'][:4]) self.assertEqual(16, len(server['adminPass'])) self.assertEqual('server_test', server['name']) - self.assertEqual('1', server['id']) - + self.assertEqual(1, server['id']) + self.assertEqual(2, server['flavorId']) + self.assertEqual(3, server['imageId']) self.assertEqual(res.status_int, 200) def test_create_instance(self): @@ -302,6 +306,56 @@ class ServersTest(test.TestCase): fakes.stub_out_key_pair_funcs(self.stubs, have_key_pair=False) self._test_create_instance_helper() + def test_create_instance_v11(self): + self._setup_for_create_instance() + + imageRef = 'http://localhost/v1.1/images/2' + flavorRef = 'http://localhost/v1.1/flavors/3' + body = { + 'server': { + 'name': 'server_test', + 'imageRef': imageRef, + 'flavorRef': flavorRef, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': {}, + }, + } + + req = webob.Request.blank('/v1.1/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + + server = json.loads(res.body)['server'] + self.assertEqual('serv', server['adminPass'][:4]) + self.assertEqual(16, len(server['adminPass'])) + self.assertEqual('server_test', server['name']) + self.assertEqual(1, server['id']) + self.assertEqual(flavorRef, server['flavorRef']) + self.assertEqual(imageRef, server['imageRef']) + self.assertEqual(res.status_int, 200) + + def test_create_instance_v11_bad_href(self): + self._setup_for_create_instance() + + imageRef = 'http://localhost/v1.1/images/asdf' + flavorRef = 'http://localhost/v1.1/flavors/3' + body = dict(server=dict( + name='server_test', imageRef=imageRef, flavorRef=flavorRef, + metadata={'hello': 'world', 'open': 'stack'}, + personality={})) + req = webob.Request.blank('/v1.1/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + def test_update_no_body(self): req = webob.Request.blank('/v1.0/servers/1') req.method = 'PUT' @@ -945,7 +999,7 @@ class TestServerInstanceCreation(test.TestCase): def _setup_mock_compute_api_for_personality(self): - class MockComputeAPI(object): + class MockComputeAPI(nova.compute.API): def __init__(self): self.injected_files = None diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 44d04a12f..d1ef68de4 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -44,6 +44,14 @@ flags.DECLARE('stub_network', 'nova.compute.manager') flags.DECLARE('live_migration_retry_count', 'nova.compute.manager') +class FakeTime(object): + def __init__(self): + self.counter = 0 + + def sleep(self, t): + self.counter += t + + class ComputeTestCase(test.TestCase): """Test case for compute""" def setUp(self): @@ -393,7 +401,7 @@ class ComputeTestCase(test.TestCase): self.mox.ReplayAll() self.assertRaises(exception.NotFound, self.compute.pre_live_migration, - c, instance_ref['id']) + c, instance_ref['id'], time=FakeTime()) def test_pre_live_migration_instance_has_volume(self): """Confirm setup_compute_volume is called when volume is mounted.""" @@ -446,7 +454,7 @@ class ComputeTestCase(test.TestCase): self.compute.driver = drivermock self.mox.ReplayAll() - ret = self.compute.pre_live_migration(c, i_ref['id']) + ret = self.compute.pre_live_migration(c, i_ref['id'], time=FakeTime()) self.assertEqual(ret, None) def test_pre_live_migration_setup_compute_node_fail(self): @@ -479,7 +487,7 @@ class ComputeTestCase(test.TestCase): self.mox.ReplayAll() self.assertRaises(exception.ProcessExecutionError, self.compute.pre_live_migration, - c, i_ref['id']) + c, i_ref['id'], time=FakeTime()) def test_live_migration_works_correctly_with_volume(self): """Confirm check_for_export to confirm volume health check.""" @@ -626,3 +634,24 @@ class ComputeTestCase(test.TestCase): db.instance_destroy(c, instance_id) db.volume_destroy(c, v_ref['id']) db.floating_ip_destroy(c, flo_addr) + + def test_run_kill_vm(self): + """Detect when a vm is terminated behind the scenes""" + instance_id = self._create_instance() + + self.compute.run_instance(self.context, instance_id) + + instances = db.instance_get_all(context.get_admin_context()) + LOG.info(_("Running instances: %s"), instances) + self.assertEqual(len(instances), 1) + + instance_name = instances[0].name + self.compute.driver.test_remove_vm(instance_name) + + # Force the compute manager to do its periodic poll + error_list = self.compute.periodic_tasks(context.get_admin_context()) + self.assertFalse(error_list) + + instances = db.instance_get_all(context.get_admin_context()) + LOG.info(_("After force-killing instances: %s"), instances) + self.assertEqual(len(instances), 0) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index 6bafac39f..fb0ba53b1 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -427,6 +427,15 @@ class LibvirtConnTestCase(test.TestCase): def fake_raise(self): raise libvirt.libvirtError('ERR') + class FakeTime(object): + def __init__(self): + self.counter = 0 + + def sleep(self, t): + self.counter += t + + fake_timer = FakeTime() + self.create_fake_libvirt_mock(nwfilterLookupByName=fake_raise) instance_ref = db.instance_create(self.context, self.test_instance) @@ -436,11 +445,15 @@ class LibvirtConnTestCase(test.TestCase): conn = libvirt_conn.LibvirtConnection(False) conn.firewall_driver.setattr('setup_basic_filtering', fake_none) conn.firewall_driver.setattr('prepare_instance_filter', fake_none) - conn.ensure_filtering_rules_for_instance(instance_ref) + conn.ensure_filtering_rules_for_instance(instance_ref, + time=fake_timer) except exception.Error, e: c1 = (0 <= e.message.find('Timeout migrating for')) self.assertTrue(c1) + self.assertEqual(29, fake_timer.counter, "Didn't wait the expected " + "amount of time") + db.instance_destroy(self.context, instance_ref['id']) def test_live_migration_raises_exception(self): diff --git a/nova/utils.py b/nova/utils.py index 6042a5332..838e11b3c 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -658,3 +658,12 @@ def get_from_path(items, path): return results else: return get_from_path(results, remainder) + + +def check_isinstance(obj, cls): + """Checks that obj is of type cls, and lets PyLint infer types""" + if isinstance(obj, cls): + return obj + raise Exception(_("Expected object of type: %s") % (str(cls))) + # TODO(justinsb): Can we make this better?? + return cls() # Ugly PyLint hack diff --git a/nova/virt/connection.py b/nova/virt/connection.py index 13181b730..af7001715 100644 --- a/nova/virt/connection.py +++ b/nova/virt/connection.py @@ -23,6 +23,8 @@ import sys from nova import flags from nova import log as logging +from nova import utils +from nova.virt import driver from nova.virt import fake from nova.virt import libvirt_conn from nova.virt import xenapi_conn @@ -72,4 +74,4 @@ def get_connection(read_only=False): if conn is None: LOG.error(_('Failed to open connection to the hypervisor')) sys.exit(1) - return conn + return utils.check_isinstance(conn, driver.ComputeDriver) diff --git a/nova/virt/driver.py b/nova/virt/driver.py new file mode 100644 index 000000000..0e3a4aa3b --- /dev/null +++ b/nova/virt/driver.py @@ -0,0 +1,234 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# 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. + +""" +Driver base-classes: + + (Beginning of) the contract that compute drivers must follow, and shared + types that support that contract +""" + +from nova.compute import power_state + + +class InstanceInfo(object): + def __init__(self, name, state): + self.name = name + assert state in power_state.valid_states(), "Bad state: %s" % state + self.state = state + + +class ComputeDriver(object): + """Base class for compute drivers. + + Lots of documentation is currently on fake.py. + """ + + def init_host(self, host): + """Adopt existing VM's running here""" + raise NotImplementedError() + + def get_info(self, instance_name): + """Get the current status of an instance, by name (not ID!) + + Returns a dict containing: + :state: the running state, one of the power_state codes + :max_mem: (int) the maximum memory in KBytes allowed + :mem: (int) the memory in KBytes used by the domain + :num_cpu: (int) the number of virtual CPUs for the domain + :cpu_time: (int) the CPU time used in nanoseconds + """ + raise NotImplementedError() + + def list_instances(self): + raise NotImplementedError() + + def list_instances_detail(self): + """Return a list of InstanceInfo for all registered VMs""" + raise NotImplementedError() + + def spawn(self, instance): + """Launch a VM for the specified instance""" + raise NotImplementedError() + + def destroy(self, instance, cleanup=True): + """Shutdown specified VM""" + raise NotImplementedError() + + def reboot(self, instance): + """Reboot specified VM""" + raise NotImplementedError() + + def snapshot_instance(self, context, instance_id, image_id): + raise NotImplementedError() + + def get_console_pool_info(self, console_type): + """??? + + Returns a dict containing: + :address: ??? + :username: ??? + :password: ??? + """ + raise NotImplementedError() + + def get_console_output(self, instance): + raise NotImplementedError() + + def get_ajax_console(self, instance): + raise NotImplementedError() + + def get_diagnostics(self, instance): + """Return data about VM diagnostics""" + raise NotImplementedError() + + def get_host_ip_addr(self): + raise NotImplementedError() + + def attach_volume(self, context, instance_id, volume_id, mountpoint): + raise NotImplementedError() + + def detach_volume(self, context, instance_id, volume_id): + raise NotImplementedError() + + def compare_cpu(self, context, cpu_info): + raise NotImplementedError() + + def migrate_disk_and_power_off(self, instance, dest): + """Transfers the VHD of a running instance to another host, then shuts + off the instance copies over the COW disk""" + raise NotImplementedError() + + def snapshot(self, instance, image_id): + """ Create snapshot from a running VM instance """ + raise NotImplementedError() + + def finish_resize(self, instance, disk_info): + """Completes a resize, turning on the migrated instance""" + raise NotImplementedError() + + def revert_resize(self, instance): + """Reverts a resize, powering back on the instance""" + raise NotImplementedError() + + def pause(self, instance, callback): + """Pause VM instance""" + raise NotImplementedError() + + def unpause(self, instance, callback): + """Unpause paused VM instance""" + raise NotImplementedError() + + def suspend(self, instance, callback): + """suspend the specified instance""" + raise NotImplementedError() + + def resume(self, instance, callback): + """resume the specified instance""" + raise NotImplementedError() + + def rescue(self, instance, callback): + """Rescue the specified instance""" + raise NotImplementedError() + + def unrescue(self, instance, callback): + """Unrescue the specified instance""" + raise NotImplementedError() + + def update_available_resource(self, ctxt, host): + """Updates compute manager resource info on ComputeNode table. + + This method is called when nova-compute launches, and + whenever admin executes "nova-manage service update_resource". + + :param ctxt: security context + :param host: hostname that compute manager is currently running + + """ + raise NotImplementedError() + + def live_migration(self, ctxt, instance_ref, dest, + post_method, recover_method): + """Spawning live_migration operation for distributing high-load. + + :params ctxt: security context + :params instance_ref: + nova.db.sqlalchemy.models.Instance object + instance object that is migrated. + :params dest: destination host + :params post_method: + post operation method. + expected nova.compute.manager.post_live_migration. + :params recover_method: + recovery method when any exception occurs. + expected nova.compute.manager.recover_live_migration. + + """ + raise NotImplementedError() + + def refresh_security_group_rules(self, security_group_id): + raise NotImplementedError() + + def refresh_security_group_members(self, security_group_id): + raise NotImplementedError() + + def reset_network(self, instance): + """reset networking for specified instance""" + raise NotImplementedError() + + def ensure_filtering_rules_for_instance(self, instance_ref): + """Setting up filtering rules and waiting for its completion. + + To migrate an instance, filtering rules to hypervisors + and firewalls are inevitable on destination host. + ( Waiting only for filtering rules to hypervisor, + since filtering rules to firewall rules can be set faster). + + Concretely, the below method must be called. + - setup_basic_filtering (for nova-basic, etc.) + - prepare_instance_filter(for nova-instance-instance-xxx, etc.) + + to_xml may have to be called since it defines PROJNET, PROJMASK. + but libvirt migrates those value through migrateToURI(), + so , no need to be called. + + Don't use thread for this method since migration should + not be started when setting-up filtering rules operations + are not completed. + + :params instance_ref: nova.db.sqlalchemy.models.Instance object + + """ + raise NotImplementedError() + + def unfilter_instance(self, instance): + """Stop filtering instance""" + raise NotImplementedError() + + def set_admin_password(self, context, instance_id, new_pass=None): + """Set the root/admin password for an instance on this server.""" + raise NotImplementedError() + + def inject_file(self, instance, b64_path, b64_contents): + """Create a file on the VM instance. The file path and contents + should be base64-encoded. + """ + raise NotImplementedError() + + def inject_network_info(self, instance): + """inject network info for specified instance""" + raise NotImplementedError() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 3a06284a1..5b0fe1877 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -26,7 +26,9 @@ semantics of real hypervisor connections. """ from nova import exception +from nova import utils from nova.compute import power_state +from nova.virt import driver def get_connection(_): @@ -34,7 +36,14 @@ def get_connection(_): return FakeConnection.instance() -class FakeConnection(object): +class FakeInstance(object): + + def __init__(self, name, state): + self.name = name + self.state = state + + +class FakeConnection(driver.ComputeDriver): """ The interface to this class talks in terms of 'instances' (Amazon EC2 and internal Nova terminology), by which we mean 'running virtual machine' @@ -90,6 +99,17 @@ class FakeConnection(object): """ return self.instances.keys() + def _map_to_instance_info(self, instance): + instance = utils.check_isinstance(instance, FakeInstance) + info = driver.InstanceInfo(instance.name, instance.state) + return info + + def list_instances_detail(self): + info_list = [] + for instance in self.instances.values(): + info_list.append(self._map_to_instance_info(instance)) + return info_list + def spawn(self, instance): """ Create a new instance/VM/domain on the virtualization platform. @@ -109,9 +129,10 @@ class FakeConnection(object): that it was before this call began. """ - fake_instance = FakeInstance() - self.instances[instance.name] = fake_instance - fake_instance._state = power_state.RUNNING + name = instance.name + state = power_state.RUNNING + fake_instance = FakeInstance(name, state) + self.instances[name] = fake_instance def snapshot(self, instance, name): """ @@ -270,7 +291,7 @@ class FakeConnection(object): raise exception.NotFound(_("Instance %s Not Found") % instance_name) i = self.instances[instance_name] - return {'state': i._state, + return {'state': i.state, 'max_mem': 0, 'mem': 0, 'num_cpu': 2, @@ -428,8 +449,6 @@ class FakeConnection(object): """This method is supported only by libvirt.""" raise NotImplementedError('This method is supported only by libvirt.') - -class FakeInstance(object): - - def __init__(self): - self._state = power_state.NOSTATE + def test_remove_vm(self, instance_name): + """ Removes the named VM, as if it crashed. For testing""" + self.instances.pop(instance_name) diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 75fed6d4f..a1ed5ebbf 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -68,6 +68,7 @@ from nova import flags from nova import log as logging from nova.auth import manager from nova.compute import power_state +from nova.virt import driver from nova.virt import images wmi = None @@ -108,8 +109,9 @@ def get_connection(_): return HyperVConnection() -class HyperVConnection(object): +class HyperVConnection(driver.ComputeDriver): def __init__(self): + super(HyperVConnection, self).__init__() self._conn = wmi.WMI(moniker='//./root/virtualization') self._cim_conn = wmi.WMI(moniker='//./root/cimv2') @@ -124,6 +126,19 @@ class HyperVConnection(object): for v in self._conn.Msvm_ComputerSystem(['ElementName'])] return vms + def list_instances_detail(self): + # TODO(justinsb): This is a terrible implementation (1+N) + instance_infos = [] + for instance_name in self.list_instances(): + info = self.get_info(instance_name) + + state = info['state'] + + instance_info = driver.InstanceInfo(instance_name, state) + instance_infos.append(instance_info) + + return instance_infos + def spawn(self, instance): """ Create a new VM and start it.""" vm = self._lookup(instance.name) @@ -345,7 +360,7 @@ class HyperVConnection(object): newinst = cl.new() #Copy the properties from the original. for prop in wmi_obj._properties: - newinst.Properties_.Item(prop).Value =\ + newinst.Properties_.Item(prop).Value = \ wmi_obj.Properties_.Item(prop).Value return newinst diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 67094320e..e1a0a6f29 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -42,7 +42,6 @@ import shutil import sys import random import subprocess -import time import uuid from xml.dom import minidom @@ -62,6 +61,7 @@ from nova.auth import manager from nova.compute import instance_types from nova.compute import power_state from nova.virt import disk +from nova.virt import driver from nova.virt import images libvirt = None @@ -133,8 +133,8 @@ def get_connection(read_only): def _late_load_cheetah(): global Template if Template is None: - t = __import__('Cheetah.Template', globals(), locals(), ['Template'], - -1) + t = __import__('Cheetah.Template', globals(), locals(), + ['Template'], -1) Template = t.Template @@ -153,9 +153,10 @@ def _get_ip_version(cidr): return int(net.version()) -class LibvirtConnection(object): +class LibvirtConnection(driver.ComputeDriver): def __init__(self, read_only): + super(LibvirtConnection, self).__init__() self.libvirt_uri = self.get_uri() self.libvirt_xml = open(FLAGS.libvirt_xml_template).read() @@ -235,6 +236,29 @@ class LibvirtConnection(object): return [self._conn.lookupByID(x).name() for x in self._conn.listDomainsID()] + def _map_to_instance_info(self, domain): + """Gets info from a virsh domain object into an InstanceInfo""" + + # domain.info() returns a list of: + # state: one of the state values (virDomainState) + # maxMemory: the maximum memory used by the domain + # memory: the current amount of memory used by the domain + # nbVirtCPU: the number of virtual CPU + # puTime: the time used by the domain in nanoseconds + + (state, _max_mem, _mem, _num_cpu, _cpu_time) = domain.info() + name = domain.name() + + return driver.InstanceInfo(name, state) + + def list_instances_detail(self): + infos = [] + for domain_id in self._conn.listDomainsID(): + domain = self._conn.lookupByID(domain_id) + info = self._map_to_instance_info(domain) + infos.append(info) + return infos + def destroy(self, instance, cleanup=True): try: virt_dom = self._conn.lookupByName(instance['name']) @@ -1147,7 +1171,8 @@ class LibvirtConnection(object): return - def ensure_filtering_rules_for_instance(self, instance_ref): + def ensure_filtering_rules_for_instance(self, instance_ref, + time=None): """Setting up filtering rules and waiting for its completion. To migrate an instance, filtering rules to hypervisors @@ -1171,6 +1196,9 @@ class LibvirtConnection(object): """ + if not time: + time = greenthread + # If any instances never launch at destination host, # basic-filtering must be set here. self.firewall_driver.setup_basic_filtering(instance_ref) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 6c1f6424a..83d5e8310 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -36,6 +36,7 @@ from nova import utils from nova.auth.manager import AuthManager from nova.compute import power_state +from nova.virt import driver from nova.virt.xenapi.network_utils import NetworkHelper from nova.virt.xenapi.vm_utils import VMHelper from nova.virt.xenapi.vm_utils import ImageType @@ -57,6 +58,8 @@ class VMOps(object): def list_instances(self): """List VM instances""" + # TODO(justinsb): Should we just always use the details method? + # Seems to be the same number of API calls.. vm_refs = [] for vm_ref in self._session.get_xenapi().VM.get_all(): vm_rec = self._session.get_xenapi().VM.get_record(vm_ref) @@ -64,6 +67,22 @@ class VMOps(object): vm_refs.append(vm_rec["name_label"]) return vm_refs + def list_instances_detail(self): + """List VM instances, returning InstanceInfo objects""" + instance_infos = [] + for vm_ref in self._session.get_xenapi().VM.get_all(): + vm_rec = self._session.get_xenapi().VM.get_record(vm_ref) + if not vm_rec["is_a_template"] and not vm_rec["is_control_domain"]: + name = vm_rec["name_label"] + + # TODO(justinsb): This a roundabout way to map the state + openstack_format = VMHelper.compile_info(vm_rec) + state = openstack_format['state'] + + instance_info = driver.InstanceInfo(name, state) + instance_infos.append(instance_info) + return instance_infos + def revert_resize(self, instance): vm_ref = VMHelper.lookup(self._session, instance.name) self._start(instance, vm_ref) @@ -993,7 +1012,7 @@ class VMOps(object): """ vm_ref = self._get_vm_opaque_ref(instance_or_vm) data = self._session.call_xenapi_request('VM.get_xenstore_data', - (vm_ref, )) + (vm_ref,)) ret = {} if keys is None: keys = data.keys() diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 2884687fb..f20fb29d8 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -69,6 +69,7 @@ from nova import db from nova import utils from nova import flags from nova import log as logging +from nova.virt import driver from nova.virt.xenapi.vmops import VMOps from nova.virt.xenapi.volumeops import VolumeOps @@ -141,10 +142,11 @@ def get_connection(_): return XenAPIConnection(url, username, password) -class XenAPIConnection(object): +class XenAPIConnection(driver.ComputeDriver): """A connection to XenServer or Xen Cloud Platform""" def __init__(self, url, user, pw): + super(XenAPIConnection, self).__init__() session = XenAPISession(url, user, pw) self._vmops = VMOps(session) self._volumeops = VolumeOps(session) @@ -160,6 +162,9 @@ class XenAPIConnection(object): """List VM instances""" return self._vmops.list_instances() + def list_instances_detail(self): + return self._vmops.list_instances_detail() + def spawn(self, instance): """Create VM instance""" self._vmops.spawn(instance) |