Add swap volume notifications (start, end)

Add the following notifications when swapping volumes.

* 'instance.volume_swap.start'
* 'instance.volume_swap.end'

A subsequent patch will add 'instance.volume_swap.error'.

Change-Id: Ic4d9d25bdc611221157e4663817e918b8d667ce9
Implements: blueprint add-swap-volume-notifications
This commit is contained in:
Takashi NATSUME 2016-07-01 21:47:20 +09:00
parent 3d7ff766a7
commit 47fb8b7579
12 changed files with 451 additions and 19 deletions

View File

@ -0,0 +1,64 @@
{
"event_type": "instance.volume_swap.end",
"payload": {
"nova_object.data": {
"architecture": "x86_64",
"availability_zone": null,
"created_at": "2012-10-29T13:42:11Z",
"deleted_at": null,
"display_name": "some-server",
"fault": null,
"flavor": {
"nova_object.data": {
"flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3",
"root_gb": 1,
"vcpus": 1,
"ephemeral_gb": 0,
"memory_mb": 512
},
"nova_object.name": "FlavorPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.0"
},
"host": "compute",
"host_name": "some-server",
"image_uuid": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
"ip_addresses": [{
"nova_object.data": {
"address": "192.168.1.3",
"device_name": "tapce531f90-19",
"label": "private-network",
"meta": {},
"port_uuid": "ce531f90-199f-48c0-816c-13e38010b442",
"version": 4,
"mac": "fa:16:3e:4c:2c:30"
},
"nova_object.name": "IpPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.0"
}],
"kernel_id": "",
"launched_at": "2012-10-29T13:42:11Z",
"metadata": {},
"new_volume_id": "227cc671-f30b-4488-96fd-7d0bf13648d8",
"node": "fake-mini",
"old_volume_id": "a07f71dc-8151-4e7d-a0cc-cd24a3f11113",
"os_type": null,
"power_state":"running",
"progress": 0,
"ramdisk_id": "",
"reservation_id": "r-6w6ruqaz",
"state": "active",
"task_state": null,
"tenant_id": "6f70656e737461636b20342065766572",
"terminated_at": null,
"user_id": "fake",
"uuid": "0ab886d0-7443-4107-9265-48371bfa662b"
},
"nova_object.name": "InstanceActionVolumeSwapPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.0"
},
"priority": "INFO",
"publisher_id": "nova-compute:compute"
}

View File

@ -0,0 +1,64 @@
{
"event_type": "instance.volume_swap.start",
"payload": {
"nova_object.data": {
"architecture": "x86_64",
"availability_zone": null,
"created_at": "2012-10-29T13:42:11Z",
"deleted_at": null,
"display_name": "some-server",
"fault": null,
"flavor": {
"nova_object.data": {
"flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3",
"root_gb": 1,
"vcpus": 1,
"ephemeral_gb": 0,
"memory_mb": 512
},
"nova_object.name": "FlavorPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.0"
},
"host": "compute",
"host_name": "some-server",
"image_uuid": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
"ip_addresses": [{
"nova_object.data": {
"address": "192.168.1.3",
"device_name": "tapce531f90-19",
"label": "private-network",
"meta": {},
"port_uuid": "ce531f90-199f-48c0-816c-13e38010b442",
"version": 4,
"mac": "fa:16:3e:4c:2c:30"
},
"nova_object.name": "IpPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.0"
}],
"kernel_id": "",
"launched_at": "2012-10-29T13:42:11Z",
"metadata": {},
"new_volume_id": "227cc671-f30b-4488-96fd-7d0bf13648d8",
"node": "fake-mini",
"old_volume_id": "a07f71dc-8151-4e7d-a0cc-cd24a3f11113",
"os_type": null,
"power_state":"running",
"progress": 0,
"ramdisk_id": "",
"reservation_id": "r-6w6ruqaz",
"state": "active",
"task_state": null,
"tenant_id": "6f70656e737461636b20342065766572",
"terminated_at": null,
"user_id": "fake",
"uuid": "0ab886d0-7443-4107-9265-48371bfa662b"
},
"nova_object.name": "InstanceActionVolumeSwapPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.0"
},
"priority": "INFO",
"publisher_id": "nova-compute:compute"
}

View File

