diff --git a/etc/heat/policy.json b/etc/heat/policy.json index f95144aacb..5833f30a52 100644 --- a/etc/heat/policy.json +++ b/etc/heat/policy.json @@ -57,6 +57,7 @@ "stacks:show_snapshot": "rule:deny_stack_user", "stacks:delete_snapshot": "rule:deny_stack_user", "stacks:list_snapshots": "rule:deny_stack_user", + "stacks:restore_snapshot": "rule:deny_stack_user", "software_configs:create": "rule:deny_stack_user", "software_configs:show": "rule:deny_stack_user", diff --git a/heat/api/openstack/v1/__init__.py b/heat/api/openstack/v1/__init__.py index 862a474fa0..b3a395142f 100644 --- a/heat/api/openstack/v1/__init__.py +++ b/heat/api/openstack/v1/__init__.py @@ -143,6 +143,12 @@ class API(wsgi.Router): action="list_snapshots", conditions={'method': 'GET'}) + stack_mapper.connect("stack_snapshot_restore", + "/stacks/{stack_name}/{stack_id}/snapshots/" + "{snapshot_id}/restore", + action="restore_snapshot", + conditions={'method': 'POST'}) + # Resources resources_resource = resources.create_resource(conf) stack_path = "/{tenant_id}/stacks/{stack_name}/{stack_id}" diff --git a/heat/api/openstack/v1/stacks.py b/heat/api/openstack/v1/stacks.py index 89cfb4ed86..6e7aa803d4 100644 --- a/heat/api/openstack/v1/stacks.py +++ b/heat/api/openstack/v1/stacks.py @@ -464,6 +464,11 @@ class StackController(object): req.context, identity) } + @util.identified_stack + def restore_snapshot(self, req, identity, snapshot_id): + self.rpc_client.stack_restore(req.context, identity, snapshot_id) + raise exc.HTTPAccepted() + class StackSerializer(serializers.JSONResponseSerializer): """Handles serialization of specific controller method responses.""" diff --git a/heat/engine/clients/os/cinder.py b/heat/engine/clients/os/cinder.py index 5a047e25a4..dcc8323249 100644 --- a/heat/engine/clients/os/cinder.py +++ b/heat/engine/clients/os/cinder.py @@ -20,13 +20,13 @@ from keystoneclient import exceptions as ks_exceptions from heat.common import exception from heat.common.i18n import _ from heat.common.i18n import _LI -from heat.engine import clients +from heat.engine.clients import client_plugin LOG = logging.getLogger(__name__) -class CinderClientPlugin(clients.client_plugin.ClientPlugin): +class CinderClientPlugin(client_plugin.ClientPlugin): exceptions_module = exceptions diff --git a/heat/engine/service.py b/heat/engine/service.py index d62c49d8de..d4eeac389e 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -1245,6 +1245,20 @@ class EngineService(service.Service): self.thread_group_mgr.start_with_lock(cnxt, stack, self.engine_id, stack.check) + @request_context + def stack_restore(self, cnxt, stack_identity, snapshot_id): + def _stack_restore(stack, snapshot): + LOG.debug("restoring stack %s" % stack.name) + stack.restore(snapshot) + + s = self._get_stack(cnxt, stack_identity) + snapshot = db_api.snapshot_get(cnxt, snapshot_id) + + stack = parser.Stack.load(cnxt, stack=s) + + self.thread_group_mgr.start_with_lock(cnxt, stack, self.engine_id, + _stack_restore, stack, snapshot) + @request_context def stack_list_snapshots(self, cnxt, stack_identity): s = self._get_stack(cnxt, stack_identity) diff --git a/heat/engine/stack.py b/heat/engine/stack.py index 5fe4d80c7b..2bb8e69c57 100644 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -1061,6 +1061,10 @@ class Stack(collections.Mapping): ''' Restore the given snapshot, invoking handle_restore on all resources. ''' + if snapshot.stack_id != self.id: + self.state_set(self.RESTORE, self.FAILED, + "Can't restore snapshot from other stack") + return self.updated_time = datetime.utcnow() tmpl = Template(snapshot.data['template']) diff --git a/heat/rpc/client.py b/heat/rpc/client.py index b827210dfc..18b36284bb 100644 --- a/heat/rpc/client.py +++ b/heat/rpc/client.py @@ -526,3 +526,8 @@ class EngineClient(object): def stack_list_snapshots(self, cnxt, stack_identity): return self.call(cnxt, self.make_msg('stack_list_snapshots', stack_identity=stack_identity)) + + def stack_restore(self, cnxt, stack_identity, snapshot_id): + return self.call(cnxt, self.make_msg('stack_restore', + stack_identity=stack_identity, + snapshot_id=snapshot_id)) diff --git a/heat/tests/test_api_openstack_v1.py b/heat/tests/test_api_openstack_v1.py index f3c71aa7b7..80594e5045 100644 --- a/heat/tests/test_api_openstack_v1.py +++ b/heat/tests/test_api_openstack_v1.py @@ -3250,6 +3250,19 @@ class RoutesTest(common.HeatTestCase): 'stack_id': 'bbbb' }) + self.assertRoute( + self.m, + '/aaaa/stacks/teststack/bbbb/snapshots/cccc/restore', + 'POST', + 'restore_snapshot', + 'StackController', + { + 'tenant_id': 'aaaa', + 'stack_name': 'teststack', + 'stack_id': 'bbbb', + 'snapshot_id': 'cccc' + }) + def test_stack_data_template(self): self.assertRoute( self.m, diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index a019c00573..eebc6b0b4a 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -3562,13 +3562,13 @@ class SnapshotServiceTest(common.HeatTestCase): utils.setup_dummy_db() self.addCleanup(self.m.VerifyAll) - def _create_stack(self): + def _create_stack(self, stub=True): stack = get_wordpress_stack('stack', self.ctx) sid = stack.store() s = db_api.stack_get(self.ctx, sid) - self.m.StubOutWithMock(parser.Stack, 'load') - + if stub: + self.m.StubOutWithMock(parser.Stack, 'load') parser.Stack.load(self.ctx, stack=s).MultipleTimes().AndReturn(stack) return stack @@ -3636,3 +3636,28 @@ class SnapshotServiceTest(common.HeatTestCase): "status_reason": "Stack SNAPSHOT completed successfully", "data": stack.prepare_abandon()} self.assertEqual([expected], snapshots) + + def test_restore_snapshot(self): + stack = self._create_stack() + self.m.ReplayAll() + snapshot = self.engine.stack_snapshot( + self.ctx, stack.identifier(), 'snap1') + self.engine.thread_group_mgr.groups[stack.id].wait() + snapshot_id = snapshot['id'] + self.engine.stack_restore(self.ctx, stack.identifier(), snapshot_id) + self.engine.thread_group_mgr.groups[stack.id].wait() + self.assertEqual((stack.RESTORE, stack.COMPLETE), stack.state) + + def test_restore_snapshot_other_stack(self): + stack1 = self._create_stack() + stack2 = self._create_stack(stub=False) + self.m.ReplayAll() + snapshot1 = self.engine.stack_snapshot( + self.ctx, stack1.identifier(), 'snap1') + self.engine.thread_group_mgr.groups[stack1.id].wait() + snapshot_id = snapshot1['id'] + self.engine.stack_restore(self.ctx, stack2.identifier(), snapshot_id) + self.engine.thread_group_mgr.groups[stack2.id].wait() + self.assertEqual((stack2.RESTORE, stack2.FAILED), stack2.state) + self.assertEqual("Can't restore snapshot from other stack", + stack2.status_reason) diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index 42bb39b434..1bccb8fa03 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -4335,7 +4335,8 @@ class StackTest(common.HeatTestCase): self.stack.create() data = copy.deepcopy(self.stack.prepare_abandon()) - fake_snapshot = collections.namedtuple('Snapshot', ('data',))(data) + fake_snapshot = collections.namedtuple( + 'Snapshot', ('data', 'stack_id'))(data, self.stack.id) new_tmpl = {'HeatTemplateFormatVersion': '2012-12-12', 'Resources': {'A': {'Type': 'GenericResourceType'}}} @@ -4374,7 +4375,8 @@ class StackTest(common.HeatTestCase): data = self.stack.prepare_abandon() data['resources']['A']['resource_data']['a_string'] = 'foo' - fake_snapshot = collections.namedtuple('Snapshot', ('data',))(data) + fake_snapshot = collections.namedtuple( + 'Snapshot', ('data', 'stack_id'))(data, self.stack.id) self.stack.restore(fake_snapshot) diff --git a/heat/tests/test_server.py b/heat/tests/test_server.py index 646fcc83be..2926da60b1 100644 --- a/heat/tests/test_server.py +++ b/heat/tests/test_server.py @@ -2750,7 +2750,8 @@ class ServersTest(common.HeatTestCase): data = stack.prepare_abandon() resource_data = data['resources']['WebServer']['resource_data'] resource_data['snapshot_image_id'] = 'CentOS 5.2' - fake_snapshot = collections.namedtuple('Snapshot', ('data',))(data) + fake_snapshot = collections.namedtuple( + 'Snapshot', ('data', 'stack_id'))(data, stack.id) stack.restore(fake_snapshot) diff --git a/heat/tests/test_volume.py b/heat/tests/test_volume.py index 8d19d733aa..b10c8705ef 100644 --- a/heat/tests/test_volume.py +++ b/heat/tests/test_volume.py @@ -1473,7 +1473,8 @@ class CinderVolumeTest(BaseVolumeTest): self.assertEqual((stack.SNAPSHOT, stack.COMPLETE), stack.state) data = stack.prepare_abandon() - fake_snapshot = collections.namedtuple('Snapshot', ('data',))(data) + fake_snapshot = collections.namedtuple( + 'Snapshot', ('data', 'stack_id'))(data, stack.id) stack.restore(fake_snapshot)