Refactor Key Manager for resource2

This change updates the Key Manager service for the resource2/proxy2
refactoring. Along with making it work with the new classes, it improves
usability--at least temporarily--by exposing the ID value necessary from
the HREFs that the service returns. The HREF that gets returned, e.g.,
from a list call, is not directly usable to then pass it into a get
call. A more long-term fix for this would potentially be to create a Key
Manager specific base class that fiddles around with IDs and looks to
see if they are an HREF and converts them to a UUID in the proper
direction depending on where the data is going, but that's too much to
tackle in this refactoring change.

Besides updating some of the resources to match the documented
attributes, one feature this does add is retrieval of the Secret
payload, which is done via a separate endpoint. However, like other
calls in Glance and Heat, we unify them in the proxy's `get_secret` call
so the user doesn't need to know it's a separate call.

This also includes some basic docs in the user guide to show how the
different ID usage is currently necessary.

Change-Id: I8b5753e121d8f79350b38803e8aac95d7b4d1627
This commit is contained in:
Brian Curtin 2016-08-24 08:19:39 -04:00
parent ce8e5141e4
commit 0a19263fac
14 changed files with 399 additions and 67 deletions

View File

@ -6,4 +6,51 @@ connection to your OpenStack cloud by following the :doc:`connect` user
guide. This will provide you with the ``conn`` variable used in the examples
below.
.. TODO(thowe): Implement this guide
.. contents:: Table of Contents
:local:
.. note:: Some interactions with the Key Manager service differ from that
of other services in that resources do not have a proper ``id`` parameter,
which is necessary to make some calls. Instead, resources have a separately
named id attribute, e.g., the Secret resource has ``secret_id``.
The examples below outline when to pass in those id values.
Create a Secret
---------------
The Key Manager service allows you to create new secrets by passing the
attributes of the :class:`~openstack.key_manager.v1.secret.Secret` to the
:meth:`~openstack.key_manager.v1._proxy.Proxy.create_secret` method.
.. literalinclude:: ../examples/key_manager/create.py
:pyobject: create_secret
List Secrets
------------
Once you have stored some secrets, they are available for you to list
via the :meth:`~openstack.key_manager.v1._proxy.Proxy.secrets` method.
This method returns a generator, which yields each
:class:`~openstack.key_manager.v1.secret.Secret`.
.. literalinclude:: ../examples/key_manager/list.py
:pyobject: list_secrets
The :meth:`~openstack.key_manager.v1._proxy.Proxy.secrets` method can
also make more advanced queries to limit the secrets that are returned.
.. literalinclude:: ../examples/key_manager/list.py
:pyobject: list_secrets_query
Get Secret Payload
------------------
Once you have received a :class:`~openstack.key_manager.v1.secret.Secret`,
you can obtain the payload for it by passing the secret's id value to
the :meth:`~openstack.key_manager.v1._proxy.Proxy.secrets` method.
Use the :data:`~openstack.key_manager.v1.secret.Secret.secret_id` attribute
when making this request.
.. literalinclude:: ../examples/key_manager/get.py
:pyobject: get_secret_payload

View File

View File

@ -0,0 +1,25 @@
# 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.
"""
List resources from the Key Manager service.
"""
def create_secret(conn):
print("Create a secret:")
conn.key_manager.create_secret(name="My public key",
secret_type="public",
expiration="2020-02-28T23:59:59",
payload="ssh rsa...",
payload_content_type="text/plain")

View File

@ -0,0 +1,26 @@
# 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.
"""
List resources from the Key Manager service.
"""
s = None
def get_secret_payload(conn):
print("Get a secret's payload:")
# Assuming you have an object `s` which you perhaps received from
# a conn.key_manager.secrets() call...
secret = conn.key_manager.get_secret(s.secret_id)
print(secret.payload)

View File

@ -0,0 +1,31 @@
# 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.
"""
List resources from the Key Manager service.
"""
def list_secrets(conn):
print("List Secrets:")
for secret in conn.key_manager.secrets():
print(secret)
def list_secrets_query(conn):
print("List Secrets:")
for secret in conn.key_manager.secrets(
secret_type="symmetric",
expiration="gte:2020-01-01T00:00:00"):
print(secret)

View File