@ -4916,6 +4916,12 @@ class ComputeManager(manager.Manager):
"""Swap volume for an instance."""
context = context.elevated()
compute_utils.notify_about_volume_swap(
context, instance, self.host,
fields.NotificationAction.VOLUME_SWAP,
fields.NotificationPhase.START,
old_volume_id, new_volume_id)
bdm = objects.BlockDeviceMapping.get_by_volume_and_instance(
context, old_volume_id, instance.uuid)
connector = self.driver.get_volume_connector(instance)
@ -4957,6 +4963,12 @@ class ComputeManager(manager.Manager):
bdm.update(values)
bdm.save()
compute_utils.notify_about_volume_swap(
context, instance, self.host,
fields.NotificationAction.VOLUME_SWAP,
fields.NotificationPhase.END,
old_volume_id, new_volume_id)
@wrap_exception()
def remove_volume_connection(self, context, volume_id, instance):
"""Remove a volume connection using the volume api."""

View File

@ -321,15 +321,7 @@ def notify_about_instance_usage(notifier, context, instance, event_suffix,
method(context, 'compute.instance.%s' % event_suffix, usage_info)
def notify_about_instance_action(context, instance, host, action, phase=None,
binary='nova-compute'):
"""Send versioned notification about the action made on the instance
:param instance: the instance which the action performed on
:param host: the host emitting the notification
:param action: the name of the action
:param phase: the phase of the action
:param binary: the binary emitting the notification
"""
def _get_instance_ips(instance):
network_info = get_nw_info_for_instance(instance)
ips = []
if network_info is not None:
@ -343,6 +335,20 @@ def notify_about_instance_action(context, instance, host, action, phase=None,
version=ip["version"],
address=ip["address"],
device_name=vif["devname"]))
return ips
def notify_about_instance_action(context, instance, host, action, phase=None,
binary='nova-compute'):
"""Send versioned notification about the action made on the instance
:param instance: the instance which the action performed on
:param host: the host emitting the notification
:param action: the name of the action
:param phase: the phase of the action
:param binary: the binary emitting the notification
"""
ips = _get_instance_ips(instance)
flavor = instance_notification.FlavorPayload(instance=instance)
# TODO(gibi): handle fault during the transformation of the first error
# notifications
@ -364,6 +370,40 @@ def notify_about_instance_action(context, instance, host, action, phase=None,
notification.emit(context)
def notify_about_volume_swap(context, instance, host, action, phase,
old_volume_id, new_volume_id):
"""Send versioned notification about the volume swap action
on the instance
:param context: the request context
:param instance: the instance which the action performed on
:param host: the host emitting the notification
:param action: the name of the action
:param phase: the phase of the action
:param old_volume_id: the ID of the volume that is copied from and detached
:param new_volume_id: the ID of the volume that is copied to and attached
"""
ips = _get_instance_ips(instance)
flavor = instance_notification.FlavorPayload(instance=instance)
payload = instance_notification.InstanceActionVolumeSwapPayload(
instance=instance,
fault=None,
ip_addresses=ips,
flavor=flavor,
old_volume_id=old_volume_id,
new_volume_id=new_volume_id)
instance_notification.InstanceActionVolumeSwapNotification(
context=context,
priority=fields.NotificationPriority.INFO,
publisher=notification_base.NotificationPublisher(
context=context, host=host, binary='nova-compute'),
event_type=notification_base.EventType(
object='instance', action=action, phase=phase),
payload=payload).emit(context)
def notify_about_server_group_update(context, event_suffix, sg_payload):
"""Send a notification about server group update.

View File

@ -97,12 +97,34 @@ class InstanceActionPayload(InstancePayload):
'fault': fields.ObjectField('ExceptionPayload', nullable=True),
}
def __init__(self, instance, fault, ip_addresses, flavor):
def __init__(self, instance, fault, ip_addresses, flavor, **kwargs):
super(InstanceActionPayload, self).__init__(
instance=instance,
fault=fault,
ip_addresses=ip_addresses,
flavor=flavor)
flavor=flavor,
**kwargs)
@nova_base.NovaObjectRegistry.register_notification
class InstanceActionVolumeSwapPayload(InstanceActionPayload):
# No SCHEMA as all the additional fields are calculated
VERSION = '1.0'
fields = {
'old_volume_id': fields.UUIDField(),
'new_volume_id': fields.UUIDField(),
}
def __init__(self, instance, fault, ip_addresses, flavor,
old_volume_id, new_volume_id):
super(InstanceActionVolumeSwapPayload, self).__init__(
instance=instance,
fault=fault,
ip_addresses=ip_addresses,
flavor=flavor,
old_volume_id=old_volume_id,
new_volume_id=new_volume_id)
@nova_base.NovaObjectRegistry.register_notification
@ -268,3 +290,15 @@ class InstanceUpdateNotification(base.NotificationBase):
fields = {
'payload': fields.ObjectField('InstanceUpdatePayload')
}
@base.notification_sample('instance-volume_swap-start.json')
@base.notification_sample('instance-volume_swap-end.json')
@nova_base.NovaObjectRegistry.register_notification
class InstanceActionVolumeSwapNotification(base.NotificationBase):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'payload': fields.ObjectField('InstanceActionVolumeSwapPayload')
}

View File

@ -790,3 +790,83 @@ class NoopConductorFixture(fixtures.Fixture):
'nova.conductor.ComputeTaskAPI', _NoopConductor))
self.useFixture(fixtures.MonkeyPatch(
'nova.conductor.API', _NoopConductor))
class CinderFixture(fixtures.Fixture):
"""A fixture to volume operations"""
# the default project_id in OSAPIFixtures
tenant_id = '6f70656e737461636b20342065766572'
SWAP_OLD_VOL = 'a07f71dc-8151-4e7d-a0cc-cd24a3f11113'
SWAP_NEW_VOL = '227cc671-f30b-4488-96fd-7d0bf13648d8'
def __init__(self, test):
super(CinderFixture, self).__init__()
self.test = test
self.swap_volume_instance_uuid = None
def setUp(self):
super(CinderFixture, self).setUp()
def fake_get(self_api, context, volume_id):
if volume_id == CinderFixture.SWAP_OLD_VOL:
volume = {
'status': 'available',
'display_name': 'TEST1',
'attach_status': 'detached',
'id': volume_id,
'size': 1
}
if (self.swap_volume_instance_uuid and
volume_id == CinderFixture.SWAP_OLD_VOL):
instance_uuid = self.swap_volume_instance_uuid
volume.update({
'status': 'in-use',
'attachments': {
instance_uuid: {
'mountpoint': '/dev/vdb',
'attachment_id': volume_id
}
},
'attach_status': 'attached'
})
return volume
else:
return {
'status': 'available',
'display_name': 'TEST2',
'attach_status': 'detached',
'id': volume_id,
'size': 1
}
def fake_initialize_connection(self, context, volume_id, connector):
return {}
def fake_migrate_volume_completion(self, context, old_volume_id,
new_volume_id, error):
return {'save_volume_id': new_volume_id}
self.test.stub_out('nova.volume.cinder.API.attach',
lambda *args, **kwargs: None)
self.test.stub_out('nova.volume.cinder.API.begin_detaching',
lambda *args, **kwargs: None)
self.test.stub_out('nova.volume.cinder.API.check_attach',
lambda *args, **kwargs: None)
self.test.stub_out('nova.volume.cinder.API.check_detach',
lambda *args, **kwargs: None)
self.test.stub_out('nova.volume.cinder.API.get',
fake_get)
self.test.stub_out('nova.volume.cinder.API.initialize_connection',
fake_initialize_connection)
self.test.stub_out(
'nova.volume.cinder.API.migrate_volume_completion',
fake_migrate_volume_completion)
self.test.stub_out('nova.volume.cinder.API.reserve_volume',
lambda *args, **kwargs: None)
self.test.stub_out('nova.volume.cinder.API.roll_detaching',
lambda *args, **kwargs: None)
self.test.stub_out('nova.volume.cinder.API.terminate_connection',
lambda *args, **kwargs: None)

View File

@ -349,6 +349,11 @@ class TestOpenStackClient(object):
(server_id), volume_attachment
).body['volumeAttachment']
def put_server_volume(self, server_id, attachment_id, volume_id):
return self.api_put('/servers/%s/os-volume_attachments/%s' %
(server_id, attachment_id),
{"volumeAttachment": {"volumeId": volume_id}})
def delete_server_volume(self, server_id, attachment_id):
return self.api_delete('/servers/%s/os-volume_attachments/%s' %
(server_id, attachment_id))

View File

@ -9,6 +9,8 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import time
from nova import context
from nova.tests import fixtures
from nova.tests.functional.notification_sample_tests \
@ -18,12 +20,26 @@ from nova.tests.unit import fake_notifier
class TestInstanceNotificationSample(
notification_sample_base.NotificationSampleTestBase):
EVENT_TYPE_SWAP_VOL_START = 'instance-volume_swap-start'
EVENT_TYPE_SWAP_VOL_END = 'instance-volume_swap-end'
def setUp(self):
self.flags(use_neutron=True)
super(TestInstanceNotificationSample, self).setUp()
self.neutron = fixtures.NeutronFixture(self)
self.useFixture(self.neutron)
self.cinder = fixtures.CinderFixture(self)
self.useFixture(self.cinder)
def _wait_until_swap_volume(self, server, volume_id):
for i in range(50):
volume_attachments = self.api.get_server_volumes(server['id'])
if len(volume_attachments) > 0:
for volume_attachment in volume_attachments:
if volume_attachment['volumeId'] == volume_id:
return
time.sleep(0.5)
self.fail('Volume swap operation failed.')
def test_instance_action(self):
# A single test case is used to test most of the instance action
@ -350,3 +366,36 @@ class TestInstanceNotificationSample(
actual=fake_notifier.VERSIONED_NOTIFICATIONS[1])
self.flags(reclaim_instance_interval=0)
def _attach_volume_to_server(self, server, volume_id):
self.api.post_server_volume(
server['id'], {"volumeAttachment": {"volumeId": volume_id}})
def _volume_swap_server(self, server, attachement_id, volume_id):
self.api.put_server_volume(server['id'], attachement_id, volume_id)
def test_volume_swap_server(self):
server = self._boot_a_server(
extra_params={'networks':
[{'port': self.neutron.port_1['id']}]})
self._attach_volume_to_server(server, self.cinder.SWAP_OLD_VOL)
self.cinder.swap_volume_instance_uuid = server['id']
self._volume_swap_server(server, self.cinder.SWAP_OLD_VOL,
self.cinder.SWAP_NEW_VOL)
self._wait_until_swap_volume(server, self.cinder.SWAP_NEW_VOL)
self.assertEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS))
self._verify_notification(
self.EVENT_TYPE_SWAP_VOL_START,
replacements={
'reservation_id': server['reservation_id'],
'uuid': server['id']},
actual=fake_notifier.VERSIONED_NOTIFICATIONS[0])
self._verify_notification(
self.EVENT_TYPE_SWAP_VOL_END,
replacements={
'reservation_id': server['reservation_id'],
'uuid': server['id']},
actual=fake_notifier.VERSIONED_NOTIFICATIONS[1])

View File

@ -44,6 +44,7 @@ from nova.network import api as network_api
from nova.network import model as network_model
from nova import objects
from nova.objects import block_device as block_device_obj
from nova.objects import fields
from nova.objects import instance as instance_obj
from nova.objects import migrate_data as migrate_data_obj
from nova.objects import network_request as net_req_obj
@ -1741,7 +1742,8 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
do_test()
def test_swap_volume_volume_api_usage(self):
@mock.patch.object(compute_utils, 'notify_about_volume_swap')
def test_swap_volume_volume_api_usage(self, mock_notify):
# This test ensures that volume_id arguments are passed to volume_api
# and that volume states are OK
volumes = {}
@ -1825,34 +1827,65 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
'_instance_update', lambda c, u, **k: {})
# Good path
instance1 = fake_instance.fake_instance_obj(
self.context, **{'uuid': uuids.instance})
self.compute.swap_volume(self.context, old_volume_id, new_volume_id,
fake_instance.fake_instance_obj(
self.context, **{'uuid': uuids.instance}))
instance1)
self.assertEqual(volumes[old_volume_id]['status'], 'in-use')
self.assertEqual(2, mock_notify.call_count)
mock_notify.assert_any_call(test.MatchType(context.RequestContext),
instance1, self.compute.host,
fields.NotificationAction.VOLUME_SWAP,
fields.NotificationPhase.START,
old_volume_id, new_volume_id)
mock_notify.assert_any_call(test.MatchType(context.RequestContext),
instance1, self.compute.host,
fields.NotificationAction.VOLUME_SWAP,
fields.NotificationPhase.END,
old_volume_id, new_volume_id)
# Error paths
mock_notify.reset_mock()
volumes[old_volume_id]['status'] = 'detaching'
volumes[new_volume_id]['status'] = 'attaching'
self.stub_out('nova.virt.fake.FakeDriver.swap_volume',
fake_func_exc)
instance2 = fake_instance.fake_instance_obj(
self.context, **{'uuid': uuids.instance})
self.assertRaises(AttributeError, self.compute.swap_volume,
self.context, old_volume_id, new_volume_id,
fake_instance.fake_instance_obj(
self.context, **{'uuid': uuids.instance}))
instance2)
self.assertEqual(volumes[old_volume_id]['status'], 'in-use')
self.assertEqual(volumes[new_volume_id]['status'], 'available')
self.assertEqual(1, mock_notify.call_count)
mock_notify.assert_called_once_with(
test.MatchType(context.RequestContext), instance2,
self.compute.host,
fields.NotificationAction.VOLUME_SWAP,
fields.NotificationPhase.START,
old_volume_id, new_volume_id)
mock_notify.reset_mock()
volumes[old_volume_id]['status'] = 'detaching'
volumes[new_volume_id]['status'] = 'attaching'
self.stub_out('nova.volume.cinder.API.initialize_connection',
fake_func_exc)
instance3 = fake_instance.fake_instance_obj(
self.context, **{'uuid': uuids.instance})
self.assertRaises(AttributeError, self.compute.swap_volume,
self.context, old_volume_id, new_volume_id,
fake_instance.fake_instance_obj(
self.context, **{'uuid': uuids.instance}))
instance3)
self.assertEqual(volumes[old_volume_id]['status'], 'in-use')
self.assertEqual(volumes[new_volume_id]['status'], 'available')
self.assertEqual(1, mock_notify.call_count)
mock_notify.assert_called_once_with(
test.MatchType(context.RequestContext), instance3,
self.compute.host,
fields.NotificationAction.VOLUME_SWAP,
fields.NotificationPhase.START,
old_volume_id, new_volume_id)
@mock.patch('nova.compute.utils.notify_about_volume_swap')
@mock.patch('nova.db.block_device_mapping_get_by_instance_and_volume_id')
@mock.patch('nova.db.block_device_mapping_update')
@mock.patch('nova.volume.cinder.API.get')
@ -1862,7 +1895,8 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
volume_connector_mock,
get_volume_mock,
update_bdm_mock,
get_bdm_mock):
get_bdm_mock,
notify_mock):
# This test ensures that delete_on_termination flag arguments
# are reserved
volumes = {}

View File

@ -38,6 +38,7 @@ from nova.network import model
from nova import objects
from nova.objects import base
from nova.objects import block_device as block_device_obj
from nova.objects import fields
from nova import rpc
from nova import test
from nova.tests.unit import fake_block_device
@ -519,6 +520,44 @@ class UsageInfoTestCase(test.TestCase):
self.assertEqual(payload['image_uuid'], uuids.fake_image_ref)
def test_notify_about_volume_swap(self):
instance = create_instance(self.context)
compute_utils.notify_about_volume_swap(
self.context, instance, 'fake-compute',
fields.NotificationAction.VOLUME_SWAP,
fields.NotificationPhase.START,
uuids.old_volume_id, uuids.new_volume_id)
self.assertEqual(len(fake_notifier.VERSIONED_NOTIFICATIONS), 1)
notification = fake_notifier.VERSIONED_NOTIFICATIONS[0]
self.assertEqual('INFO', notification['priority'])
self.assertEqual('instance.%s.%s' %
(fields.NotificationAction.VOLUME_SWAP,
fields.NotificationPhase.START),
notification['event_type'])
self.assertEqual('nova-compute:fake-compute',
notification['publisher_id'])
payload = notification['payload']['nova_object.data']
self.assertEqual(self.project_id, payload['tenant_id'])
self.assertEqual(self.user_id, payload['user_id'])
self.assertEqual(instance['uuid'], payload['uuid'])
flavorid = flavors.get_flavor_by_name('m1.tiny')['flavorid']
flavor = payload['flavor']['nova_object.data']
self.assertEqual(flavorid, str(flavor['flavorid']))
for attr in ('display_name', 'created_at', 'launched_at',
'state', 'task_state'):
self.assertIn(attr, payload)
self.assertEqual(uuids.fake_image_ref, payload['image_uuid'])
self.assertEqual(uuids.old_volume_id, payload['old_volume_id'])
self.assertEqual(uuids.new_volume_id, payload['new_volume_id'])
def test_notify_usage_exists_instance_not_found(self):
# Ensure 'exists' notification generates appropriate usage data.
instance = create_instance(self.context)

View File

@ -265,6 +265,9 @@ notification_object_data = {
'FlavorPayload': '1.0-8ad962ab0bafc7270f474c7dda0b7c20',
'InstanceActionNotification': '1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceActionPayload': '1.0-d94994d6043bb87fde603976ce811cba',
'InstanceActionVolumeSwapNotification':
'1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceActionVolumeSwapPayload': '1.0-1043222e12be67403cab471ea1989b76',
'InstancePayload': '1.0-4473793aa2a0a4083d328847f3ab638a',
'InstanceStateUpdatePayload': '1.0-a934d04e1b314318e42e8062647edd11',
'InstanceUpdateNotification': '1.0-a73147b93b520ff0061865849d3dfa56',

View File

@ -0,0 +1,8 @@
---
features:
- |
The following versioned swap volume notifications have been added
in the compute manager:
* instance.volume_swap.start
* instance.volume_swap.end