diff options
29 files changed, 317 insertions, 148 deletions
diff --git a/etc/nova/policy.json b/etc/nova/policy.json index a9a584237..056a36b3b 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -34,6 +34,7 @@ "compute_extension:baremetal_nodes": "rule:admin_api", "compute_extension:cells": "rule:admin_api", "compute_extension:certificates": "", + "compute_extension:v3:os-certificates": "", "compute_extension:cloudpipe": "rule:admin_api", "compute_extension:cloudpipe_update": "rule:admin_api", "compute_extension:console_output": "", diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index bec919f4b..dd746e23d 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -358,9 +358,12 @@ def raise_http_conflict_for_instance_invalid_state(exc, action): """ attr = exc.kwargs.get('attr') state = exc.kwargs.get('state') + not_launched = exc.kwargs.get('not_launched') if attr and state: msg = _("Cannot '%(action)s' while instance is in %(attr)s " "%(state)s") % {'action': action, 'attr': attr, 'state': state} + elif not_launched: + msg = _("Cannot '%s' an instance which has never been active") % action else: # At least give some meaningful message msg = _("Instance is in an invalid state for '%s'") % action diff --git a/nova/api/openstack/compute/contrib/certificates.py b/nova/api/openstack/compute/contrib/certificates.py index 64a6e26fe..4fe49aadf 100644 --- a/nova/api/openstack/compute/contrib/certificates.py +++ b/nova/api/openstack/compute/contrib/certificates.py @@ -38,15 +38,6 @@ class CertificateTemplate(xmlutil.TemplateBuilder): return xmlutil.MasterTemplate(root, 1) -class CertificatesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('certificates') - elem = xmlutil.SubTemplateElement(root, 'certificate', - selector='certificates') - make_certificate(elem) - return xmlutil.MasterTemplate(root, 1) - - def _translate_certificate_view(certificate, private_key=None): return { 'data': certificate, @@ -64,7 +55,7 @@ class CertificatesController(object): @wsgi.serializers(xml=CertificateTemplate) def show(self, req, id): - """Return a list of certificates.""" + """Return certificate information.""" context = req.environ['nova.context'] authorize(context) if id != 'root': @@ -76,7 +67,7 @@ class CertificatesController(object): @wsgi.serializers(xml=CertificateTemplate) def create(self, req, body=None): - """Return a list of certificates.""" + """Create a certificate.""" context = req.environ['nova.context'] authorize(context) pk, cert = self.cert_rpcapi.generate_x509_cert(context, diff --git a/nova/api/openstack/compute/plugins/v3/certificates.py b/nova/api/openstack/compute/plugins/v3/certificates.py index 64a6e26fe..175780f9c 100644 --- a/nova/api/openstack/compute/plugins/v3/certificates.py +++ b/nova/api/openstack/compute/plugins/v3/certificates.py @@ -22,7 +22,8 @@ from nova.api.openstack import xmlutil import nova.cert.rpcapi from nova import network -authorize = extensions.extension_authorizer('compute', 'certificates') +ALIAS = "os-certificates" +authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS) def make_certificate(elem): @@ -38,15 +39,6 @@ class CertificateTemplate(xmlutil.TemplateBuilder): return xmlutil.MasterTemplate(root, 1) -class CertificatesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('certificates') - elem = xmlutil.SubTemplateElement(root, 'certificate', - selector='certificates') - make_certificate(elem) - return xmlutil.MasterTemplate(root, 1) - - def _translate_certificate_view(certificate, private_key=None): return { 'data': certificate, @@ -64,7 +56,7 @@ class CertificatesController(object): @wsgi.serializers(xml=CertificateTemplate) def show(self, req, id): - """Return a list of certificates.""" + """Return certificate information.""" context = req.environ['nova.context'] authorize(context) if id != 'root': @@ -76,7 +68,7 @@ class CertificatesController(object): @wsgi.serializers(xml=CertificateTemplate) def create(self, req, body=None): - """Return a list of certificates.""" + """Create a certificate.""" context = req.environ['nova.context'] authorize(context) pk, cert = self.cert_rpcapi.generate_x509_cert(context, @@ -85,21 +77,21 @@ class CertificatesController(object): return {'certificate': _translate_certificate_view(cert, pk)} -class Certificates(extensions.ExtensionDescriptor): +class Certificates(extensions.V3APIExtensionBase): """Certificates support.""" name = "Certificates" - alias = "os-certificates" + alias = ALIAS namespace = ("http://docs.openstack.org/compute/ext/" - "certificates/api/v1.1") - updated = "2012-01-19T00:00:00+00:00" + "certificates/api/v3") + version = 1 def get_resources(self): - resources = [] - - res = extensions.ResourceExtension('os-certificates', - CertificatesController(), - member_actions={}) - resources.append(res) - + resources = [ + extensions.ResourceExtension('os-certificates', + CertificatesController(), + member_actions={})] return resources + + def get_controller_extensions(self): + return [] diff --git a/nova/cells/scheduler.py b/nova/cells/scheduler.py index c54b9b578..aa081a726 100644 --- a/nova/cells/scheduler.py +++ b/nova/cells/scheduler.py @@ -214,7 +214,8 @@ class CellsScheduler(base.Base): instance_uuids = [inst['uuid'] for inst in build_inst_kwargs['instances']] instances = build_inst_kwargs['instances'] - request_spec = scheduler_utils.build_request_spec(image, instances) + request_spec = scheduler_utils.build_request_spec(message.ctxt, + image, instances) filter_properties = copy.copy(build_inst_kwargs['filter_properties']) filter_properties.update({'context': message.ctxt, 'scheduler': self, diff --git a/nova/compute/api.py b/nova/compute/api.py index 1e24e8ce5..0a9b0e67b 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -110,10 +110,12 @@ RO_SECURITY_GROUPS = ['default'] SM_IMAGE_PROP_PREFIX = "image_" -def check_instance_state(vm_state=None, task_state=(None,)): +def check_instance_state(vm_state=None, task_state=(None,), + must_have_launched=True): """Decorator to check VM and/or task state before entry to API functions. - If the instance is in the wrong state, the wrapper will raise an exception. + If the instance is in the wrong state, or has not been sucessfully started + at least once the wrapper will raise an exception. """ if vm_state is not None and not isinstance(vm_state, set): @@ -137,6 +139,13 @@ def check_instance_state(vm_state=None, task_state=(None,)): instance_uuid=instance['uuid'], state=instance['task_state'], method=f.__name__) + if must_have_launched and not instance['launched_at']: + raise exception.InstanceInvalidState( + attr=None, + not_launched=True, + instance_uuid=instance['uuid'], + state=instance['vm_state'], + method=f.__name__) return f(self, context, instance, *args, **kw) return inner @@ -1305,7 +1314,8 @@ class API(base.Base): # NOTE(maoy): we allow delete to be called no matter what vm_state says. @wrap_check_policy @check_instance_lock - @check_instance_state(vm_state=None, task_state=None) + @check_instance_state(vm_state=None, task_state=None, + must_have_launched=True) def soft_delete(self, context, instance): """Terminate an instance.""" LOG.debug(_('Going to try to soft delete instance'), @@ -1329,7 +1339,8 @@ class API(base.Base): @wrap_check_policy @check_instance_lock - @check_instance_state(vm_state=None, task_state=None) + @check_instance_state(vm_state=None, task_state=None, + must_have_launched=False) def delete(self, context, instance): """Terminate an instance.""" LOG.debug(_("Going to try to terminate instance"), instance=instance) @@ -1369,7 +1380,8 @@ class API(base.Base): @wrap_check_policy @check_instance_lock - @check_instance_state(vm_state=[vm_states.SOFT_DELETED]) + @check_instance_state(vm_state=[vm_states.SOFT_DELETED], + must_have_launched=False) def force_delete(self, context, instance): """Force delete a previously deleted (but not reclaimed) instance.""" self._delete_instance(context, instance) @@ -1790,7 +1802,8 @@ class API(base.Base): @wrap_check_policy @check_instance_lock @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED, - vm_states.PAUSED, vm_states.SUSPENDED], + vm_states.PAUSED, vm_states.SUSPENDED, + vm_states.ERROR], task_state=[None, task_states.REBOOTING, task_states.REBOOTING_HARD, task_states.RESUMING, @@ -1826,7 +1839,8 @@ class API(base.Base): @wrap_check_policy @check_instance_lock - @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED], + @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED, + vm_states.ERROR], task_state=[None]) def rebuild(self, context, instance, image_href, admin_password, **kwargs): """Rebuild the given instance with the provided attributes.""" @@ -2224,7 +2238,8 @@ class API(base.Base): @wrap_check_policy @check_instance_lock - @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED]) + @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED, + vm_states.ERROR]) def rescue(self, context, instance, rescue_password=None): """Rescue the given instance.""" diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 0a6a9d08d..237831cd1 100755 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -3707,8 +3707,8 @@ class ComputeManager(manager.SchedulerDependentManager): {'migration_id': migration['id'], 'instance_uuid': instance_uuid}) try: - instance = self.conductor_api.instance_get_by_uuid( - context, instance_uuid) + instance = instance_obj.Instance.get_by_uuid(context, + instance_uuid) except exception.InstanceNotFound: reason = (_("Instance %s not found") % instance_uuid) diff --git a/nova/conductor/manager.py b/nova/conductor/manager.py index 3b8c5f55d..cc1b05cc4 100644 --- a/nova/conductor/manager.py +++ b/nova/conductor/manager.py @@ -66,7 +66,7 @@ class ConductorManager(manager.Manager): namespace. See the ComputeTaskManager class for details. """ - RPC_API_VERSION = '1.51' + RPC_API_VERSION = '1.52' def __init__(self, *args, **kwargs): super(ConductorManager, self).__init__(service_name='conductor', @@ -475,6 +475,9 @@ class ConductorManager(manager.Manager): self.compute_api.stop(context, instance, do_cast) def compute_confirm_resize(self, context, instance, migration_ref): + if isinstance(instance, nova_object.NovaObject): + # NOTE(danms): Remove this at RPC API v2.0 + instance = dict(instance.items()) self.compute_api.confirm_resize(context, instance, migration_ref) def compute_unrescue(self, context, instance): @@ -541,7 +544,8 @@ class ComputeTaskManager(object): def build_instances(self, context, instances, image, filter_properties, admin_password, injected_files, requested_networks, security_groups, block_device_mapping): - request_spec = scheduler_utils.build_request_spec(image, instances) + request_spec = scheduler_utils.build_request_spec(context, image, + instances) # NOTE(alaski): For compatibility until a new scheduler method is used. request_spec.update({'block_device_mapping': block_device_mapping, 'security_group': security_groups}) diff --git a/nova/conductor/rpcapi.py b/nova/conductor/rpcapi.py index bb66ca8b2..fcbb87f0e 100644 --- a/nova/conductor/rpcapi.py +++ b/nova/conductor/rpcapi.py @@ -102,6 +102,7 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): 1.50 - Added object_action() and object_class_action() 1.51 - Added the 'legacy' argument to block_device_mapping_get_all_by_instance + 1.52 - Pass instance objects for compute_confirm_resize """ BASE_RPC_API_VERSION = '1.0' @@ -459,11 +460,10 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): return self.call(context, msg, version='1.43') def compute_confirm_resize(self, context, instance, migration_ref): - instance_p = jsonutils.to_primitive(instance) migration_p = jsonutils.to_primitive(migration_ref) - msg = self.make_msg('compute_confirm_resize', instance=instance_p, + msg = self.make_msg('compute_confirm_resize', instance=instance, migration_ref=migration_p) - return self.call(context, msg, version='1.46') + return self.call(context, msg, version='1.52') def compute_unrescue(self, context, instance): instance_p = jsonutils.to_primitive(instance) diff --git a/nova/objects/__init__.py b/nova/objects/__init__.py index e39f0154c..00f8240f1 100644 --- a/nova/objects/__init__.py +++ b/nova/objects/__init__.py @@ -18,3 +18,4 @@ def register_all(): # function in order for it to be registered by services that may # need to receive it via RPC. __import__('nova.objects.instance') + __import__('nova.objects.instance_info_cache') diff --git a/nova/scheduler/utils.py b/nova/scheduler/utils.py index 028c72ff7..b707f424c 100644 --- a/nova/scheduler/utils.py +++ b/nova/scheduler/utils.py @@ -15,19 +15,26 @@ """Utility methods for scheduling.""" from nova.compute import flavors +from nova import db from nova.openstack.common import jsonutils -def build_request_spec(image, instances): +def build_request_spec(ctxt, image, instances): """Build a request_spec for the scheduler. The request_spec assumes that all instances to be scheduled are the same type. """ instance = instances[0] + instance_type = flavors.extract_flavor(instance) + # NOTE(comstud): This is a bit ugly, but will get cleaned up when + # we're passing an InstanceType internal object. + extra_specs = db.instance_type_extra_specs_get(ctxt, + instance_type['flavorid']) + instance_type['extra_specs'] = extra_specs request_spec = { 'image': image, 'instance_properties': instance, - 'instance_type': flavors.extract_flavor(instance), + 'instance_type': instance_type, 'instance_uuids': [inst['uuid'] for inst in instances]} return jsonutils.to_primitive(request_spec) diff --git a/nova/tests/api/ec2/test_cloud.py b/nova/tests/api/ec2/test_cloud.py index 543bf4a62..22a6947f2 100644 --- a/nova/tests/api/ec2/test_cloud.py +++ b/nova/tests/api/ec2/test_cloud.py @@ -46,6 +46,7 @@ from nova.network import api as network_api from nova.network import quantumv2 from nova.openstack.common import log as logging from nova.openstack.common import rpc +from nova.openstack.common import timeutils from nova import test from nova.tests.api.openstack.compute.contrib import ( test_quantum_security_groups as test_quantum) @@ -880,6 +881,7 @@ class CloudTestCase(test.TestCase): 'instance_type_id': 1, 'host': 'host1', 'vm_state': 'active', + 'launched_at': timeutils.utcnow(), 'hostname': 'server-1111', 'created_at': datetime.datetime(2012, 5, 1, 1, 1, 1), 'system_metadata': sys_meta @@ -891,6 +893,7 @@ class CloudTestCase(test.TestCase): 'instance_type_id': 1, 'host': 'host2', 'vm_state': 'active', + 'launched_at': timeutils.utcnow(), 'hostname': 'server-1112', 'created_at': datetime.datetime(2012, 5, 1, 1, 1, 2), 'system_metadata': sys_meta @@ -2442,6 +2445,7 @@ class CloudTestCase(test.TestCase): 'image_ref': image_uuid, 'instance_type_id': 1, 'vm_state': 'active', + 'launched_at': timeutils.utcnow(), 'hostname': 'server-1111', 'created_at': datetime.datetime(2012, 5, 1, 1, 1, 1) } @@ -2492,6 +2496,7 @@ class CloudTestCase(test.TestCase): 'image_ref': image_uuid, 'instance_type_id': 1, 'vm_state': 'active', + 'launched_at': timeutils.utcnow(), 'hostname': 'server-1111', 'created_at': datetime.datetime(2012, 5, 1, 1, 1, 1) } @@ -2501,6 +2506,7 @@ class CloudTestCase(test.TestCase): 'image_ref': image_uuid, 'instance_type_id': 1, 'vm_state': 'active', + 'launched_at': timeutils.utcnow(), 'hostname': 'server-1112', 'created_at': datetime.datetime(2012, 5, 1, 1, 1, 2) } 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 5e64bdf94..39eebbcd1 100644 --- a/nova/tests/api/openstack/compute/contrib/test_admin_actions.py +++ b/nova/tests/api/openstack/compute/contrib/test_admin_actions.py @@ -26,6 +26,7 @@ from nova.conductor import api as conductor_api from nova import context from nova import exception from nova.openstack.common import jsonutils +from nova.openstack.common import timeutils from nova import test from nova.tests.api.openstack import fakes @@ -41,6 +42,7 @@ INSTANCE = { "tenant_id": 'fake_tenant_id', "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "launched_at": datetime.datetime(2010, 11, 11, 11, 0, 0), "security_groups": [{"id": 1, "name": "test"}], "progress": 0, "image_ref": 'http://foo.com/123', @@ -61,7 +63,7 @@ def fake_compute_api_raises_invalid_state(*args, **kwargs): def fake_compute_api_get(self, context, instance_id): return {'id': 1, 'uuid': instance_id, 'vm_state': vm_states.ACTIVE, - 'task_state': None} + 'task_state': None, 'launched_at': timeutils.utcnow()} class AdminActionsTest(test.TestCase): diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_certificates.py b/nova/tests/api/openstack/compute/plugins/v3/test_certificates.py index df5a7e9a1..222087872 100644 --- a/nova/tests/api/openstack/compute/plugins/v3/test_certificates.py +++ b/nova/tests/api/openstack/compute/plugins/v3/test_certificates.py @@ -15,7 +15,7 @@ from lxml import etree -from nova.api.openstack.compute.contrib import certificates +from nova.api.openstack.compute.plugins.v3 import certificates from nova import context from nova.openstack.common import rpc from nova import test @@ -44,7 +44,7 @@ class CertificatesTest(test.TestCase): def test_certificates_show_root(self): self.stubs.Set(rpc, 'call', fake_get_root_cert) - req = fakes.HTTPRequest.blank('/v2/fake/os-certificates/root') + req = fakes.HTTPRequestV3.blank('/os-certificates/root') res_dict = self.controller.show(req, 'root') cert = fake_get_root_cert(self.context) @@ -53,7 +53,7 @@ class CertificatesTest(test.TestCase): def test_certificates_create_certificate(self): self.stubs.Set(rpc, 'call', fake_create_cert) - req = fakes.HTTPRequest.blank('/v2/fake/os-certificates/') + req = fakes.HTTPRequestV3.blank('/os-certificates/') res_dict = self.controller.create(req) pk, cert = fake_create_cert(self.context) diff --git a/nova/tests/api/openstack/compute/test_server_metadata.py b/nova/tests/api/openstack/compute/test_server_metadata.py index fa25ad4a3..f0548ffa0 100644 --- a/nova/tests/api/openstack/compute/test_server_metadata.py +++ b/nova/tests/api/openstack/compute/test_server_metadata.py @@ -26,6 +26,7 @@ from nova.compute import vm_states import nova.db from nova import exception from nova.openstack.common import jsonutils +from nova.openstack.common import timeutils from nova import test from nova.tests.api.openstack import fakes @@ -77,6 +78,7 @@ def return_server(context, server_id): 'uuid': '0cc3346e-9fef-4445-abe6-5d2b2690ec64', 'name': 'fake', 'locked': False, + 'launched_at': timeutils.utcnow(), 'vm_state': vm_states.ACTIVE} @@ -85,6 +87,7 @@ def return_server_by_uuid(context, server_uuid): 'uuid': '0cc3346e-9fef-4445-abe6-5d2b2690ec64', 'name': 'fake', 'locked': False, + 'launched_at': timeutils.utcnow(), 'vm_state': vm_states.ACTIVE} diff --git a/nova/tests/cells/test_cells_scheduler.py b/nova/tests/cells/test_cells_scheduler.py index 9cd637cdf..600867f43 100644 --- a/nova/tests/cells/test_cells_scheduler.py +++ b/nova/tests/cells/test_cells_scheduler.py @@ -178,7 +178,7 @@ class CellsSchedulerTestCase(test.TestCase): call_info['target_cell'] = target_cell call_info['build_inst_kwargs'] = build_inst_kwargs - def fake_build_request_spec(image, instances): + def fake_build_request_spec(ctxt, image, instances): request_spec = { 'instance_uuids': [inst['uuid'] for inst in instances], 'image': image} @@ -264,7 +264,7 @@ class CellsSchedulerTestCase(test.TestCase): def fake_rpc_build_instances(ctxt, **build_inst_kwargs): call_info['build_inst_kwargs'] = build_inst_kwargs - def fake_build_request_spec(image, instances): + def fake_build_request_spec(ctxt, image, instances): request_spec = { 'instance_uuids': [inst['uuid'] for inst in instances], 'image': image} @@ -341,7 +341,7 @@ class CellsSchedulerTestCase(test.TestCase): self.assertEqual(vm_states.ERROR, values['vm_state']) call_info['errored_uuids'].append(instance_uuid) - def fake_build_request_spec(image, instances): + def fake_build_request_spec(ctxt, image, instances): request_spec = { 'instance_uuids': [inst['uuid'] for inst in instances], 'image': image} @@ -385,7 +385,7 @@ class CellsSchedulerTestCase(test.TestCase): self.assertEqual(vm_states.ERROR, instance['vm_state']) call_info['errored_uuids2'].append(instance['uuid']) - def fake_build_request_spec(image, instances): + def fake_build_request_spec(ctxt, image, instances): request_spec = { 'instance_uuids': [inst['uuid'] for inst in instances], 'image': image} diff --git a/nova/tests/compute/test_compute.py b/nova/tests/compute/test_compute.py index dc828cef8..c887041f8 100644 --- a/nova/tests/compute/test_compute.py +++ b/nova/tests/compute/test_compute.py @@ -259,6 +259,9 @@ class BaseTestCase(test.TestCase): inst['os_type'] = 'Linux' inst['system_metadata'] = make_fake_sys_meta() inst['locked'] = False + inst['created_at'] = timeutils.utcnow() + inst['updated_at'] = timeutils.utcnow() + inst['launched_at'] = timeutils.utcnow() inst.update(params) _create_service_entries(self.context.elevated(), {'fake_zone': [inst['host']]}) @@ -1248,6 +1251,7 @@ class ComputeTestCase(BaseTestCase): def test_run_terminate_timestamps(self): # Make sure timestamps are set for launched and destroyed. instance = jsonutils.to_primitive(self._create_fake_instance()) + instance['launched_at'] = None self.assertEqual(instance['launched_at'], None) self.assertEqual(instance['deleted_at'], None) launch = timeutils.utcnow() @@ -4441,19 +4445,27 @@ class ComputeTestCase(BaseTestCase): self.assertTrue(instance) def test_poll_unconfirmed_resizes(self): - instances = [{'uuid': 'fake_uuid1', 'vm_state': vm_states.RESIZED, - 'task_state': None}, - {'uuid': 'noexist'}, - {'uuid': 'fake_uuid2', 'vm_state': vm_states.ERROR, - 'task_state': None}, - {'uuid': 'fake_uuid3', 'vm_state': vm_states.ACTIVE, - 'task_state': task_states.REBOOTING}, - {'uuid': 'fake_uuid4', 'vm_state': vm_states.RESIZED, - 'task_state': None}, - {'uuid': 'fake_uuid5', 'vm_state': vm_states.ACTIVE, - 'task_state': None}, - {'uuid': 'fake_uuid6', 'vm_state': vm_states.RESIZED, - 'task_state': 'deleting'}] + instances = [ + fake_instance.fake_db_instance(uuid='fake_uuid1', + vm_state=vm_states.RESIZED, + task_state=None), + fake_instance.fake_db_instance(uuid='noexist'), + fake_instance.fake_db_instance(uuid='fake_uuid2', + vm_state=vm_states.ERROR, + task_state=None), + fake_instance.fake_db_instance(uuid='fake_uuid3', + vm_state=vm_states.ACTIVE, + task_state= + task_states.REBOOTING), + fake_instance.fake_db_instance(uuid='fake_uuid4', + vm_state=vm_states.RESIZED, + task_state=None), + fake_instance.fake_db_instance(uuid='fake_uuid5', + vm_state=vm_states.ACTIVE, + task_state=None), + fake_instance.fake_db_instance(uuid='fake_uuid6', + vm_state=vm_states.RESIZED, + task_state='deleting')] expected_migration_status = {'fake_uuid1': 'confirmed', 'noexist': 'error', 'fake_uuid2': 'error', @@ -5714,6 +5726,21 @@ class ComputeAPITestCase(BaseTestCase): db.instance_destroy(self.context, instance['uuid']) + def test_delete_if_not_launched(self): + instance, instance_uuid = self._run_instance(params={ + 'host': CONF.host}) + + db.instance_update(self.context, instance['uuid'], + {"vm_state": vm_states.ERROR, + "launched_at": None}) + + self.compute_api.delete(self.context, instance) + + instance = db.instance_get_by_uuid(self.context, instance_uuid) + self.assertEqual(instance['task_state'], task_states.DELETING) + + db.instance_destroy(self.context, instance['uuid']) + def test_delete_in_resizing(self): def fake_quotas_reserve(context, expire=None, project_id=None, **deltas): @@ -5988,7 +6015,7 @@ class ComputeAPITestCase(BaseTestCase): db.instance_destroy(self.context, instance['uuid']) - def test_rebuild(self): + def _test_rebuild(self, vm_state): instance = jsonutils.to_primitive(self._create_fake_instance()) instance_uuid = instance['uuid'] self.compute.run_instance(self.context, instance=instance) @@ -6019,6 +6046,10 @@ class ComputeAPITestCase(BaseTestCase): image_ref = instance["image_ref"] + '-new_image_ref' password = "new_password" + + db.instance_update(self.context, instance['uuid'], + {"vm_state": vm_state}) + self.compute_api.rebuild(self.context, instance, image_ref, password) self.assertEqual(info['image_ref'], image_ref) @@ -6033,6 +6064,34 @@ class ComputeAPITestCase(BaseTestCase): 'preserved': 'preserve this!'}) db.instance_destroy(self.context, instance['uuid']) + def test_rebuild(self): + # Test we can rebuild an instance in the Error State + self._test_rebuild(vm_state=vm_states.ACTIVE) + + def test_rebuild_in_error_state(self): + # Test we can rebuild an instance in the Error State + self._test_rebuild(vm_state=vm_states.ERROR) + + def test_rebuild_in_error_not_launched(self): + instance = jsonutils.to_primitive( + self._create_fake_instance(params={'image_ref': ''})) + instance_uuid = instance['uuid'] + self.stubs.Set(fake_image._FakeImageService, 'show', self.fake_show) + self.compute.run_instance(self.context, instance=instance) + + db.instance_update(self.context, instance['uuid'], + {"vm_state": vm_states.ERROR, + "launched_at": None}) + + instance = db.instance_get_by_uuid(self.context, instance['uuid']) + + self.assertRaises(exception.InstanceInvalidState, + self.compute_api.rebuild, + self.context, + instance, + instance['image_ref'], + "new password") + def test_rebuild_no_image(self): instance = jsonutils.to_primitive( self._create_fake_instance(params={'image_ref': ''})) @@ -6179,7 +6238,7 @@ class ComputeAPITestCase(BaseTestCase): self.stubs.Set(nova.virt.fake.FakeDriver, 'legacy_nwinfo', lambda x: False) - def test_reboot_soft(self): + def _test_reboot_soft(self, vm_state): # Ensure instance can be soft rebooted. instance = jsonutils.to_primitive(self._create_fake_instance()) self.compute.run_instance(self.context, instance=instance) @@ -6196,6 +6255,9 @@ class ComputeAPITestCase(BaseTestCase): inst_ref = db.instance_get_by_uuid(self.context, instance['uuid']) self.assertEqual(inst_ref['task_state'], None) + db.instance_update(self.context, instance['uuid'], + {"vm_state": vm_state}) + reboot_type = "SOFT" self._stub_out_reboot(device_name) self.compute_api.reboot(self.context, inst_ref, reboot_type) @@ -6205,7 +6267,15 @@ class ComputeAPITestCase(BaseTestCase): db.instance_destroy(self.context, inst_ref['uuid']) - def test_reboot_hard(self): + def test_soft_reboot(self): + # Ensure instance can be rebooted while in error state. + self._test_reboot_soft(vm_state=vm_states.ACTIVE) + + def test_soft_reboot_of_instance_in_error(self): + # Ensure instance can be rebooted while in error state. + self._test_reboot_soft(vm_state=vm_states.ERROR) + + def test_reboot_hard(self, vm_state=vm_states.ACTIVE): # Ensure instance can be hard rebooted. instance = jsonutils.to_primitive(self._create_fake_instance()) self.compute.run_instance(self.context, instance=instance) @@ -6222,6 +6292,9 @@ class ComputeAPITestCase(BaseTestCase): inst_ref = db.instance_get_by_uuid(self.context, instance['uuid']) self.assertEqual(inst_ref['task_state'], None) + db.instance_update(self.context, instance['uuid'], + {"vm_state": vm_state}) + reboot_type = "HARD" self._stub_out_reboot(device_name) self.compute_api.reboot(self.context, inst_ref, reboot_type) @@ -6231,6 +6304,10 @@ class ComputeAPITestCase(BaseTestCase): db.instance_destroy(self.context, inst_ref['uuid']) + def test_hard_reboot_of_instance_in_error(self): + # Ensure instance can be rebooted while in error state. + self.test_reboot_hard(vm_state=vm_states.ERROR) + def test_hard_reboot_of_soft_rebooting_instance(self): # Ensure instance can be hard rebooted while soft rebooting. instance = jsonutils.to_primitive(self._create_fake_instance()) @@ -6267,7 +6344,7 @@ class ComputeAPITestCase(BaseTestCase): inst_ref, reboot_type) - def test_soft_reboot_of_rescued_instance(self): + def test_reboot_of_rescued_instance(self): # Ensure instance can't be rebooted while in rescued state. instance = jsonutils.to_primitive(self._create_fake_instance()) self.compute.run_instance(self.context, instance=instance) @@ -6291,6 +6368,33 @@ class ComputeAPITestCase(BaseTestCase): inst_ref, 'HARD') + def test_reboot_of_instance_in_error_not_launched(self): + # Ensure instance can be not rebooted while in error states + # if they have never been booted at least once. + instance = jsonutils.to_primitive(self._create_fake_instance()) + self.compute.run_instance(self.context, instance=instance) + + inst_ref = db.instance_get_by_uuid(self.context, instance['uuid']) + self.assertEqual(inst_ref['task_state'], None) + + db.instance_update(self.context, instance['uuid'], + {"vm_state": vm_states.ERROR, + "launched_at": None}) + + inst_ref = db.instance_get_by_uuid(self.context, inst_ref['uuid']) + + self.assertRaises(exception.InstanceInvalidState, + self.compute_api.reboot, + self.context, + inst_ref, + 'SOFT') + + self.assertRaises(exception.InstanceInvalidState, + self.compute_api.reboot, + self.context, + inst_ref, + 'HARD') + def test_hostname_create(self): # Ensure instance hostname is set during creation. inst_type = flavors.get_flavor_by_name('m1.tiny') @@ -7730,7 +7834,8 @@ class ComputeAPITestCase(BaseTestCase): self.assertRaises(exception.InvalidDevicePath, self.compute_api.attach_volume, self.context, - {'locked': False, 'vm_state': vm_states.ACTIVE}, + {'locked': False, 'vm_state': vm_states.ACTIVE, + 'launched_at': timeutils.utcnow()}, None, '/invalid') @@ -8037,6 +8142,7 @@ class ComputeAPITestCase(BaseTestCase): # Ensure exception is raised while detaching an un-attached volume instance = {'uuid': 'uuid1', 'locked': False, + 'launched_at': timeutils.utcnow(), 'vm_state': vm_states.ACTIVE} volume = {'id': 1, 'attach_status': 'detached'} @@ -8049,6 +8155,7 @@ class ComputeAPITestCase(BaseTestCase): # instance doesn't match. instance = {'uuid': 'uuid1', 'locked': False, + 'launched_at': timeutils.utcnow(), 'vm_state': vm_states.ACTIVE} volume = {'id': 1, 'attach_status': 'in-use', 'instance_uuid': 'uuid2'} @@ -8363,6 +8470,7 @@ class ComputeAPITestCase(BaseTestCase): def test_fail_evacuate_from_non_existing_host(self): inst = {} inst['vm_state'] = vm_states.ACTIVE + inst['launched_at'] = timeutils.utcnow() inst['image_ref'] = FAKE_IMAGE_REF inst['reservation_id'] = 'r-fakeres' inst['user_id'] = self.user_id diff --git a/nova/tests/conductor/test_conductor.py b/nova/tests/conductor/test_conductor.py index 9bb2c00ec..a2a015313 100644 --- a/nova/tests/conductor/test_conductor.py +++ b/nova/tests/conductor/test_conductor.py @@ -1183,18 +1183,28 @@ class _BaseTaskTestCase(object): def test_build_instances(self): instance_type = flavors.get_default_flavor() system_metadata = flavors.save_flavor_info({}, instance_type) - # NOTE(alaski): instance_type -> system_metadata -> instance_type loses - # some data (extra_specs) so we need both for testing. - instance_type_extract = flavors.extract_flavor( + # NOTE(alaski): instance_type -> system_metadata -> instance_type + # loses some data (extra_specs). This build process is using + # scheduler/utils:build_request_spec() which extracts flavor from + # system_metadata and will re-query the DB for extra_specs.. so + # we need to test this properly + expected_instance_type = flavors.extract_flavor( {'system_metadata': system_metadata}) + expected_instance_type['extra_specs'] = 'fake-specs' + + self.mox.StubOutWithMock(db, 'instance_type_extra_specs_get') self.mox.StubOutWithMock(self.conductor_manager.scheduler_rpcapi, 'run_instance') + + db.instance_type_extra_specs_get( + self.context, + instance_type['flavorid']).AndReturn('fake-specs') self.conductor_manager.scheduler_rpcapi.run_instance(self.context, request_spec={ 'image': {'fake_data': 'should_pass_silently'}, 'instance_properties': {'system_metadata': system_metadata, 'uuid': 'fakeuuid'}, - 'instance_type': instance_type_extract, + 'instance_type': expected_instance_type, 'instance_uuids': ['fakeuuid', 'fakeuuid2'], 'block_device_mapping': 'block_device_mapping', 'security_group': 'security_groups'}, diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index ed8cc7424..70f84499a 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -112,6 +112,7 @@ policy_data = """ "compute_extension:baremetal_nodes": "", "compute_extension:cells": "", "compute_extension:certificates": "", + "compute_extension:v3:os-certificates": "", "compute_extension:cloudpipe": "", "compute_extension:cloudpipe_update": "", "compute_extension:config_drive": "", diff --git a/nova/tests/virt/baremetal/test_pxe.py b/nova/tests/virt/baremetal/test_pxe.py index 022f9c692..cd4e5c143 100644 --- a/nova/tests/virt/baremetal/test_pxe.py +++ b/nova/tests/virt/baremetal/test_pxe.py @@ -116,6 +116,7 @@ class PXEClassMethodsTestCase(BareMetalPXETestCase): 'deployment_ari_path': 'eee', 'aki_path': 'fff', 'ari_path': 'ggg', + 'network_info': self.test_network_info, } config = pxe.build_pxe_config(**args) self.assertThat(config, matchers.StartsWith('default deploy')) @@ -140,6 +141,21 @@ class PXEClassMethodsTestCase(BareMetalPXETestCase): matchers.Not(matchers.Contains('kernel ddd')), )) + def test_build_pxe_network_config(self): + self.flags( + pxe_network_config=True, + group='baremetal', + ) + net = utils.get_test_network_info(1) + config = pxe.build_pxe_network_config(net) + self.assertIn('eth0:off', config) + self.assertNotIn('eth1', config) + + net = utils.get_test_network_info(2) + config = pxe.build_pxe_network_config(net) + self.assertIn('eth0:off', config) + self.assertIn('eth1:off', config) + def test_build_network_config(self): net = utils.get_test_network_info(1) config = pxe.build_network_config(net) @@ -458,7 +474,8 @@ class PXEPublicMethodsTestCase(BareMetalPXETestCase): bm_utils.random_alnum(32).AndReturn('alnum') pxe.build_pxe_config( self.node['id'], 'alnum', iqn, - 'aaaa', 'bbbb', 'cccc', 'dddd').AndReturn(pxe_config) + 'aaaa', 'bbbb', 'cccc', 'dddd', + self.test_network_info).AndReturn(pxe_config) bm_utils.write_to_file(pxe_path, pxe_config) for mac in macs: bm_utils.create_link_without_raise( @@ -466,7 +483,8 @@ class PXEPublicMethodsTestCase(BareMetalPXETestCase): self.mox.ReplayAll() - self.driver.activate_bootloader(self.context, self.node, self.instance) + self.driver.activate_bootloader(self.context, self.node, self.instance, + network_info=self.test_network_info) self.mox.VerifyAll() @@ -515,8 +533,8 @@ class PXEPublicMethodsTestCase(BareMetalPXETestCase): row = db.bm_node_get(self.context, 1) self.assertTrue(row['deploy_key'] is None) - self.driver.activate_bootloader(self.context, self.node, - self.instance) + self.driver.activate_bootloader(self.context, self.node, self.instance, + network_info=self.test_network_info) row = db.bm_node_get(self.context, 1) self.assertTrue(row['deploy_key'] is not None) diff --git a/nova/tests/virt/baremetal/test_tilera.py b/nova/tests/virt/baremetal/test_tilera.py index 488cba4df..7ad5c4b6a 100755 --- a/nova/tests/virt/baremetal/test_tilera.py +++ b/nova/tests/virt/baremetal/test_tilera.py @@ -317,7 +317,8 @@ class TileraPublicMethodsTestCase(BareMetalTileraTestCase): self.mox.ReplayAll() - self.driver.activate_bootloader(self.context, self.node, self.instance) + self.driver.activate_bootloader(self.context, self.node, self.instance, + network_info=self.test_network_info) self.mox.VerifyAll() @@ -334,8 +335,8 @@ class TileraPublicMethodsTestCase(BareMetalTileraTestCase): row = db.bm_node_get(self.context, 1) self.assertTrue(row['deploy_key'] is None) - self.driver.activate_bootloader(self.context, self.node, - self.instance) + self.driver.activate_bootloader(self.context, self.node, self.instance, + network_info=self.test_network_info) row = db.bm_node_get(self.context, 1) self.assertTrue(row['deploy_key'] is not None) diff --git a/nova/virt/baremetal/base.py b/nova/virt/baremetal/base.py index 2029400ba..876c70b23 100644 --- a/nova/virt/baremetal/base.py +++ b/nova/virt/baremetal/base.py @@ -30,7 +30,7 @@ class NodeDriver(object): def destroy_images(self, context, node, instance): raise NotImplementedError() - def activate_bootloader(self, context, node, instance): + def activate_bootloader(self, context, node, instance, **kwargs): raise NotImplementedError() def deactivate_bootloader(self, context, node, instance): diff --git a/nova/virt/baremetal/driver.py b/nova/virt/baremetal/driver.py index 376921360..4e8543c3e 100755 --- a/nova/virt/baremetal/driver.py +++ b/nova/virt/baremetal/driver.py @@ -248,7 +248,8 @@ class BareMetalDriver(driver.ComputeDriver): injected_files=injected_files, network_info=network_info, ) - self.driver.activate_bootloader(context, node, instance) + self.driver.activate_bootloader(context, node, instance, + network_info=network_info) self.power_on(instance, node) self.driver.activate_node(context, node, instance) _update_state(context, node, instance, baremetal_states.ACTIVE) diff --git a/nova/virt/baremetal/fake.py b/nova/virt/baremetal/fake.py index b3f39fdc3..76586ab74 100644 --- a/nova/virt/baremetal/fake.py +++ b/nova/virt/baremetal/fake.py @@ -28,7 +28,7 @@ class FakeDriver(base.NodeDriver): def destroy_images(self, context, node, instance): pass - def activate_bootloader(self, context, node, instance): + def activate_bootloader(self, context, node, instance, **kwargs): pass def deactivate_bootloader(self, context, node, instance): diff --git a/nova/virt/baremetal/pxe.py b/nova/virt/baremetal/pxe.py index 6a5a5ece5..f44a5f87a 100644 --- a/nova/virt/baremetal/pxe.py +++ b/nova/virt/baremetal/pxe.py @@ -54,6 +54,10 @@ pxe_opts = [ cfg.IntOpt('pxe_deploy_timeout', help='Timeout for PXE deployments. Default: 0 (unlimited)', default=0), + cfg.BoolOpt('pxe_network_config', + help='If set, pass the network configuration details to the ' + 'initramfs via cmdline.', + default=False), ] LOG = logging.getLogger(__name__) @@ -77,9 +81,22 @@ def _get_cheetah(): return CHEETAH +def build_pxe_network_config(network_info): + interfaces = bm_utils.map_network_interfaces(network_info, CONF.use_ipv6) + template = None + if not CONF.use_ipv6: + template = "ip=%(address)s::%(gateway)s:%(netmask)s::%(name)s:off" + else: + template = ("ip=[%(address_v6)s]::[%(gateway_v6)s]:" + "[%(netmask_v6)s]::%(name)s:off") + + net_config = [template % iface for iface in interfaces] + return ' '.join(net_config) + + def build_pxe_config(deployment_id, deployment_key, deployment_iscsi_iqn, deployment_aki_path, deployment_ari_path, - aki_path, ari_path): + aki_path, ari_path, network_info): """Build the PXE config file for a node This method builds the PXE boot configuration file for a node, @@ -90,6 +107,11 @@ def build_pxe_config(deployment_id, deployment_key, deployment_iscsi_iqn, """ LOG.debug(_("Building PXE config for deployment %s.") % deployment_id) + + network_config = None + if network_info and CONF.baremetal.pxe_network_config: + network_config = build_pxe_network_config(network_info) + pxe_options = { 'deployment_id': deployment_id, 'deployment_key': deployment_key, @@ -99,6 +121,7 @@ def build_pxe_config(deployment_id, deployment_key, deployment_iscsi_iqn, 'aki_path': aki_path, 'ari_path': ari_path, 'pxe_append_params': CONF.baremetal.pxe_append_params, + 'pxe_network_config': network_config, } cheetah = _get_cheetah() pxe_config = str(cheetah( @@ -110,33 +133,7 @@ def build_pxe_config(deployment_id, deployment_key, deployment_iscsi_iqn, 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, - '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) - + interfaces = bm_utils.map_network_interfaces(network_info, CONF.use_ipv6) cheetah = _get_cheetah() network_config = str(cheetah( open(CONF.baremetal.net_config_template).read(), @@ -354,7 +351,7 @@ class PXE(base.NodeDriver): bm_utils.unlink_without_raise(get_image_file_path(instance)) bm_utils.rmtree_without_raise(get_image_dir_path(instance)) - def activate_bootloader(self, context, node, instance): + def activate_bootloader(self, context, node, instance, network_info): """Configure PXE boot loader for an instance Kernel and ramdisk images are downloaded by cache_tftp_images, @@ -398,6 +395,7 @@ class PXE(base.NodeDriver): image_info['deploy_ramdisk'][1], image_info['kernel'][1], image_info['ramdisk'][1], + network_info, ) bm_utils.write_to_file(pxe_config_file_path, pxe_config) diff --git a/nova/virt/baremetal/pxe_config.template b/nova/virt/baremetal/pxe_config.template index f2fcc9b14..54dd98baf 100644 --- a/nova/virt/baremetal/pxe_config.template +++ b/nova/virt/baremetal/pxe_config.template @@ -8,4 +8,4 @@ ipappend 3 label boot kernel ${pxe_options.aki_path} -append initrd=${pxe_options.ari_path} root=${ROOT} ro ${pxe_options.pxe_append_params} +append initrd=${pxe_options.ari_path} root=${ROOT} ro ${pxe_options.pxe_append_params} ${pxe_options.pxe_network_config} diff --git a/nova/virt/baremetal/tilera.py b/nova/virt/baremetal/tilera.py index 64335298c..bb89a5f94 100755 --- a/nova/virt/baremetal/tilera.py +++ b/nova/virt/baremetal/tilera.py @@ -55,31 +55,7 @@ def _get_cheetah(): def build_network_config(network_info): - 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, - '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) - + interfaces = bm_utils.map_network_interfaces(network_info, CONF.use_ipv6) cheetah = _get_cheetah() network_config = str(cheetah( open(CONF.baremetal.net_config_template).read(), @@ -262,7 +238,7 @@ class Tilera(base.NodeDriver): bm_utils.unlink_without_raise(get_image_file_path(instance)) bm_utils.rmtree_without_raise(get_image_dir_path(instance)) - def activate_bootloader(self, context, node, instance): + def activate_bootloader(self, context, node, instance, network_info): """Configure Tilera boot loader for an instance Kernel and ramdisk images are downloaded by cache_tftp_images, diff --git a/nova/virt/baremetal/utils.py b/nova/virt/baremetal/utils.py index b18bfac85..96abcd41b 100644 --- a/nova/virt/baremetal/utils.py +++ b/nova/virt/baremetal/utils.py @@ -81,3 +81,32 @@ def random_alnum(count): import string chars = string.ascii_uppercase + string.digits return "".join(random.choice(chars) for _ in range(count)) + + +def map_network_interfaces(network_info, use_ipv6=False): + # TODO(deva): fix assumption that device names begin with "eth" + # and fix assumption about ordering + if not isinstance(network_info, list): + network_info = [network_info] + + interfaces = [] + for id, (network, mapping) in enumerate(network_info): + address_v6 = None + gateway_v6 = None + netmask_v6 = None + if use_ipv6: + address_v6 = mapping['ip6s'][0]['ip'] + netmask_v6 = mapping['ip6s'][0]['netmask'] + gateway_v6 = mapping['gateway_v6'] + interface = { + 'name': 'eth%d' % id, + '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) + return interfaces @@ -54,6 +54,7 @@ console_scripts = nova-xvpvncproxy = nova.cmd.xvpvncproxy:main nova.api.v3.extensions = + certificates = nova.api.openstack.compute.plugins.v3.certificates:Certificates consoles = nova.api.openstack.compute.plugins.v3.consoles:Consoles extension_info = nova.api.openstack.compute.plugins.v3.extension_info:ExtensionInfo fixed_ips = nova.api.openstack.compute.plugins.v3.fixed_ips:FixedIPs |