Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
9777be8
commit 2606bc6
Showing
2 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) |