Merge "Support Snapshot policy in volumes"
This commit is contained in:
commit
f333d77084
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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': {}})
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue