diff --git a/heat/engine/clients/os/barbican.py b/heat/engine/clients/os/barbican.py index 541f8f972a..764b14ffd7 100644 --- a/heat/engine/clients/os/barbican.py +++ b/heat/engine/clients/os/barbican.py @@ -10,12 +10,13 @@ # 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 barbicanclient import client as barbican_client +from barbicanclient import containers import six +from heat.common import exception from heat.engine.clients import client_plugin - - -from barbicanclient import client as barbican_client +from heat.engine import constraints CLIENT_NAME = 'barbican' @@ -38,3 +39,32 @@ class BarbicanClientPlugin(client_plugin.ClientPlugin): # This is the only exception the client raises # Inspecting the message to see if it's a 'Not Found' return 'Not Found' in six.text_type(ex) + + def create_generic_container(self, **props): + return containers.Container( + self.client().containers._api, **props) + + def create_certificate(self, **props): + return containers.CertificateContainer( + self.client().containers._api, **props) + + def create_rsa(self, **props): + return containers.RSAContainer( + self.client().containers._api, **props) + + def get_secret_by_ref(self, secret_ref): + try: + return self.client().secrets.get( + secret_ref)._get_formatted_entity() + except Exception as ex: + if self.is_not_found(ex): + raise exception.EntityNotFound( + entity="Secret", + name=secret_ref) + raise ex + + +class SecretConstraint(constraints.BaseCustomConstraint): + resource_client_name = CLIENT_NAME + resource_getter_name = 'get_secret_by_ref' + expected_exceptions = (exception.EntityNotFound,) diff --git a/heat/engine/resources/openstack/barbican/container.py b/heat/engine/resources/openstack/barbican/container.py new file mode 100644 index 0000000000..106c48a79b --- /dev/null +++ b/heat/engine/resources/openstack/barbican/container.py @@ -0,0 +1,260 @@ +# +# 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 six + +from heat.common import exception +from heat.common.i18n import _ +from heat.engine import attributes +from heat.engine import constraints +from heat.engine import properties +from heat.engine import resource +from heat.engine import support + + +class GenericContainer(resource.Resource): + """A resource for creating Barbican generic container. + + A generic container is used for any type of secret that a user + may wish to aggregate. There are no restrictions on the amount + of secrets that can be held within this container. + """ + + support_status = support.SupportStatus(version='6.0.0') + + default_client_name = 'barbican' + + entity = 'containers' + + PROPERTIES = ( + NAME, SECRETS, + ) = ( + 'name', 'secrets', + ) + + ATTRIBUTES = ( + STATUS, CONTAINER_REF, SECRET_REFS, CONSUMERS, + ) = ( + 'status', 'container_ref', 'secret_refs', 'consumers', + ) + + _SECRETS_PROPERTIES = ( + NAME, REF, + ) = ( + 'name', 'ref' + ) + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('Human-readable name for the container.'), + ), + SECRETS: properties.Schema( + properties.Schema.LIST, + _('References to secrets that will be stored in container.'), + schema=properties.Schema( + properties.Schema.MAP, + schema={ + NAME: properties.Schema( + properties.Schema.STRING, + _('Name of the secret.'), + required=True + ), + REF: properties.Schema( + properties.Schema.STRING, + _('Reference to the secret.'), + required=True, + constraints=[constraints.CustomConstraint( + 'barbican.secret')], + ), + } + ), + ), + } + + attributes_schema = { + STATUS: attributes.Schema( + _('The status of the container.'), + type=attributes.Schema.STRING + ), + CONTAINER_REF: attributes.Schema( + _('The URI to the container.'), + type=attributes.Schema.STRING + ), + SECRET_REFS: attributes.Schema( + _('The URIs to secrets stored in container.'), + type=attributes.Schema.MAP + ), + CONSUMERS: attributes.Schema( + _('The URIs to container consumers.'), + type=attributes.Schema.LIST + ), + } + + def get_refs(self): + secrets = self.properties.get(self.SECRETS) or [] + return [secret['ref'] for secret in secrets] + + def validate(self): + super(GenericContainer, self).validate() + refs = self.get_refs() + if len(refs) != len(set(refs)): + msg = _("Duplicate refs are not allowed.") + raise exception.StackValidationFailed(message=msg) + + def create_container(self): + if self.properties[self.SECRETS]: + secrets = dict((secret['name'], secret['ref']) + for secret in self.properties[self.SECRETS]) + else: + secrets = {} + info = {'secret_refs': secrets} + if self.properties[self.NAME] is not None: + info.update({'name': self.properties[self.NAME]}) + return self.client_plugin().create_generic_container(**info) + + def handle_create(self): + container_ref = self.create_container().store() + self.resource_id_set(container_ref) + return container_ref + + def check_create_complete(self, container_href): + container = self.client().containers.get(container_href) + + if container.status == 'ERROR': + reason = container.error_reason + code = container.error_status_code + msg = (_("Container '%(name)s' creation failed: " + "%(code)s - %(reason)s") + % {'name': self.name, 'code': code, 'reason': reason}) + raise exception.ResourceInError( + status_reason=msg, resource_status=container.status) + + return container.status == 'ACTIVE' + + def _resolve_attribute(self, name): + container = self.client().containers.get(self.resource_id) + return getattr(container, name, None) + + # TODO(ochuprykov): remove this method when bug #1485619 will be fixed + def _show_resource(self): + container = self.client().containers.get(self.resource_id) + info = container._get_formatted_entity() + return dict(zip(info[0], info[1])) + + +class CertificateContainer(GenericContainer): + """A resource for creating barbican certificate container. + + A certificate container is used for storing the secrets that + are relevant to certificates. + """ + + PROPERTIES = ( + NAME, CERTIFICATE_REF, PRIVATE_KEY_REF, + PRIVATE_KEY_PASSPHRASE_REF, INTERMEDIATES_REF, + ) = ( + 'name', 'certificate_ref', 'private_key_ref', + 'private_key_passphrase_ref', 'intermediates_ref', + ) + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('Human-readable name for the container.'), + ), + CERTIFICATE_REF: properties.Schema( + properties.Schema.STRING, + _('Reference to certificate.'), + constraints=[constraints.CustomConstraint('barbican.secret')], + ), + PRIVATE_KEY_REF: properties.Schema( + properties.Schema.STRING, + _('Reference to private key.'), + constraints=[constraints.CustomConstraint('barbican.secret')], + ), + PRIVATE_KEY_PASSPHRASE_REF: properties.Schema( + properties.Schema.STRING, + _('Reference to private key passphrase.'), + constraints=[constraints.CustomConstraint('barbican.secret')], + ), + INTERMEDIATES_REF: properties.Schema( + properties.Schema.STRING, + _('Reference to intermediates.'), + constraints=[constraints.CustomConstraint('barbican.secret')], + ), + } + + def create_container(self): + info = dict((k, v) for k, v in six.iteritems(self.properties) + if v is not None) + return self.client_plugin().create_certificate(**info) + + def get_refs(self): + return [v for k, v in six.iteritems(self.properties) + if (k != self.NAME and v is not None)] + + +class RSAContainer(GenericContainer): + """A resource for creating barbican RSA container. + + An RSA container is used for storing RSA public keys, private keys, + and private key pass phrases. + """ + + PROPERTIES = ( + NAME, PRIVATE_KEY_REF, PRIVATE_KEY_PASSPHRASE_REF, + PUBLIC_KEY_REF, + ) = ( + 'name', 'private_key_ref', 'private_key_passphrase_ref', + 'public_key_ref', + ) + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('Human-readable name for the container.'), + ), + PRIVATE_KEY_REF: properties.Schema( + properties.Schema.STRING, + _('Reference to private key.'), + constraints=[constraints.CustomConstraint('barbican.secret')], + ), + PRIVATE_KEY_PASSPHRASE_REF: properties.Schema( + properties.Schema.STRING, + _('Reference to private key passphrase.'), + constraints=[constraints.CustomConstraint('barbican.secret')], + ), + PUBLIC_KEY_REF: properties.Schema( + properties.Schema.STRING, + _('Reference to public key.'), + constraints=[constraints.CustomConstraint('barbican.secret')], + ), + } + + def create_container(self): + info = dict((k, v) for k, v in six.iteritems(self.properties) + if v is not None) + return self.client_plugin().create_rsa(**info) + + def get_refs(self): + return [v for k, v in six.iteritems(self.properties) + if (k != self.NAME and v is not None)] + + +def resource_mapping(): + return { + 'OS::Barbican::GenericContainer': GenericContainer, + 'OS::Barbican::CertificateContainer': CertificateContainer, + 'OS::Barbican::RSAContainer': RSAContainer + } diff --git a/heat/tests/clients/test_barbican_client.py b/heat/tests/clients/test_barbican_client.py index 24d8c6efa0..91d6136f4c 100644 --- a/heat/tests/clients/test_barbican_client.py +++ b/heat/tests/clients/test_barbican_client.py @@ -11,14 +11,61 @@ # License for the specific language governing permissions and limitations # under the License. +from barbicanclient import exceptions +import mock + +from heat.common import exception +from heat.engine.clients.os import barbican from heat.tests import common from heat.tests import utils class BarbicanClientPluginTest(common.HeatTestCase): + def setUp(self): + super(BarbicanClientPluginTest, self).setUp() + self.barbican_client = mock.MagicMock() + con = utils.dummy_context() + c = con.clients + self.barbican_plugin = c.client_plugin('barbican') + self.barbican_plugin._client = self.barbican_client + def test_create(self): context = utils.dummy_context() plugin = context.clients.client_plugin('barbican') client = plugin.client() self.assertIsNotNone(client.orders) + + def test_get_secret_by_ref(self): + self.barbican_client.secrets.get( + )._get_formatted_entity.return_value = {} + self.assertEqual({}, self.barbican_plugin.get_secret_by_ref("secret")) + + def test_get_secret_by_ref_not_found(self): + self.barbican_client.secrets.get( + )._get_formatted_entity.side_effect = exceptions.HTTPClientError( + message="Not Found") + self.assertRaises( + exception.EntityNotFound, + self.barbican_plugin.get_secret_by_ref, + "secret") + + +class SecretConstraintTest(common.HeatTestCase): + + def setUp(self): + super(SecretConstraintTest, self).setUp() + self.ctx = utils.dummy_context() + self.mock_get_secret_by_ref = mock.Mock() + self.ctx.clients.client_plugin( + 'barbican').get_secret_by_ref = self.mock_get_secret_by_ref + self.constraint = barbican.SecretConstraint() + + def test_validation(self): + self.mock_get_secret_by_ref.return_value = {} + self.assertTrue(self.constraint.validate("foo", self.ctx)) + + def test_validation_error(self): + self.mock_get_secret_by_ref.side_effect = exception.EntityNotFound( + entity='Secret', name='bar') + self.assertFalse(self.constraint.validate("bar", self.ctx)) diff --git a/heat/tests/common.py b/heat/tests/common.py index cdbf28ce91..2db304fddf 100644 --- a/heat/tests/common.py +++ b/heat/tests/common.py @@ -25,6 +25,7 @@ import testtools from heat.common import context from heat.common import messaging from heat.common import policy +from heat.engine.clients.os import barbican from heat.engine.clients.os import cinder from heat.engine.clients.os import glance from heat.engine.clients.os import keystone @@ -297,3 +298,7 @@ class HeatTestCase(testscenarios.WithScenarios, def stub_ProviderConstraint_validate(self): validate = self.patchobject(neutron.ProviderConstraint, 'validate') validate.return_value = True + + def stub_SecretConstraint_validate(self): + validate = self.patchobject(barbican.SecretConstraint, 'validate') + validate.return_value = True diff --git a/heat/tests/openstack/barbican/test_container.py b/heat/tests/openstack/barbican/test_container.py new file mode 100644 index 0000000000..4bb65ba383 --- /dev/null +++ b/heat/tests/openstack/barbican/test_container.py @@ -0,0 +1,214 @@ +# +# 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 +import six + +from heat.common import exception +from heat.common import template_format +from heat.engine.resources.openstack.barbican import container +from heat.engine import rsrc_defn +from heat.engine import scheduler +from heat.tests import common +from heat.tests import utils + +stack_template_generic = ''' +heat_template_version: 2015-10-15 +description: Test template +resources: + container: + type: OS::Barbican::GenericContainer + properties: + name: mynewcontainer + secrets: + - name: secret1 + ref: ref1 + - name: secret2 + ref: ref2 +''' + +stack_template_certificate = ''' +heat_template_version: 2015-10-15 +description: Test template +resources: + container: + type: OS::Barbican::CertificateContainer + properties: + name: mynewcontainer + certificate_ref: cref + private_key_ref: pkref + private_key_passphrase_ref: pkpref + intermediates_ref: iref +''' + +stack_template_rsa = ''' +heat_template_version: 2015-10-15 +description: Test template +resources: + container: + type: OS::Barbican::RSAContainer + properties: + name: mynewcontainer + private_key_ref: pkref + private_key_passphrase_ref: pkpref + public_key_ref: pubref +''' + + +def template_by_name(name='OS::Barbican::GenericContainer'): + mapping = {'OS::Barbican::GenericContainer': stack_template_generic, + 'OS::Barbican::CertificateContainer': + stack_template_certificate, + 'OS::Barbican::RSAContainer': stack_template_rsa} + return mapping[name] + + +class FakeContainer(object): + + def __init__(self, name): + self.name = name + + def store(self): + return self.name + + +class TestContainer(common.HeatTestCase): + + def setUp(self): + super(TestContainer, self).setUp() + utils.setup_dummy_db() + self.ctx = utils.dummy_context() + + self.patcher_client = mock.patch.object( + container.GenericContainer, 'client') + self.patcher_plugin = mock.patch.object( + container.GenericContainer, 'client_plugin') + mock_client = self.patcher_client.start() + self.client = mock_client.return_value + mock_plugin = self.patcher_plugin.start() + self.client_plugin = mock_plugin.return_value + self.stub_SecretConstraint_validate() + + def tearDown(self): + super(TestContainer, self).tearDown() + self.patcher_client.stop() + self.patcher_plugin.stop() + + def _create_resource(self, name, snippet=None, stack=None, + tmpl_name='OS::Barbican::GenericContainer'): + + tmpl = template_format.parse(template_by_name(tmpl_name)) + if stack is None: + self.stack = utils.parse_stack(tmpl) + else: + self.stack = stack + resource_defns = self.stack.t.resource_definitions(stack) + if snippet is None: + snippet = resource_defns['container'] + res_class = container.resource_mapping()[tmpl_name] + res = res_class(name, snippet, self.stack) + res.check_create_complete = mock.Mock(return_value=True) + create_generic_container = self.client_plugin.create_generic_container + create_generic_container.return_value = FakeContainer('generic') + self.client_plugin.create_certificate.return_value = FakeContainer( + 'certificate' + ) + self.client_plugin.create_rsa.return_value = FakeContainer('rsa') + scheduler.TaskRunner(res.create)() + return res + + def test_create_generic(self): + res = self._create_resource('foo') + expected_state = (res.CREATE, res.COMPLETE) + self.assertEqual(expected_state, res.state) + args = self.client_plugin.create_generic_container.call_args[1] + self.assertEqual('mynewcontainer', args['name']) + self.assertEqual({'secret1': 'ref1', 'secret2': 'ref2'}, + args['secret_refs']) + self.assertEqual(sorted(['ref1', 'ref2']), sorted(res.get_refs())) + + def test_create_certificate(self): + res = self._create_resource( + 'foo', tmpl_name='OS::Barbican::CertificateContainer') + expected_state = (res.CREATE, res.COMPLETE) + self.assertEqual(expected_state, res.state) + args = self.client_plugin.create_certificate.call_args[1] + self.assertEqual('mynewcontainer', args['name']) + self.assertEqual('cref', args['certificate_ref']) + self.assertEqual('pkref', args['private_key_ref']) + self.assertEqual('pkpref', args['private_key_passphrase_ref']) + self.assertEqual('iref', args['intermediates_ref']) + self.assertEqual(sorted(['pkref', 'pkpref', 'iref', 'cref']), + sorted(res.get_refs())) + + def test_create_rsa(self): + res = self._create_resource( + 'foo', tmpl_name='OS::Barbican::RSAContainer') + expected_state = (res.CREATE, res.COMPLETE) + self.assertEqual(expected_state, res.state) + args = self.client_plugin.create_rsa.call_args[1] + self.assertEqual('mynewcontainer', args['name']) + self.assertEqual('pkref', args['private_key_ref']) + self.assertEqual('pubref', args['public_key_ref']) + self.assertEqual('pkpref', args['private_key_passphrase_ref']) + self.assertEqual(sorted(['pkref', 'pubref', 'pkpref']), + sorted(res.get_refs())) + + def test_create_failed_on_validation(self): + tmpl = template_format.parse(template_by_name()) + stack = utils.parse_stack(tmpl) + props = tmpl['resources']['container']['properties'] + props['secrets'].append({'name': 'secret3', 'ref': 'ref1'}) + defn = rsrc_defn.ResourceDefinition( + 'failed_container', 'OS::Barbican::GenericContainer', props) + res = container.GenericContainer('foo', defn, stack) + self.assertRaisesRegexp(exception.StackValidationFailed, + 'Duplicate refs are not allowed', + res.validate) + + def test_attributes(self): + mock_container = mock.Mock() + mock_container.status = 'test-status' + mock_container.container_ref = 'test-container-ref' + mock_container.secret_refs = {'name': 'ref'} + mock_container.consumers = [{'name': 'name1', 'ref': 'ref1'}] + mock_container._get_formatted_entity.return_value = ( + ('attr', ), ('v',)) + res = self._create_resource('foo') + self.client.containers.get.return_value = mock_container + self.assertEqual('test-status', res.FnGetAtt('status')) + self.assertEqual('test-container-ref', res.FnGetAtt('container_ref')) + self.assertEqual({'name': 'ref'}, res.FnGetAtt('secret_refs')) + self.assertEqual([{'name': 'name1', 'ref': 'ref1'}], + res.FnGetAtt('consumers')) + self.assertEqual({'attr': 'v'}, res.FnGetAtt('show')) + + def test_check_create_complete(self): + tmpl = template_format.parse(template_by_name()) + stack = utils.parse_stack(tmpl) + resource_defns = stack.t.resource_definitions(stack) + res_template = resource_defns['container'] + res = container.GenericContainer('foo', res_template, stack) + mock_active = mock.Mock(status='ACTIVE') + self.client.containers.get.return_value = mock_active + self.assertTrue(res.check_create_complete('foo')) + mock_not_active = mock.Mock(status='PENDING') + self.client.containers.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.client.containers.get.return_value = mock_not_active + exc = self.assertRaises(exception.ResourceInError, + res.check_create_complete, 'foo') + self.assertIn('foo', six.text_type(exc)) + self.assertIn('500', six.text_type(exc)) diff --git a/setup.cfg b/setup.cfg index d1a852b163..dffe6b4a2b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -74,6 +74,7 @@ heat.clients = zaqar = heat.engine.clients.os.zaqar:ZaqarClientPlugin heat.constraints = + barbican.secret = heat.engine.clients.os.barbican:SecretConstraint nova.flavor = heat.engine.clients.os.nova:FlavorConstraint nova.host = heat.engine.clients.os.nova:HostConstraint nova.network = heat.engine.clients.os.nova:NetworkConstraint