From 984496a1e5d8cfe17ed61b61c336b93861928189 Mon Sep 17 00:00:00 2001 From: Thomas Herve Date: Thu, 2 May 2013 15:13:52 +0200 Subject: [PATCH] Support Snapshot policy in volumes This handles the Snapshot DeletionPolicy in the AWS::EC2::Volume resource, creating a backup of the volume before deleting it. It requires a recent cinder client with backup support. Implements: blueprint volume-snapshots Change-Id: Idbb14a434ad6ccde9f198e07799e8aef0867dd6f --- heat/engine/resource.py | 18 ++-- heat/engine/resources/volume.py | 16 ++++ heat/tests/test_validate.py | 27 ++++++ heat/tests/test_volume.py | 144 +++++++++++++++++++++++++++----- 4 files changed, 177 insertions(+), 28 deletions(-) diff --git a/heat/engine/resource.py b/heat/engine/resource.py index 7728df02..92263211 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -403,18 +403,16 @@ class Resource(object): self.validate_deletion_policy(self.t) return self.properties.validate() - @staticmethod - def validate_deletion_policy(template): + @classmethod + def validate_deletion_policy(cls, template): deletion_policy = template.get('DeletionPolicy', 'Delete') if deletion_policy not in ('Delete', 'Retain', 'Snapshot'): msg = 'Invalid DeletionPolicy %s' % deletion_policy raise exception.StackValidationFailed(message=msg) elif deletion_policy == 'Snapshot': - # Some resources will support it in the future, in which case we - # should check for the presence of a handle_snapshot method for - # example. - msg = 'Snapshot DeletionPolicy not supported' - raise exception.StackValidationFailed(message=msg) + if not callable(getattr(cls, 'handle_snapshot', None)): + msg = 'Snapshot DeletionPolicy not supported' + raise exception.StackValidationFailed(message=msg) def delete(self): ''' @@ -434,9 +432,13 @@ class Resource(object): try: self.state_set(self.DELETE_IN_PROGRESS) - if self.t.get('DeletionPolicy', 'Delete') == 'Delete': + deletion_policy = self.t.get('DeletionPolicy', 'Delete') + if deletion_policy == 'Delete': if callable(getattr(self, 'handle_delete', None)): self.handle_delete() + elif deletion_policy == 'Snapshot': + if callable(getattr(self, 'handle_snapshot', None)): + self.handle_snapshot() except Exception as ex: logger.exception('Delete %s', str(self)) failure = exception.ResourceFailure(ex) diff --git a/heat/engine/resources/volume.py b/heat/engine/resources/volume.py index f4daa3bc..5f367b39 100644 --- a/heat/engine/resources/volume.py +++ b/heat/engine/resources/volume.py @@ -15,15 +15,19 @@ import eventlet from heat.openstack.common import log as logging +from heat.openstack.common.importutils import try_import from heat.common import exception from heat.engine import clients from heat.engine import resource +volume_backups = try_import('cinderclient.v1.volume_backups') + logger = logging.getLogger(__name__) class Volume(resource.Resource): + properties_schema = {'AvailabilityZone': {'Type': 'String', 'Required': True}, 'Size': {'Type': 'Number'}, @@ -50,6 +54,18 @@ class Volume(resource.Resource): def handle_update(self, json_snippet): return self.UPDATE_REPLACE + if volume_backups is not None: + def handle_snapshot(self): + if self.resource_id is not None: + # We use backups as snapshots are not independent of volumes + backup = self.cinder().backups.create(self.resource_id) + while backup.status == 'creating': + eventlet.sleep(1) + backup.get() + if backup.status != 'available': + raise exception.Error(backup.status) + self.handle_delete() + def handle_delete(self): if self.resource_id is not None: try: diff --git a/heat/tests/test_validate.py b/heat/tests/test_validate.py index d393a1bf..127623b7 100644 --- a/heat/tests/test_validate.py +++ b/heat/tests/test_validate.py @@ -323,6 +323,23 @@ test_template_snapshot_deletion_policy = ''' } ''' +test_template_volume_snapshot = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "test.", + "Resources" : { + "DataVolume" : { + "Type" : "AWS::EC2::Volume", + "DeletionPolicy": "Snapshot", + "Properties" : { + "Size" : "6", + "AvailabilityZone" : "nova" + } + } + } +} +''' + class validateTest(HeatTestCase): def setUp(self): @@ -457,3 +474,13 @@ class validateTest(HeatTestCase): res = dict(engine.validate_template(None, t)) self.assertEqual( res, {'Error': 'Snapshot DeletionPolicy not supported'}) + + def test_volume_snapshot_deletion_policy(self): + t = template_format.parse(test_template_volume_snapshot) + self.m.StubOutWithMock(instances.Instance, 'nova') + instances.Instance.nova().AndReturn(self.fc) + self.m.ReplayAll() + + engine = service.EngineService('a', 't') + res = dict(engine.validate_template(None, t)) + self.assertEqual(res, {'Description': u'test.', 'Parameters': {}}) diff --git a/heat/tests/test_volume.py b/heat/tests/test_volume.py index ef6cde43..9a28907f 100644 --- a/heat/tests/test_volume.py +++ b/heat/tests/test_volume.py @@ -24,20 +24,27 @@ from heat.engine import parser from heat.engine import scheduler from heat.engine.resources import volume as vol from heat.engine import clients +from heat.openstack.common.importutils import try_import from heat.tests.common import HeatTestCase from heat.tests.v1_1 import fakes -from heat.tests.utils import setup_dummy_db +from heat.tests.utils import setup_dummy_db, skip_if + +from cinderclient.v1 import client as cinderclient + + +volume_backups = try_import('cinderclient.v1.volume_backups') class VolumeTest(HeatTestCase): def setUp(self): super(VolumeTest, self).setUp() self.fc = fakes.FakeClient() + self.cinder_fc = cinderclient.Client('username', 'password') self.m.StubOutWithMock(clients.OpenStackClients, 'cinder') self.m.StubOutWithMock(clients.OpenStackClients, 'nova') - self.m.StubOutWithMock(self.fc.volumes, 'create') - self.m.StubOutWithMock(self.fc.volumes, 'get') - self.m.StubOutWithMock(self.fc.volumes, 'delete') + self.m.StubOutWithMock(self.cinder_fc.volumes, 'create') + self.m.StubOutWithMock(self.cinder_fc.volumes, 'get') + self.m.StubOutWithMock(self.cinder_fc.volumes, 'delete') self.m.StubOutWithMock(self.fc.volumes, 'create_server_volume') self.m.StubOutWithMock(self.fc.volumes, 'delete_server_volume') self.m.StubOutWithMock(eventlet, 'sleep') @@ -86,19 +93,20 @@ class VolumeTest(HeatTestCase): stack_name = 'test_volume_stack' # create script - clients.OpenStackClients.cinder().MultipleTimes().AndReturn(self.fc) - self.fc.volumes.create( + clients.OpenStackClients.cinder().MultipleTimes().AndReturn( + self.cinder_fc) + self.cinder_fc.volumes.create( u'1', display_description='%s.DataVolume' % stack_name, display_name='%s.DataVolume' % stack_name).AndReturn(fv) # delete script - self.fc.volumes.get('vol-123').AndReturn(fv) + self.cinder_fc.volumes.get('vol-123').AndReturn(fv) eventlet.sleep(1).AndReturn(None) - self.fc.volumes.get('vol-123').AndReturn(fv) - self.fc.volumes.delete('vol-123').AndReturn(None) + self.cinder_fc.volumes.get('vol-123').AndReturn(fv) + self.cinder_fc.volumes.delete('vol-123').AndReturn(None) - self.fc.volumes.get('vol-123').AndRaise( + self.cinder_fc.volumes.get('vol-123').AndRaise( clients.cinder_exceptions.NotFound('Not found')) self.m.ReplayAll() @@ -126,8 +134,8 @@ class VolumeTest(HeatTestCase): stack_name = 'test_volume_create_error_stack' # create script - clients.OpenStackClients.cinder().AndReturn(self.fc) - self.fc.volumes.create( + clients.OpenStackClients.cinder().AndReturn(self.cinder_fc) + self.cinder_fc.volumes.create( u'1', display_description='%s.DataVolume' % stack_name, display_name='%s.DataVolume' % stack_name).AndReturn(fv) @@ -152,14 +160,14 @@ class VolumeTest(HeatTestCase): stack_name = 'test_volume_attach_error_stack' # volume create - clients.OpenStackClients.cinder().MultipleTimes().AndReturn(self.fc) - self.fc.volumes.create( + clients.OpenStackClients.cinder().MultipleTimes().AndReturn( + self.cinder_fc) + self.cinder_fc.volumes.create( u'1', display_description='%s.DataVolume' % stack_name, display_name='%s.DataVolume' % stack_name).AndReturn(fv) # create script clients.OpenStackClients.nova().MultipleTimes().AndReturn(self.fc) -# clients.OpenStackClients.cinder().MultipleTimes().AndReturn(self.fc) eventlet.sleep(1).MultipleTimes().AndReturn(None) self.fc.volumes.create_server_volume( @@ -167,7 +175,7 @@ class VolumeTest(HeatTestCase): server_id=u'WikiDatabase', volume_id=u'vol-123').AndReturn(fva) - self.fc.volumes.get('vol-123').AndReturn(fva) + self.cinder_fc.volumes.get('vol-123').AndReturn(fva) self.m.ReplayAll() @@ -190,8 +198,9 @@ class VolumeTest(HeatTestCase): stack_name = 'test_volume_attach_stack' # volume create - clients.OpenStackClients.cinder().MultipleTimes().AndReturn(self.fc) - self.fc.volumes.create( + clients.OpenStackClients.cinder().MultipleTimes().AndReturn( + self.cinder_fc) + self.cinder_fc.volumes.create( u'1', display_description='%s.DataVolume' % stack_name, display_name='%s.DataVolume' % stack_name).AndReturn(fv) @@ -204,13 +213,13 @@ class VolumeTest(HeatTestCase): server_id=u'WikiDatabase', volume_id=u'vol-123').AndReturn(fva) - self.fc.volumes.get('vol-123').AndReturn(fva) + self.cinder_fc.volumes.get('vol-123').AndReturn(fva) # delete script fva = FakeVolume('in-use', 'available') self.fc.volumes.delete_server_volume('WikiDatabase', 'vol-123').AndReturn(None) - self.fc.volumes.get('vol-123').AndReturn(fva) + self.cinder_fc.volumes.get('vol-123').AndReturn(fva) self.m.ReplayAll() @@ -227,6 +236,96 @@ class VolumeTest(HeatTestCase): self.m.VerifyAll() + @skip_if(volume_backups is None, 'unable to import volume_backups') + def test_snapshot(self): + stack_name = 'test_volume_stack' + fv = FakeVolume('creating', 'available') + fb = FakeBackup('creating', 'available') + + # create script + clients.OpenStackClients.cinder().MultipleTimes().AndReturn( + self.cinder_fc) + self.cinder_fc.volumes.create( + u'1', display_description='%s.DataVolume' % stack_name, + display_name='%s.DataVolume' % stack_name).AndReturn(fv) + eventlet.sleep(1).AndReturn(None) + + # snapshot script + self.m.StubOutWithMock(self.cinder_fc.backups, 'create') + self.cinder_fc.backups.create('vol-123').AndReturn(fb) + eventlet.sleep(1).AndReturn(None) + self.cinder_fc.volumes.get('vol-123').AndReturn(fv) + self.cinder_fc.volumes.delete('vol-123').AndReturn(None) + self.m.ReplayAll() + + t = self.load_template() + t['Resources']['DataVolume']['DeletionPolicy'] = 'Snapshot' + stack = self.parse_stack(t, stack_name) + + resource = self.create_volume(t, stack, 'DataVolume') + + self.assertEqual(resource.destroy(), None) + + self.m.VerifyAll() + + @skip_if(volume_backups is None, 'unable to import volume_backups') + def test_snapshot_error(self): + stack_name = 'test_volume_stack' + fv = FakeVolume('creating', 'available') + fb = FakeBackup('creating', 'error') + + # create script + clients.OpenStackClients.cinder().MultipleTimes().AndReturn( + self.cinder_fc) + self.cinder_fc.volumes.create( + u'1', display_description='%s.DataVolume' % stack_name, + display_name='%s.DataVolume' % stack_name).AndReturn(fv) + eventlet.sleep(1).AndReturn(None) + + # snapshot script + self.m.StubOutWithMock(self.cinder_fc.backups, 'create') + self.cinder_fc.backups.create('vol-123').AndReturn(fb) + eventlet.sleep(1).AndReturn(None) + self.m.ReplayAll() + + t = self.load_template() + t['Resources']['DataVolume']['DeletionPolicy'] = 'Snapshot' + stack = self.parse_stack(t, stack_name) + + resource = self.create_volume(t, stack, 'DataVolume') + + self.assertRaises(exception.ResourceFailure, resource.destroy) + + self.m.VerifyAll() + + def test_snapshot_no_volume(self): + stack_name = 'test_volume_stack' + fv = FakeVolume('creating', 'error') + + # create script + clients.OpenStackClients.cinder().MultipleTimes().AndReturn( + self.cinder_fc) + self.cinder_fc.volumes.create( + u'1', display_description='%s.DataVolume' % stack_name, + display_name='%s.DataVolume' % stack_name).AndReturn(fv) + eventlet.sleep(1).AndReturn(None) + + self.m.ReplayAll() + + t = self.load_template() + t['Resources']['DataVolume']['DeletionPolicy'] = 'Snapshot' + stack = self.parse_stack(t, stack_name) + resource = vol.Volume('DataVolume', + t['Resources']['DataVolume'], + stack) + + create = scheduler.TaskRunner(resource.create) + self.assertRaises(exception.ResourceFailure, create) + + self.assertEqual(resource.destroy(), None) + + self.m.VerifyAll() + class FakeVolume: status = 'attaching' @@ -238,3 +337,8 @@ class FakeVolume: def get(self): self.status = self.final_status + + +class FakeBackup(FakeVolume): + status = 'creating' + id = 'backup-123' -- 2.45.2