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 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from barbicanclient import client as barbican_client
from barbicanclient import containers
import six import six
from heat.common import exception
from heat.engine.clients import client_plugin from heat.engine.clients import client_plugin
from heat.engine import constraints
from barbicanclient import client as barbican_client
CLIENT_NAME = 'barbican' CLIENT_NAME = 'barbican'
@ -38,3 +39,32 @@ class BarbicanClientPlugin(client_plugin.ClientPlugin):
# This is the only exception the client raises # This is the only exception the client raises
# Inspecting the message to see if it's a 'Not Found' # Inspecting the message to see if it's a 'Not Found'
return 'Not Found' in six.text_type(ex) 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 # License for the specific language governing permissions and limitations
# under the License. # 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 common
from heat.tests import utils from heat.tests import utils
class BarbicanClientPluginTest(common.HeatTestCase): 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): def test_create(self):
context = utils.dummy_context() context = utils.dummy_context()
plugin = context.clients.client_plugin('barbican') plugin = context.clients.client_plugin('barbican')
client = plugin.client() client = plugin.client()
self.assertIsNotNone(client.orders) 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 context
from heat.common import messaging from heat.common import messaging
from heat.common import policy 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 cinder
from heat.engine.clients.os import glance from heat.engine.clients.os import glance
from heat.engine.clients.os import keystone from heat.engine.clients.os import keystone
@ -297,3 +298,7 @@ class HeatTestCase(testscenarios.WithScenarios,
def stub_ProviderConstraint_validate(self): def stub_ProviderConstraint_validate(self):
validate = self.patchobject(neutron.ProviderConstraint, 'validate') validate = self.patchobject(neutron.ProviderConstraint, 'validate')
validate.return_value = True 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 zaqar = heat.engine.clients.os.zaqar:ZaqarClientPlugin
heat.constraints = heat.constraints =
barbican.secret = heat.engine.clients.os.barbican:SecretConstraint
nova.flavor = heat.engine.clients.os.nova:FlavorConstraint nova.flavor = heat.engine.clients.os.nova:FlavorConstraint
nova.host = heat.engine.clients.os.nova:HostConstraint nova.host = heat.engine.clients.os.nova:HostConstraint
nova.network = heat.engine.clients.os.nova:NetworkConstraint nova.network = heat.engine.clients.os.nova:NetworkConstraint