]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Support Snapshot policy in volumes
authorThomas Herve <therve@gmail.com>
Thu, 2 May 2013 13:13:52 +0000 (15:13 +0200)
committerThomas Herve <therve@gmail.com>
Fri, 3 May 2013 10:09:43 +0000 (12:09 +0200)
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
heat/engine/resources/volume.py
heat/tests/test_validate.py
heat/tests/test_volume.py

index 7728df021d30b3f29b0312bd6f64ce60bb926c32..92263211c99b74fa246ea0d8316078807e80bff6 100644 (file)
@@ -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)
index f4daa3bce1a31e30a5c6c54927b23d173054efc5..5f367b39c94dfee5b63ef949b4508422c0b8e5c2 100644 (file)
 
 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:
index d393a1bf8a7d9f26b66026855e866d5512e08ff4..127623b7b1df55b6e29ce746f2811ba1f9cd4f77 100644 (file)
@@ -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': {}})
index ef6cde43fc1896969829683d7ef4f0dfd47cad15..9a28907f6ac57d5b99d45b7a305bddf814fc6f4d 100644 (file)
@@ -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'