Merge "Add OS::Barbican::*Container"

This commit is contained in:
Jenkins 2016-02-11 13:25:58 +00:00 committed by Gerrit Code Review
commit 4686c5d2f2
6 changed files with 560 additions and 3 deletions

View File

@ -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,)

View File

@ -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
}

View File

@ -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))

View File

@ -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

View File

@ -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))

View File

@ -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