From 2606bc6ec6797d1167afae0c5866784ef1a4e787 Mon Sep 17 00:00:00 2001 From: Anderson Mesquita Date: Thu, 20 Mar 2014 13:06:58 -0500 Subject: [PATCH] Add OS:Barbican:Order resource This adds a Barbican Order resource to contrib plugins allowing orders for secrets to be issued using barbican's infrastructure. Implements: blueprint barbican-resources Change-Id: I8c9bc4bc2c1fecc9c3a5263af74e739dd9eea2ab --- contrib/barbican/barbican/resources/order.py | 163 ++++++++++++++++ contrib/barbican/barbican/tests/test_order.py | 180 ++++++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 contrib/barbican/barbican/resources/order.py create mode 100644 contrib/barbican/barbican/tests/test_order.py diff --git a/contrib/barbican/barbican/resources/order.py b/contrib/barbican/barbican/resources/order.py new file mode 100644 index 00000000000..6dfecb624fa --- /dev/null +++ b/contrib/barbican/barbican/resources/order.py @@ -0,0 +1,163 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from heat.common import exception +from heat.engine import attributes +from heat.engine import constraints +from heat.engine import properties +from heat.engine import resource +from heat.openstack.common import log as logging + +from .. import clients # noqa + + +LOG = logging.getLogger(__name__) + + +class Order(resource.Resource): + + PROPERTIES = ( + NAME, PAYLOAD_CONTENT_TYPE, MODE, EXPIRATION, + ALGORITHM, BIT_LENGTH, + ) = ( + 'name', 'payload_content_type', 'mode', 'expiration', + 'algorithm', 'bit_length', + ) + + ATTRIBUTES = ( + STATUS, ORDER_REF, SECRET_REF, + ) = ( + 'status', 'order_ref', 'secret_ref', + ) + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('Human readable name for the secret.'), + ), + PAYLOAD_CONTENT_TYPE: properties.Schema( + properties.Schema.STRING, + _('The type/format the secret data is provided in.'), + default='application/octet-stream', + constraints=[ + constraints.AllowedValues([ + 'application/octet-stream', + ]), + ], + ), + EXPIRATION: properties.Schema( + properties.Schema.STRING, + _('The expiration date for the secret in ISO-8601 format.'), + constraints=[ + constraints.CustomConstraint('iso_8601'), + ], + ), + ALGORITHM: properties.Schema( + properties.Schema.STRING, + _('The algorithm type used to generate the secret.'), + default='aes', + constraints=[ + constraints.AllowedValues([ + 'aes', + ]), + ], + ), + BIT_LENGTH: properties.Schema( + properties.Schema.NUMBER, + _('The bit-length of the secret.'), + constraints=[ + constraints.AllowedValues([ + 128, + 196, + 256, + ]), + ], + ), + MODE: properties.Schema( + properties.Schema.STRING, + _('The type/mode of the algorithm associated with the secret ' + 'information.'), + default='cbc', + constraints=[ + constraints.AllowedValues([ + 'cbc', + ]), + ], + ), + } + + attributes_schema = { + STATUS: attributes.Schema(_('The status of the order.')), + ORDER_REF: attributes.Schema(_('The URI to the order.')), + SECRET_REF: attributes.Schema(_('The URI to the created secret.')), + } + + def __init__(self, name, json_snippet, stack): + super(Order, self).__init__(name, json_snippet, stack) + self.clients = clients.Clients(self.context) + + def handle_create(self): + info = dict(self.properties) + order_ref = self.clients.barbican().orders.create(**info) + self.resource_id_set(order_ref) + return order_ref + + def check_create_complete(self, order_href): + order = self.clients.barbican().orders.get(order_href) + + if order.status == 'ERROR': + reason = order.error_reason + code = order.error_status_code + msg = (_("Order '%(name)s' failed: %(code)s - %(reason)s") + % {'name': self.name, 'code': code, 'reason': reason}) + raise exception.Error(msg) + + return order.status == 'ACTIVE' + + def handle_delete(self): + if not self.resource_id: + return + + try: + self.clients.barbican().orders.delete(self.resource_id) + self.resource_id_set(None) + except clients.barbican_client.HTTPClientError as exc: + # This is the only exception the client raises + # Inspecting the message to see if it's a 'Not Found' + if 'Not Found' in str(exc): + self.resource_id_set(None) + else: + raise + + def _resolve_attribute(self, name): + try: + order = self.clients.barbican().orders.get(self.resource_id) + except clients.barbican_client.HTTPClientError as exc: + LOG.warn(_("Order '%(name)s' not found: %(exc)s") % + {'name': self.resource_id, 'exc': str(exc)}) + return '' + + return getattr(order, name) + + +def resource_mapping(): + return { + 'OS::Barbican::Order': Order, + } + + +def available_resource_mapping(): + if not clients.barbican_client: + return {} + + return resource_mapping() diff --git a/contrib/barbican/barbican/tests/test_order.py b/contrib/barbican/barbican/tests/test_order.py new file mode 100644 index 00000000000..522c263498b --- /dev/null +++ b/contrib/barbican/barbican/tests/test_order.py @@ -0,0 +1,180 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 mock + +from heat.common import exception +from heat.common import template_format +from heat.engine import resource +from heat.engine import scheduler +from heat.tests.common import HeatTestCase +from heat.tests import utils + +from ..resources import order # noqa + + +stack_template = ''' +heat_template_version: 2013-05-23 +description: Test template +resources: + order: + type: OS::Barbican::Order + properties: + name: foobar-order + algorithm: aes + bit_length: 256 + mode: cbc +''' + + +class TestOrder(HeatTestCase): + + def setUp(self): + super(TestOrder, self).setUp() + utils.setup_dummy_db() + self.ctx = utils.dummy_context() + self.stack = utils.parse_stack(template_format.parse(stack_template)) + + self.res_template = self.stack.t['resources']['order'] + self.props = self.res_template['Properties'] + self._register_resources() + + self.patcher_client = mock.patch.object(order.clients, 'Clients') + mock_client = self.patcher_client.start() + self.barbican = mock_client.return_value.barbican.return_value + + def tearDown(self): + super(TestOrder, self).tearDown() + self.patcher_client.stop() + + def _register_resources(self): + for res_name, res_class in order.resource_mapping().iteritems(): + resource._register_class(res_name, res_class) + + def _create_resource(self, name, snippet, stack): + res = order.Order(name, snippet, stack) + res.check_create_complete = mock.Mock(return_value=True) + self.barbican.orders.create.return_value = name + scheduler.TaskRunner(res.create)() + return res + + def test_create_order(self): + res = self._create_resource('foo', self.res_template, self.stack) + expected_state = (res.CREATE, res.COMPLETE) + self.assertEqual(expected_state, res.state) + args = self.barbican.orders.create.call_args[1] + self.assertEqual('foobar-order', args['name']) + self.assertEqual('aes', args['algorithm']) + self.assertEqual('cbc', args['mode']) + self.assertEqual(256, args['bit_length']) + + def test_attributes(self): + mock_order = mock.Mock() + mock_order.status = 'test-status' + mock_order.order_ref = 'test-order-ref' + mock_order.secret_ref = 'test-secret-ref' + + self.barbican.orders.get.return_value = mock_order + res = self._create_resource('foo', self.res_template, self.stack) + + self.assertEqual('test-order-ref', res.FnGetAtt('order_ref')) + self.assertEqual('test-secret-ref', res.FnGetAtt('secret_ref')) + + @mock.patch.object(order.clients, 'barbican_client', new=mock.Mock()) + def test_attributes_handle_exceptions(self): + mock_order = mock.Mock() + self.barbican.orders.get.return_value = mock_order + res = self._create_resource('foo', self.res_template, self.stack) + + order.clients.barbican_client.HTTPClientError = Exception + not_found_exc = order.clients.barbican_client.HTTPClientError('boom') + self.barbican.orders.get.side_effect = not_found_exc + + self.assertEqual('', res.FnGetAtt('order_ref')) + + def test_create_order_sets_resource_id(self): + self.barbican.orders.create.return_value = 'foo' + res = self._create_resource('foo', self.res_template, self.stack) + + self.assertEqual('foo', res.resource_id) + + def test_create_order_defaults_to_octet_stream(self): + res = self._create_resource('foo', self.res_template, self.stack) + + args = self.barbican.orders.create.call_args[1] + self.assertEqual('application/octet-stream', + args[res.PAYLOAD_CONTENT_TYPE]) + + def test_create_order_with_octet_stream(self): + content_type = 'application/octet-stream' + self.props['payload_content_type'] = content_type + res = self._create_resource('foo', self.res_template, self.stack) + + args = self.barbican.orders.create.call_args[1] + self.assertEqual(content_type, args[res.PAYLOAD_CONTENT_TYPE]) + + def test_create_order_other_content_types_now_allowed(self): + self.props['payload_content_type'] = 'not/allowed' + res = order.Order('order', self.res_template, self.stack) + + self.assertRaises(exception.ResourceFailure, + scheduler.TaskRunner(res.create)) + + def test_delete_order(self): + self.barbican.orders.create.return_value = 'foo' + res = self._create_resource('foo', self.res_template, self.stack) + self.assertEqual('foo', res.resource_id) + + scheduler.TaskRunner(res.delete)() + self.assertIsNone(res.resource_id) + self.barbican.orders.delete.assert_called_once_with('foo') + + @mock.patch.object(order.clients, 'barbican_client', new=mock.Mock()) + def test_handle_delete_ignores_not_found_errors(self): + res = self._create_resource('foo', self.res_template, self.stack) + + order.clients.barbican_client.HTTPClientError = Exception + exc = order.clients.barbican_client.HTTPClientError('Not Found. Nope.') + self.barbican.orders.delete.side_effect = exc + scheduler.TaskRunner(res.delete)() + self.assertTrue(self.barbican.orders.delete.called) + + @mock.patch.object(order.clients, 'barbican_client', new=mock.Mock()) + def test_handle_delete_raises_resource_failure_on_error(self): + res = self._create_resource('foo', self.res_template, self.stack) + + order.clients.barbican_client.HTTPClientError = Exception + exc = order.clients.barbican_client.HTTPClientError('Boom.') + self.barbican.orders.delete.side_effect = exc + exc = self.assertRaises(exception.ResourceFailure, + scheduler.TaskRunner(res.delete)) + self.assertIn('Boom.', str(exc)) + + def test_check_create_complete(self): + res = order.Order('foo', self.res_template, self.stack) + + mock_active = mock.Mock(status='ACTIVE') + self.barbican.orders.get.return_value = mock_active + self.assertTrue(res.check_create_complete('foo')) + + mock_not_active = mock.Mock(status='PENDING') + self.barbican.orders.get.return_value = mock_not_active + self.assertFalse(res.check_create_complete('foo')) + + mock_not_active = mock.Mock(status='ERROR', error_reason='foo', + error_status_code=500) + self.barbican.orders.get.return_value = mock_not_active + exc = self.assertRaises(exception.Error, + res.check_create_complete, 'foo') + self.assertIn('foo', str(exc)) + self.assertIn('500', str(exc))