diff --git a/doc/notification_samples/instance-interface_attach-end.json b/doc/notification_samples/instance-interface_attach-end.json new file mode 100644 index 000000000000..1c7e1b1f91f6 --- /dev/null +++ b/doc/notification_samples/instance-interface_attach-end.json @@ -0,0 +1,107 @@ +{ + "publisher_id": "nova-compute:compute", + "event_type": "instance.interface_attach.end", + "priority": "INFO", + "payload": { + "nova_object.data": { + "fault": null, + "ramdisk_id": "", + "kernel_id": "", + "progress": 0, + "deleted_at": null, + "reservation_id": "r-xweiyxxa", + "created_at": "2012-10-29T13:42:11Z", + "uuid": "cb968404-7797-4896-9164-bbb1a1f9530b", + "display_description": "some-server", + "node": "fake-mini", + "os_type": null, + "host_name": "some-server", + "locked": false, + "user_id": "fake", + "ip_addresses": [ + { + "nova_object.data": { + "device_name": "tapce531f90-19", + "address": "192.168.1.3", + "version": 4, + "label": "private-network", + "port_uuid": "ce531f90-199f-48c0-816c-13e38010b442", + "mac": "fa:16:3e:4c:2c:30", + "meta": {} + }, + "nova_object.name": "IpPayload", + "nova_object.namespace": "nova", + "nova_object.version": "1.0" + }, + { + "nova_object.data": { + "device_name": "tap88dae9fa-0d", + "address": "192.168.1.30", + "version": 4, + "label": "private-network", + "port_uuid": "88dae9fa-0dc6-49e3-8c29-3abc41e99ac9", + "mac": "00:0c:29:0d:11:74", + "meta": {} + }, + "nova_object.name": "IpPayload", + "nova_object.namespace": "nova", + "nova_object.version": "1.0" + } + ], + "architecture": "x86_64", + "display_name": "some-server", + "launched_at": "2012-10-29T13:42:11Z", + "updated_at": "2012-10-29T13:42:11Z", + "key_name": "my-key", + "auto_disk_config": "MANUAL", + "block_devices": [ + { + "nova_object.data": { + "boot_index": null, + "delete_on_termination": false, + "device_name": "/dev/sdb", + "tag": null, + "volume_id": "a07f71dc-8151-4e7d-a0cc-cd24a3f11113" + }, + "nova_object.name": "BlockDevicePayload", + "nova_object.namespace": "nova", + "nova_object.version": "1.0" + } + ], + "flavor": { + "nova_object.data": { + "swap": 0, + "rxtx_factor": 1.0, + "memory_mb": 512, + "name": "test_flavor", + "vcpu_weight": 0, + "root_gb": 1, + "vcpus": 1, + "is_public": true, + "ephemeral_gb": 0, + "extra_specs": { + "hw:watchdog_action": "disabled" + }, + "disabled": false, + "projects": null, + "flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3" + }, + "nova_object.name": "FlavorPayload", + "nova_object.namespace": "nova", + "nova_object.version": "1.3" + }, + "image_uuid": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + "power_state": "running", + "metadata": {}, + "tenant_id": "6f70656e737461636b20342065766572", + "terminated_at": null, + "task_state": null, + "host": "compute", + "state": "active", + "availability_zone": "nova" + }, + "nova_object.name": "InstanceActionPayload", + "nova_object.namespace": "nova", + "nova_object.version": "1.5" + } +} diff --git a/doc/notification_samples/instance-interface_attach-start.json b/doc/notification_samples/instance-interface_attach-start.json new file mode 100644 index 000000000000..8849cfa0a120 --- /dev/null +++ b/doc/notification_samples/instance-interface_attach-start.json @@ -0,0 +1,93 @@ +{ + "priority": "INFO", + "event_type": "instance.interface_attach.start", + "payload": { + "nova_object.name": "InstanceActionPayload", + "nova_object.version": "1.5", + "nova_object.namespace": "nova", + "nova_object.data": { + "power_state": "running", + "host_name": "some-server", + "reservation_id": "r-f8grvm0d", + "metadata": {}, + "os_type": null, + "display_description": "some-server", + "ramdisk_id": "", + "deleted_at": null, + "launched_at": "2012-10-29T13:42:11Z", + "updated_at": "2012-10-29T13:42:11Z", + "key_name": "my-key", + "auto_disk_config": "MANUAL", + "block_devices": [ + { + "nova_object.data": { + "boot_index": null, + "delete_on_termination": false, + "device_name": "/dev/sdb", + "tag": null, + "volume_id": "a07f71dc-8151-4e7d-a0cc-cd24a3f11113" + }, + "nova_object.name": "BlockDevicePayload", + "nova_object.namespace": "nova", + "nova_object.version": "1.0" + } + ], + "availability_zone": "nova", + "display_name": "some-server", + "fault": null, + "locked": false, + "ip_addresses": [ + { + "nova_object.name": "IpPayload", + "nova_object.version": "1.0", + "nova_object.namespace": "nova", + "nova_object.data": { + "label": "private-network", + "mac": "fa:16:3e:4c:2c:30", + "version": 4, + "address": "192.168.1.3", + "meta": {}, + "port_uuid": "ce531f90-199f-48c0-816c-13e38010b442", + "device_name": "tapce531f90-19" + } + } + ], + "kernel_id": "", + "progress": 0, + "tenant_id": "6f70656e737461636b20342065766572", + "state": "active", + "task_state": null, + "uuid": "edbe0f81-b150-4fce-9258-4e03bb2ecb41", + "user_id": "fake", + "created_at": "2012-10-29T13:42:11Z", + "image_uuid": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + "node": "fake-mini", + "flavor": { + "nova_object.name": "FlavorPayload", + "nova_object.version": "1.3", + "nova_object.namespace": "nova", + "nova_object.data": { + "vcpu_weight": 0, + "memory_mb": 512, + "name": "test_flavor", + "root_gb": 1, + "extra_specs": { + "hw:watchdog_action": "disabled" + }, + "disabled": false, + "rxtx_factor": 1.0, + "vcpus": 1, + "is_public": true, + "swap": 0, + "flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3", + "projects": null, + "ephemeral_gb": 0 + } + }, + "host": "compute", + "terminated_at": null, + "architecture": "x86_64" + } + }, + "publisher_id": "nova-compute:compute" +} diff --git a/nova/compute/manager.py b/nova/compute/manager.py index f2cde6f185bf..8430f706ed77 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -5329,6 +5329,12 @@ class ComputeManager(manager.Manager): self.driver.capabilities.get('supports_tagged_attach_interface', False)): raise exception.NetworkInterfaceTaggedAttachNotSupported() + + compute_utils.notify_about_instance_action( + context, instance, self.host, + action=fields.NotificationAction.INTERFACE_ATTACH, + phase=fields.NotificationPhase.START) + bind_host_id = self.driver.network_binding_host_id(context, instance) network_info = self.network_api.allocate_port_for_instance( context, instance, port_id, network_id, requested_ip, @@ -5358,6 +5364,11 @@ class ComputeManager(manager.Manager): raise exception.InterfaceAttachFailed( instance_uuid=instance.uuid) + compute_utils.notify_about_instance_action( + context, instance, self.host, + action=fields.NotificationAction.INTERFACE_ATTACH, + phase=fields.NotificationPhase.END) + return network_info[0] @wrap_exception() diff --git a/nova/notifications/objects/base.py b/nova/notifications/objects/base.py index ec873eef7cea..d3b4318554a6 100644 --- a/nova/notifications/objects/base.py +++ b/nova/notifications/objects/base.py @@ -48,7 +48,9 @@ class EventType(NotificationObject): # NotificationActionField enum # Version 1.5: Aggregate related values have been added to # NotificationActionField enum - VERSION = '1.5' + # Version 1.6: ADD_FIX_IP replaced with INTERFACE_ATTACH in + # NotificationActionField enum + VERSION = '1.6' fields = { 'object': fields.StringField(nullable=False), diff --git a/nova/notifications/objects/instance.py b/nova/notifications/objects/instance.py index cf9d5bf323d1..5739fe99a5bd 100644 --- a/nova/notifications/objects/instance.py +++ b/nova/notifications/objects/instance.py @@ -401,8 +401,8 @@ class InstanceStateUpdatePayload(base.NotificationPayloadBase): @base.notification_sample('instance-shutdown-end.json') @base.notification_sample('instance-snapshot-start.json') @base.notification_sample('instance-snapshot-end.json') -# @base.notification_sample('instance-add_fixed_ip-start.json') -# @base.notification_sample('instance-add_fixed_ip-end.json') +@base.notification_sample('instance-interface_attach-start.json') +@base.notification_sample('instance-interface_attach-end.json') @base.notification_sample('instance-shelve-start.json') @base.notification_sample('instance-shelve-end.json') @base.notification_sample('instance-resume-start.json') diff --git a/nova/objects/fields.py b/nova/objects/fields.py index 089f12c95eba..c670829bbad3 100644 --- a/nova/objects/fields.py +++ b/nova/objects/fields.py @@ -816,7 +816,7 @@ class NotificationAction(BaseNovaEnum): REBOOT = 'reboot' SHUTDOWN = 'shutdown' SNAPSHOT = 'snapshot' - ADD_FIXED_IP = 'add_fixed_ip' + INTERFACE_ATTACH = 'interface_attach' SHELVE = 'shelve' RESUME = 'resume' RESTORE = 'restore' @@ -847,7 +847,7 @@ class NotificationAction(BaseNovaEnum): REMOVE_HOST = 'remove_host' ALL = (UPDATE, EXCEPTION, DELETE, PAUSE, UNPAUSE, RESIZE, VOLUME_SWAP, - SUSPEND, POWER_ON, REBOOT, SHUTDOWN, SNAPSHOT, ADD_FIXED_IP, + SUSPEND, POWER_ON, REBOOT, SHUTDOWN, SNAPSHOT, INTERFACE_ATTACH, POWER_OFF, SHELVE, RESUME, RESTORE, EXISTS, RESCUE, VOLUME_ATTACH, VOLUME_DETACH, CREATE, EVACUATE, RESIZE_FINISH, LIVE_MIGRATION_ABORT, LIVE_MIGRATION_POST_DEST, LIVE_MIGRATION_POST, diff --git a/nova/tests/fixtures.py b/nova/tests/fixtures.py index 1be06f1ac41f..35dfe89cd587 100644 --- a/nova/tests/fixtures.py +++ b/nova/tests/fixtures.py @@ -19,6 +19,7 @@ from __future__ import absolute_import import collections from contextlib import contextmanager +import copy import logging as std_logging import os import warnings @@ -1099,7 +1100,6 @@ class NeutronFixture(fixtures.Fixture): 'tenant_id': tenant_id } - # This port is only used if the fixture is created with multiple_ports=True port_2 = { 'id': '88dae9fa-0dc6-49e3-8c29-3abc41e99ac9', 'network_id': network_1['id'], @@ -1164,10 +1164,14 @@ class NeutronFixture(fixtures.Fixture): "qbg_params": None }] - def __init__(self, test, multiple_ports=False): + def __init__(self, test): super(NeutronFixture, self).__init__() self.test = test - self.multiple_ports = multiple_ports + self._ports = [copy.deepcopy(NeutronFixture.port_1)] + self._extensions = [] + self._networks = [NeutronFixture.network_1] + self._subnets = [NeutronFixture.subnet_1] + self._floatingips = [] def setUp(self): super(NeutronFixture, self).setUp() @@ -1202,35 +1206,54 @@ class NeutronFixture(fixtures.Fixture): 'get_instances_security_groups_bindings', lambda *args, **kwargs: {}) - mock_neutron_client = mock.Mock() - mock_neutron_client.list_extensions.return_value = {'extensions': []} + self.test.stub_out('nova.network.neutronv2.api.get_client', + lambda *args, **kwargs: self) - def stub_show_port(port_id, *args, **kwargs): - if port_id == NeutronFixture.port_1['id']: - return {'port': NeutronFixture.port_1} - if port_id == NeutronFixture.port_2['id']: - return {'port': NeutronFixture.port_2} + def _get_first_id_match(self, id, list): + filtered_list = [p for p in list if p['id'] == id] + if len(filtered_list) > 0: + return filtered_list[0] + else: + return None + + def list_extensions(self, *args, **kwargs): + return copy.deepcopy({'extensions': self._extensions}) + + def show_port(self, port_id, **_params): + port = self._get_first_id_match(port_id, self._ports) + if port is None: raise exception.PortNotFound(port_id=port_id) + return {'port': port} - mock_neutron_client.show_port.side_effect = stub_show_port - mock_neutron_client.list_networks.return_value = { - 'networks': [NeutronFixture.network_1]} + def delete_port(self, port, **_params): + for current_port in self._ports: + if current_port['id'] == port: + self._ports.remove(current_port) - def stub_list_ports(*args, **kwargs): - ports = {'ports': [NeutronFixture.port_1]} - if self.multiple_ports: - ports['ports'].append(NeutronFixture.port_2) - return ports + def list_networks(self, retrieve_all=True, **_params): + return copy.deepcopy({'networks': self._networks}) - mock_neutron_client.list_ports.side_effect = stub_list_ports - mock_neutron_client.list_subnets.return_value = { - 'subnets': [NeutronFixture.subnet_1]} - mock_neutron_client.list_floatingips.return_value = {'floatingips': []} - mock_neutron_client.update_port.side_effect = stub_show_port + def list_ports(self, retrieve_all=True, **_params): + return copy.deepcopy({'ports': self._ports}) - self.test.stub_out( - 'nova.network.neutronv2.api.get_client', - lambda *args, **kwargs: mock_neutron_client) + def list_subnets(self, retrieve_all=True, **_params): + return copy.deepcopy({'subnets': self._subnets}) + + def list_floatingips(self, retrieve_all=True, **_params): + return copy.deepcopy({'floatingips': self._floatingips}) + + def create_port(self, *args, **kwargs): + self._ports.append(copy.deepcopy(NeutronFixture.port_2)) + return copy.deepcopy({'port': NeutronFixture.port_2}) + + def update_port(self, port_id, body=None): + new_port = self._get_first_id_match(port_id, self._ports) + + if body is not None: + for k, v in body['port'].items(): + new_port[k] = v + + return {'port': new_port} class _NoopConductor(object): diff --git a/nova/tests/functional/api/client.py b/nova/tests/functional/api/client.py index f5df234f96c1..18630f46f329 100644 --- a/nova/tests/functional/api/client.py +++ b/nova/tests/functional/api/client.py @@ -421,6 +421,9 @@ class TestOpenStackClient(object): return self.api_get('/servers/%s/os-interface' % (server_id)).body['interfaceAttachments'] + def attach_interface(self, server_id, post): + return self.api_post('/servers/%s/os-interface' % server_id, post) + def detach_interface(self, server_id, port_id): return self.api_delete('/servers/%s/os-interface/%s' % (server_id, port_id)) diff --git a/nova/tests/functional/notification_sample_tests/test_instance.py b/nova/tests/functional/notification_sample_tests/test_instance.py index cffc3f0ca529..32f2aad9af6c 100644 --- a/nova/tests/functional/notification_sample_tests/test_instance.py +++ b/nova/tests/functional/notification_sample_tests/test_instance.py @@ -149,6 +149,7 @@ class TestInstanceNotificationSample( self._test_unrescue_server, self._test_soft_delete_server, self._test_attach_volume_error, + self._test_interface_attach, ] for action in actions: @@ -1143,3 +1144,25 @@ class TestInstanceNotificationSample( 'volume_id': self.cinder.SWAP_NEW_VOL, 'uuid': server['id']}, actual=fake_notifier.VERSIONED_NOTIFICATIONS[1]) + + def _test_interface_attach(self, server): + post = { + 'interfaceAttachment': { + 'net_id': fixtures.NeutronFixture.network_1['id'] + } + } + self.api.attach_interface(server['id'], post) + self._wait_for_notification('instance.interface_attach.end') + self.assertEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + self._verify_notification( + 'instance-interface_attach-start', + replacements={ + 'reservation_id': server['reservation_id'], + 'uuid': server['id']}, + actual=fake_notifier.VERSIONED_NOTIFICATIONS[0]) + self._verify_notification( + 'instance-interface_attach-end', + replacements={ + 'reservation_id': server['reservation_id'], + 'uuid': server['id']}, + actual=fake_notifier.VERSIONED_NOTIFICATIONS[1]) diff --git a/nova/tests/functional/test_list_servers_ip_filter.py b/nova/tests/functional/test_list_servers_ip_filter.py index e52ee9ad1faf..1a6594764f1c 100644 --- a/nova/tests/functional/test_list_servers_ip_filter.py +++ b/nova/tests/functional/test_list_servers_ip_filter.py @@ -29,7 +29,9 @@ class TestListServersIpFilter(test.TestCase): super(TestListServersIpFilter, self).setUp() self.useFixture(policy_fixture.RealPolicyFixture()) self.neutron = self.useFixture( - nova_fixtures.NeutronFixture(self, multiple_ports=True)) + nova_fixtures.NeutronFixture(self)) + # Add a 2nd port to the neutron fixture to have multiple ports + self.neutron.create_port(self.neutron.port_2) api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( api_version='v2.1')) self.api = api_fixture.api diff --git a/nova/tests/unit/compute/test_compute.py b/nova/tests/unit/compute/test_compute.py index 45ea2c30b07b..a605016b7590 100644 --- a/nova/tests/unit/compute/test_compute.py +++ b/nova/tests/unit/compute/test_compute.py @@ -9840,7 +9840,8 @@ class ComputeAPITestCase(BaseTestCase): self.compute_api.get_console_output, self.context, instance) - def test_attach_interface(self): + @mock.patch.object(compute_utils, 'notify_about_instance_action') + def test_attach_interface(self, mock_notify): new_type = flavors.get_flavor_by_flavor_id('4') instance = objects.Instance(image_ref=uuids.image_instance, system_metadata={}, @@ -9864,9 +9865,15 @@ class ComputeAPITestCase(BaseTestCase): mock_allocate.assert_called_once_with( self.context, instance, port_id, network_id, req_ip, bind_host_id='fake-host', tag=None) + mock_notify.assert_has_calls([ + mock.call(self.context, instance, self.compute.host, + action='interface_attach', phase='start'), + mock.call(self.context, instance, self.compute.host, + action='interface_attach', phase='end')]) return nwinfo, port_id - def test_interface_tagged_attach(self): + @mock.patch.object(compute_utils, 'notify_about_instance_action') + def test_interface_tagged_attach(self, mock_notify): new_type = flavors.get_flavor_by_flavor_id('4') instance = objects.Instance(image_ref=uuids.image_instance, system_metadata={}, @@ -9891,6 +9898,11 @@ class ComputeAPITestCase(BaseTestCase): mock_allocate.assert_called_once_with( self.context, instance, port_id, network_id, req_ip, bind_host_id='fake-host', tag='foo') + mock_notify.assert_has_calls([ + mock.call(self.context, instance, self.compute.host, + action='interface_attach', phase='start'), + mock.call(self.context, instance, self.compute.host, + action='interface_attach', phase='end')]) return nwinfo, port_id def test_tagged_attach_interface_raises(self): @@ -9918,6 +9930,7 @@ class ComputeAPITestCase(BaseTestCase): req_ip = '1.2.3.4' with test.nested( + mock.patch.object(compute_utils, 'notify_about_instance_action'), mock.patch.object(self.compute.driver, 'attach_interface'), mock.patch.object(self.compute.network_api, 'allocate_port_for_instance'), @@ -9925,7 +9938,8 @@ class ComputeAPITestCase(BaseTestCase): 'deallocate_port_for_instance'), mock.patch.dict(self.compute.driver.capabilities, supports_attach_interface=True)) as ( - mock_attach, mock_allocate, mock_deallocate, mock_dict): + mock_notify, mock_attach, mock_allocate, mock_deallocate, + mock_dict): mock_allocate.return_value = nwinfo mock_attach.side_effect = exception.NovaException("attach_failed") @@ -9938,6 +9952,9 @@ class ComputeAPITestCase(BaseTestCase): tag=None) mock_deallocate.assert_called_once_with(self.context, instance, port_id) + mock_notify.assert_has_calls([ + mock.call(self.context, instance, self.compute.host, + action='interface_attach', phase='start')]) def test_detach_interface(self): nwinfo, port_id = self.test_attach_interface() diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py index 7ffb32fd332d..2b6556194fd6 100644 --- a/nova/tests/unit/compute/test_compute_mgr.py +++ b/nova/tests/unit/compute/test_compute_mgr.py @@ -1768,13 +1768,14 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase): db_instance) e = exception.InterfaceAttachFailed(instance_uuid=f_instance.uuid) + @mock.patch.object(compute_utils, 'notify_about_instance_action') @mock.patch.object(compute_utils, 'add_instance_fault_from_exc') @mock.patch.object(self.compute.network_api, 'allocate_port_for_instance', side_effect=e) @mock.patch.object(self.compute, '_instance_update', side_effect=lambda *a, **k: {}) - def do_test(update, meth, add_fault): + def do_test(update, meth, add_fault, notify): self.assertRaises(exception.InterfaceAttachFailed, self.compute.attach_interface, self.context, f_instance, 'net_id', 'port_id', diff --git a/nova/tests/unit/notifications/objects/test_notification.py b/nova/tests/unit/notifications/objects/test_notification.py index 975fffa8e515..2f34a91d333d 100644 --- a/nova/tests/unit/notifications/objects/test_notification.py +++ b/nova/tests/unit/notifications/objects/test_notification.py @@ -373,7 +373,7 @@ notification_object_data = { 'AuditPeriodPayload': '1.0-2b429dd307b8374636703b843fa3f9cb', 'BandwidthPayload': '1.0-ee2616a7690ab78406842a2b68e34130', 'BlockDevicePayload': '1.0-29751e1b6d41b1454e36768a1e764df8', - 'EventType': '1.5-ffa6d332f4462c45a2a363356a14165f', + 'EventType': '1.6-7d6ac2f1335a814202c2f7878b2ef978', 'ExceptionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'ExceptionPayload': '1.0-27db46ee34cd97e39f2643ed92ad0cc5', 'FlavorNotification': '1.0-a73147b93b520ff0061865849d3dfa56',