@ -0,0 +1,39 @@
# 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 openstack import format
from six.moves.urllib import parse
class HREFToUUID(format.Formatter):
@classmethod
def deserialize(cls, value):
"""Convert a HREF to the UUID portion"""
parts = parse.urlsplit(value)
# Only try to proceed if we have an actual URI.
# Just check that we have a scheme, netloc, and path.
if not all(parts[:3]):
raise ValueError("Unable to convert %s to an ID" % value)
# The UUID will be the last portion of the URI.
return parts.path.split("/")[-1]
@classmethod
def serialize(cls, value):
# NOTE(briancurtin): If we had access to the session to get
# the endpoint we could do something smart here like take an ID
# and give back an HREF, but this will just have to be something
# that works different because Barbican does what it does...
return value

View File

@ -13,10 +13,10 @@
from openstack.key_manager.v1 import container as _container
from openstack.key_manager.v1 import order as _order
from openstack.key_manager.v1 import secret as _secret
from openstack import proxy
from openstack import proxy2
class Proxy(proxy.BaseProxy):
class Proxy(proxy2.BaseProxy):
def create_container(self, **attrs):
"""Create a new container from attributes

View File

@ -11,34 +11,39 @@
# under the License.
from openstack.key_manager import key_manager_service
from openstack import resource
from openstack.key_manager.v1 import _format
from openstack import resource2
class Container(resource.Resource):
id_attribute = 'container_ref'
class Container(resource2.Resource):
resources_key = 'containers'
base_path = '/containers'
service = key_manager_service.KeyManagerService()
# capabilities
allow_create = True
allow_retrieve = True
allow_get = True
allow_update = True
allow_delete = True
allow_list = True
# Properties
#: A URI for this container
container_ref = resource.prop('container_ref')
container_ref = resource2.Body('container_ref')
#: The ID for this container
container_id = resource2.Body('container_ref', alternate_id=True,
type=_format.HREFToUUID)
#: The timestamp when this container was created.
created_at = resource.prop('created')
created_at = resource2.Body('created')
#: The name of this container
name = resource.prop('name')
name = resource2.Body('name')
#: A list of references to secrets in this container
secret_refs = resource.prop('secret_refs')
secret_refs = resource2.Body('secret_refs', type=list)
#: The status of this container
status = resource.prop('status')
status = resource2.Body('status')
#: The type of this container
type = resource.prop('type')
type = resource2.Body('type')
#: The timestamp when this container was updated.
updated_at = resource.prop('updated')
updated_at = resource2.Body('updated')
#: A party interested in this container.
consumers = resource2.Body('consumers', type=list)

View File

@ -11,34 +11,45 @@
# under the License.
from openstack.key_manager import key_manager_service
from openstack import resource
from openstack.key_manager.v1 import _format
from openstack import resource2
class Order(resource.Resource):
class Order(resource2.Resource):
resources_key = 'orders'
base_path = '/orders'
service = key_manager_service.KeyManagerService()
# capabilities
allow_create = True
allow_retrieve = True
allow_get = True
allow_update = True
allow_delete = True
allow_list = True
# Properties
# TODO(briancurtin): not documented
error_reason = resource.prop('error_reason')
# TODO(briancurtin): not documented
error_status_code = resource.prop('error_status_code')
#: a dictionary containing key-value parameters which specify the
#: Timestamp in ISO8601 format of when the order was created
created_at = resource2.Body('created')
#: Keystone Id of the user who created the order
creator_id = resource2.Body('creator_id')
#: A dictionary containing key-value parameters which specify the
#: details of an order request
meta = resource.prop('meta')
meta = resource2.Body('meta', type=dict)
#: A URI for this order
order_ref = resource.prop('order_ref')
#: TODO(briancurtin): not documented
secret_ref = resource.prop('secret_ref')
order_ref = resource2.Body('order_ref')
#: The ID of this order
order_id = resource2.Body('order_ref', alternate_id=True,
type=_format.HREFToUUID)
#: Secret href associated with the order
secret_ref = resource2.Body('secret_ref')
#: Secret ID associated with the order
secret_id = resource2.Body('secret_ref', type=_format.HREFToUUID)
# The status of this order
status = resource.prop('status')
status = resource2.Body('status')
#: Metadata associated with the order
sub_status = resource2.Body('sub_status')
#: Metadata associated with the order
sub_status_message = resource2.Body('sub_status_message')
# The type of order
type = resource.prop('type')
type = resource2.Body('type')
#: Timestamp in ISO8601 format of the last time the order was updated.
updated_at = resource2.Body('updated')

View File

@ -11,39 +11,96 @@
# under the License.
from openstack.key_manager import key_manager_service
from openstack import resource
from openstack.key_manager.v1 import _format
from openstack import resource2
from openstack import utils
class Secret(resource.Resource):
id_attribute = 'secret_ref'
class Secret(resource2.Resource):
resources_key = 'secrets'
base_path = '/secrets'
service = key_manager_service.KeyManagerService()
# capabilities
allow_create = True
allow_retrieve = True
allow_get = True
allow_update = True
allow_delete = True
allow_list = True
_query_mapping = resource2.QueryParameters("name", "mode", "bits",
"secret_type", "acl_only",
"created", "updated",
"expiration", "sort",
algorithm="alg")
# Properties
#: Metadata provided by a user or system for informational purposes
algorithm = resource.prop('algorithm')
algorithm = resource2.Body('algorithm')
#: Metadata provided by a user or system for informational purposes.
#: Value must be greater than zero.
bit_length = resource.prop('bit_length')
bit_length = resource2.Body('bit_length')
#: A list of content types
content_types = resource.prop('content_types')
content_types = resource2.Body('content_types', type=dict)
#: Once this timestamp has past, the secret will no longer be available.
expires_at = resource.prop('expiration')
expires_at = resource2.Body('expiration')
#: Timestamp of when the secret was created.
created_at = resource2.Body('created')
#: Timestamp of when the secret was last updated.
updated_at = resource2.Body('updated')
#: The type/mode of the algorithm associated with the secret information.
mode = resource.prop('mode')
mode = resource2.Body('mode')
#: The name of the secret set by the user
name = resource.prop('name')
name = resource2.Body('name')
#: A URI to the sercret
secret_ref = resource.prop('secret_ref')
secret_ref = resource2.Body('secret_ref')
#: The ID of the secret
# NOTE: This is not really how alternate IDs are supposed to work and
# ultimately means this has to work differently than all other services
# in all of OpenStack because of the departure from using actual IDs
# that even this service can't even use itself.
secret_id = resource2.Body('secret_ref', alternate_id=True,
type=_format.HREFToUUID)
#: Used to indicate the type of secret being stored.
secret_type = resource2.Body('secret_type')
#: The status of this secret
status = resource.prop('status')
status = resource2.Body('status')
#: A timestamp when this secret was updated.
updated_at = resource.prop('updated')
updated_at = resource2.Body('updated')
#: The secret's data to be stored. payload_content_type must also
#: be supplied if payload is included. (optional)
payload = resource2.Body('payload')
#: The media type for the content of the payload.
#: (required if payload is included)
payload_content_type = resource2.Body('payload_content_type')
#: The encoding used for the payload to be able to include it in
#: the JSON request. Currently only base64 is supported.
#: (required if payload is encoded)
payload_content_encoding = resource2.Body('payload_content_encoding')
def get(self, session, requires_id=True):
request = self._prepare_request(requires_id=requires_id)
response = session.get(request.uri,
endpoint_filter=self.service).json()
content_type = None
if self.payload_content_type is not None:
content_type = self.payload_content_type
elif "content_types" in response:
content_type = response["content_types"]["default"]
# Only try to get the payload if a content type has been explicitly
# specified or if one was found in the metadata response
if content_type is not None:
payload = session.get(utils.urljoin(request.uri, "payload"),
endpoint_filter=self.service,
headers={"Accept": content_type})
response["payload"] = payload.text
# We already have the JSON here so don't call into _translate_response
body = self._filter_component(response, self._body_mapping())
self._body.attributes.update(body)
self._body.clean()
return self

View File

@ -14,15 +14,17 @@ import testtools
from openstack.key_manager.v1 import container
IDENTIFIER = 'http://localhost/containers/IDENTIFIER'
ID_VAL = "123"
IDENTIFIER = 'http://localhost/containers/%s' % ID_VAL
EXAMPLE = {
'container_ref': IDENTIFIER,
'created': '2015-03-09T12:14:57.233772',
'name': '3',
'secret_refs': '4',
'secret_refs': ['4'],
'status': '5',
'type': '6',
'updated': '2015-03-09T12:15:57.233772',
'consumers': ['7']
}
@ -35,14 +37,13 @@ class TestContainer(testtools.TestCase):
self.assertEqual('/containers', sot.base_path)
self.assertEqual('key-manager', sot.service.service_type)
self.assertTrue(sot.allow_create)
self.assertTrue(sot.allow_retrieve)
self.assertTrue(sot.allow_get)
self.assertTrue(sot.allow_update)
self.assertTrue(sot.allow_delete)
self.assertTrue(sot.allow_list)
def test_make_it(self):
sot = container.Container(EXAMPLE)
self.assertEqual(EXAMPLE['container_ref'], sot.container_ref)
sot = container.Container(**EXAMPLE)
self.assertEqual(EXAMPLE['created'], sot.created_at)
self.assertEqual(EXAMPLE['name'], sot.name)
self.assertEqual(EXAMPLE['secret_refs'], sot.secret_refs)
@ -50,3 +51,6 @@ class TestContainer(testtools.TestCase):
self.assertEqual(EXAMPLE['type'], sot.type)
self.assertEqual(EXAMPLE['updated'], sot.updated_at)
self.assertEqual(EXAMPLE['container_ref'], sot.id)
self.assertEqual(EXAMPLE['container_ref'], sot.container_ref)
self.assertEqual(ID_VAL, sot.container_id)
self.assertEqual(EXAMPLE['consumers'], sot.consumers)

View File

@ -14,15 +14,20 @@ import testtools
from openstack.key_manager.v1 import order
IDENTIFIER = 'IDENTIFIER'
ID_VAL = "123"
SECRET_ID = "5"
IDENTIFIER = 'http://localhost/orders/%s' % ID_VAL
EXAMPLE = {
'error_reason': '1',
'error_status_code': '2',
'meta': '3',
'order_ref': '4',
'secret_ref': '5',
'created': '1',
'creator_id': '2',
'meta': {'key': '3'},
'order_ref': IDENTIFIER,
'secret_ref': 'http://localhost/secrets/%s' % SECRET_ID,
'status': '6',
'type': '7',
'sub_status': '7',
'sub_status_message': '8',
'type': '9',
'updated': '10'
}
@ -35,17 +40,22 @@ class TestOrder(testtools.TestCase):
self.assertEqual('/orders', sot.base_path)
self.assertEqual('key-manager', sot.service.service_type)
self.assertTrue(sot.allow_create)
self.assertTrue(sot.allow_retrieve)
self.assertTrue(sot.allow_get)
self.assertTrue(sot.allow_update)
self.assertTrue(sot.allow_delete)
self.assertTrue(sot.allow_list)
def test_make_it(self):
sot = order.Order(EXAMPLE)
self.assertEqual(EXAMPLE['error_reason'], sot.error_reason)
self.assertEqual(EXAMPLE['error_status_code'], sot.error_status_code)
sot = order.Order(**EXAMPLE)
self.assertEqual(EXAMPLE['created'], sot.created_at)
self.assertEqual(EXAMPLE['creator_id'], sot.creator_id)
self.assertEqual(EXAMPLE['meta'], sot.meta)
self.assertEqual(EXAMPLE['order_ref'], sot.order_ref)
self.assertEqual(ID_VAL, sot.order_id)
self.assertEqual(EXAMPLE['secret_ref'], sot.secret_ref)
self.assertEqual(SECRET_ID, sot.secret_id)
self.assertEqual(EXAMPLE['status'], sot.status)
self.assertEqual(EXAMPLE['sub_status'], sot.sub_status)
self.assertEqual(EXAMPLE['sub_status_message'], sot.sub_status_message)
self.assertEqual(EXAMPLE['type'], sot.type)
self.assertEqual(EXAMPLE['updated'], sot.updated_at)

View File

@ -14,12 +14,12 @@ from openstack.key_manager.v1 import _proxy
from openstack.key_manager.v1 import container
from openstack.key_manager.v1 import order
from openstack.key_manager.v1 import secret
from openstack.tests.unit import test_proxy_base
from openstack.tests.unit import test_proxy_base2
class TestKeyManagementProxy(test_proxy_base.TestProxyBase):
class TestKeyManagerProxy(test_proxy_base2.TestProxyBase):
def setUp(self):
super(TestKeyManagementProxy, self).setUp()
super(TestKeyManagerProxy, self).setUp()
self.proxy = _proxy.Proxy(self.session)
def test_server_create_attrs(self):

View File

@ -10,21 +10,28 @@
# License for the specific language governing permissions and limitations
# under the License.
import mock
import testtools
from openstack.key_manager.v1 import secret
IDENTIFIER = 'http://localhost:9311/v1/secrets/ID'
ID_VAL = "123"
IDENTIFIER = 'http://localhost:9311/v1/secrets/%s' % ID_VAL
EXAMPLE = {
'algorithm': '1',
'bit_length': '2',
'content_types': '3',
'content_types': {'default': '3'},
'expiration': '2017-03-09T12:14:57.233772',
'mode': '5',
'name': '6',
'secret_ref': IDENTIFIER,
'status': '8',
'updated': '2015-03-09T12:15:57.233772',
'updated': '2015-03-09T12:15:57.233773',
'created': '2015-03-09T12:15:57.233774',
'secret_type': '9',
'payload': '10',
'payload_content_type': '11',
'payload_content_encoding': '12'
}
@ -37,13 +44,25 @@ class TestSecret(testtools.TestCase):
self.assertEqual('/secrets', sot.base_path)
self.assertEqual('key-manager', sot.service.service_type)
self.assertTrue(sot.allow_create)
self.assertTrue(sot.allow_retrieve)
self.assertTrue(sot.allow_get)
self.assertTrue(sot.allow_update)
self.assertTrue(sot.allow_delete)
self.assertTrue(sot.allow_list)
self.assertDictEqual({"name": "name",
"mode": "mode",
"bits": "bits",
"secret_type": "secret_type",
"acl_only": "acl_only",
"created": "created",
"updated": "updated",
"expiration": "expiration",
"sort": "sort",
"algorithm": "alg"},
sot._query_mapping._mapping)
def test_make_it(self):
sot = secret.Secret(EXAMPLE)
sot = secret.Secret(**EXAMPLE)
self.assertEqual(EXAMPLE['algorithm'], sot.algorithm)
self.assertEqual(EXAMPLE['bit_length'], sot.bit_length)
self.assertEqual(EXAMPLE['content_types'], sot.content_types)
@ -51,6 +70,64 @@ class TestSecret(testtools.TestCase):
self.assertEqual(EXAMPLE['mode'], sot.mode)
self.assertEqual(EXAMPLE['name'], sot.name)
self.assertEqual(EXAMPLE['secret_ref'], sot.secret_ref)
self.assertEqual(EXAMPLE['secret_ref'], sot.id)
self.assertEqual(ID_VAL, sot.secret_id)
self.assertEqual(EXAMPLE['status'], sot.status)
self.assertEqual(EXAMPLE['updated'], sot.updated_at)
self.assertEqual(EXAMPLE['secret_ref'], sot.id)
self.assertEqual(EXAMPLE['secret_type'], sot.secret_type)
self.assertEqual(EXAMPLE['payload'], sot.payload)
self.assertEqual(EXAMPLE['payload_content_type'],
sot.payload_content_type)
self.assertEqual(EXAMPLE['payload_content_encoding'],
sot.payload_content_encoding)
def test_get_no_payload(self):
sot = secret.Secret(id="id")
sess = mock.Mock()
rv = mock.Mock()
return_body = {"status": "cool"}
rv.json = mock.Mock(return_value=return_body)
sess.get = mock.Mock(return_value=rv)
sot.get(sess)
sess.get.assert_called_once_with("secrets/id",
endpoint_filter=sot.service)
def _test_payload(self, sot, metadata, content_type):
content_type = "some/type"
sot = secret.Secret(id="id", payload_content_type=content_type)
metadata_response = mock.Mock()
metadata_response.json = mock.Mock(return_value=metadata)
payload_response = mock.Mock()
payload = "secret info"
payload_response.text = payload
sess = mock.Mock()
sess.get = mock.Mock(side_effect=[metadata_response, payload_response])
rv = sot.get(sess)
sess.get.assert_has_calls(
[mock.call("secrets/id", endpoint_filter=sot.service),
mock.call("secrets/id/payload", endpoint_filter=sot.service,
headers={"Accept": content_type})])
self.assertEqual(rv.payload, payload)
self.assertEqual(rv.status, metadata["status"])
def test_get_with_payload_from_argument(self):
metadata = {"status": "great"}
content_type = "some/type"
sot = secret.Secret(id="id", payload_content_type=content_type)
self._test_payload(sot, metadata, content_type)
def test_get_with_payload_from_content_types(self):
content_type = "some/type"
metadata = {"status": "fine",
"content_types": {"default": content_type}}
sot = secret.Secret(id="id")
self._test_payload(sot, metadata, content_type)