summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Wood <john.wood@rackspace.com>2013-09-05 14:43:19 -0700
committerJohn Wood <john.wood@rackspace.com>2013-09-05 14:43:19 -0700
commitf93580b5c3bc3f0341395d98d49dc44f421f61b9 (patch)
tree9866cb815d20ddedaa8338d606114231c232858c
parent35e1ceb205d8078dfb9cb81cfe294c94b8400c8d (diff)
parentac5078433f237f2be09cb995c8440f61c6052335 (diff)
downloadpython-barbicanclient-f93580b5c3bc3f0341395d98d49dc44f421f61b9.tar.gz
Merge pull request #17 from dmend/master
Client refactor for M3
-rw-r--r--.gitignore3
-rw-r--r--README.md74
-rw-r--r--barbicanclient/base.py38
-rw-r--r--barbicanclient/client.py528
-rw-r--r--barbicanclient/common/auth.py125
-rw-r--r--barbicanclient/common/exceptions.py15
-rw-r--r--barbicanclient/keep.py322
-rw-r--r--barbicanclient/orders.py142
-rw-r--r--barbicanclient/secrets.py186
-rw-r--r--barbicanclient/test/__init__.py (renamed from tests/__init__.py)0
-rw-r--r--barbicanclient/test/common/__init__.py0
-rw-r--r--barbicanclient/test/common/test_auth.py23
-rw-r--r--barbicanclient/test/test_client.py93
-rw-r--r--barbicanclient/test/test_keep.py (renamed from tests/keep_test.py)0
-rw-r--r--barbicanclient/version.py2
-rw-r--r--setup.py16
-rw-r--r--tests/client_test.py359
-rwxr-xr-xtools/hacking.sh3
-rw-r--r--tools/pip-requires8
-rw-r--r--tools/test-requires22
-rw-r--r--tox.ini8
21 files changed, 878 insertions, 1089 deletions
diff --git a/.gitignore b/.gitignore
index 820ed7f..00f981f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,9 @@ pip-log.txt
.tox
nosetests.xml
+# pyenv
+.python-version
+
# Translations
*.mo
diff --git a/README.md b/README.md
index 9f962b7..1e4c8a8 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,74 @@
-python-barbicanclient
-=====================
+# python-barbicanclient
-This is a client for the [Barbican](https://github.com/cloudkeep/barbican)
+This is a client for the [Barbican](https://github.com/stackforge/barbican)
Key Management API. There is a Python library for accessing the API
(`barbicanclient` module), and a command-line script (`keep`).
+
+## barbicanclient - Python API
+The full api is [documented in the wiki](https://github.com/cloudkeep/python-barbicanclient/wiki/Client-Usage).
+
+### Quickstart
+Store a secret in barbican using keystone for authentication:
+```python
+>>> from barbicanclient.common import auth
+>>> from barbicanclient import client
+# We'll use keystone for authentication
+>>> keystone = auth.KeystoneAuthV2(auth_url='http://keystone-int.cloudkeep.io:5000/v2.0',
+... username='USER', password='PASSWORD', tenant_name='TENANT')
+>>> barbican = client.Client(auth_plugin=keystone)
+# Let's store some sensitive data, Barbican encrypts it and stores it securely in the cloud
+>>> secret_uri = barbican.secrets.store(name='Self destruction sequence',
+... payload='the magic words are squeamish ossifrage',
+... payload_content_type='text/plain')
+# Let's look at some properties of a barbican Secret
+>>> secret = barbican.secrets.get(secret_uri)
+>>> print(secret.secret_ref)
+u'http://api-01-int.cloudkeep.io:9311/v1/test_tenant/secrets/49496a6d-c674-4384-b208-7cf4988f84ee'
+>>> print(secret.name)
+Self destruction sequence
+# Now let's retrieve the secret payload. Barbican decrypts it and sends it back.
+>>> print(barbican.secrets.decrypt(secret.secret_ref))
+the magic words are squeamish ossifrage
+```
+
+## keep - Command Line Client
+Command line client configuration and usage is [documented in the wiki](https://github.com/cloudkeep/python-barbicanclient/wiki/Command-Line-Client).
+
+```
+usage: keep [-h] [--no-auth | --os-auth-url <auth-url>]
+ [--os-username <auth-user-name>] [--os-password <auth-password>]
+ [--os-tenant-name <auth-tenant-name>] [--os-tenant-id <tenant-id>]
+ [--endpoint <barbican-url>]
+ <entity> <action> ...
+
+Command-line interface to the Barbican API.
+
+positional arguments:
+ <entity> Entity used for command, e.g., order, secret.
+
+optional arguments:
+ -h, --help show this help message and exit
+ --no-auth, -N Do not use authentication.
+ --os-auth-url <auth-url>, -A <auth-url>
+ Defaults to env[OS_AUTH_URL].
+ --os-username <auth-user-name>, -U <auth-user-name>
+ Defaults to env[OS_USERNAME].
+ --os-password <auth-password>, -P <auth-password>
+ Defaults to env[OS_PASSWORD].
+ --os-tenant-name <auth-tenant-name>, -T <auth-tenant-name>
+ Defaults to env[OS_TENANT_NAME].
+ --os-tenant-id <tenant-id>, -I <tenant-id>
+ Defaults to env[OS_TENANT_ID].
+ --endpoint <barbican-url>, -E <barbican-url>
+ Defaults to env[BARBICAN_ENDPOINT].
+
+subcommands:
+ Action to perform
+
+ <action>
+ create Create a new order.
+ store Store a secret in barbican.
+ get Retrieve a secret or an order by providing its URI.
+ list List secrets or orders
+ delete Delete a secret or an order by providing its href.
+```
diff --git a/barbicanclient/base.py b/barbicanclient/base.py
new file mode 100644
index 0000000..ca157a1
--- /dev/null
+++ b/barbicanclient/base.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2013 Rackspace, Inc.
+#
+# 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.
+"""
+Base utilites to build API operation managers.
+"""
+
+
+class BaseEntityManager(object):
+ def __init__(self, api, entity):
+ self.api = api
+ self.entity = entity
+
+ def _remove_empty_keys(self, dictionary):
+ for k in dictionary.keys():
+ if dictionary[k] is None:
+ dictionary.pop(k)
+
+ def total(self):
+ """
+ Returns the toatl number of entities stored in Barbican.
+ """
+ href = '{0}/{1}'.format(self.api.base_url, self.entity)
+ params = {'limit': 0, 'offset': 0}
+ resp = self.api.get(href, params)
+
+ return resp['total']
diff --git a/barbicanclient/client.py b/barbicanclient/client.py
index b42d536..81d2c6f 100644
--- a/barbicanclient/client.py
+++ b/barbicanclient/client.py
@@ -1,445 +1,141 @@
-import eventlet
-eventlet.monkey_patch(socket=True, select=True)
-
+# Copyright (c) 2013 Rackspace, Inc.
+#
+# 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 json
import os
+
import requests
-from barbicanclient.secrets import Secret
-from barbicanclient.orders import Order
-from barbicanclient.common import auth
-from barbicanclient.openstack.common import log
-from barbicanclient.common.exceptions import ClientException
+from barbicanclient.openstack.common import log as logging
from barbicanclient.openstack.common.gettextutils import _
-from urlparse import urljoin
+from barbicanclient import orders
+from barbicanclient import secrets
-LOG = log.getLogger(__name__)
-log.setup('barbicanclient')
+LOG = logging.getLogger(__name__)
+logging.setup('barbicanclient')
-class Connection(object):
- SECRETS_PATH = 'secrets'
- ORDERS_PATH = 'orders'
+class HTTPError(Exception):
+ """Base exception for HTTP errors."""
+ def __init__(self, message):
+ super(HTTPError, self).__init__(message)
- def __init__(self, auth_endpoint=None, user=None, key=None, tenant=None,
- token=None, **kwargs):
- """
- Authenticate and connect to the service endpoint, which can be
- received through authentication.
- Environment variables will be used by default when their corresponding
- arguments are not passed in.
+class HTTPServerError(HTTPError):
+ """Raised for 5xx responses from the server."""
+ pass
- :param auth_endpoint: The auth URL to authenticate against
- default: env('OS_AUTH_URL')
- :param user: The user to authenticate as
- default: env('OS_USERNAME')
- :param key: The API key or password to auth with
- default: env('OS_PASSWORD')
- :param tenant: The tenant ID
- default: env('OS_TENANT_NAME')
- :keyword param endpoint: The barbican endpoint to connect to
- default: env('BARBICAN_ENDPOINT')
- If a token is provided, an endpoint should be as well.
- """
+class HTTPClientError(HTTPError):
+ """Raised for 4xx responses from the server."""
+ pass
- LOG.debug(_("Creating Connection object"))
- self.env = kwargs.get('fake_env') or env
- self._auth_endpoint = auth_endpoint or self.env('OS_AUTH_URL')
- self._user = user or self.env('OS_USERNAME')
- self._key = key or self.env('OS_PASSWORD')
- self._tenant = tenant or self.env('OS_TENANT_NAME')
- if not all([self._auth_endpoint, self._user, self._key, self._tenant]):
- raise ClientException("The authorization endpoint, username, key,"
- " and tenant name should either be passed i"
- "n or defined as environment variables.")
- self.authenticate = kwargs.get('authenticate') or auth.authenticate
- self.request = kwargs.get('request') or requests.request
- self._endpoint = (kwargs.get('endpoint') or
- self.env('BARBICAN_ENDPOINT'))
- self._cacert = kwargs.get('cacert')
- self.connect(token=(token or self.env('AUTH_TOKEN')))
+class HTTPAuthError(HTTPError):
+ """Raised for 401 Unauthorized responses from the server."""
+ pass
- @property
- def _conn(self):
- """Property to enable decorators to work properly"""
- return self
- @property
- def auth_endpoint(self):
- """The fully-qualified URI of the auth endpoint"""
- return self._auth_endpoint
+class Client(object):
- @property
- def endpoint(self):
- """The fully-qualified URI of the endpoint"""
- return self._endpoint
-
- @endpoint.setter
- def endpoint(self, value):
- self._endpoint = value
-
- def connect(self, token=None):
+ def __init__(self, auth_plugin=None, endpoint=None, tenant_id=None):
"""
- Establishes a connection. If token is not None or empty, it will be
- used for this connection, and authentication will not take place.
+ Barbican client object used to interact with barbican service.
- :param token: An authentication token
+ :param auth_plugin: Authentication backend plugin
+ defaults to None
+ :param endpoint: Barbican endpoint url. Required when not using
+ an auth_plugin. When not provided, the client will try to
+ fetch this from the auth service catalog
+ :param tenant_id: The tenant ID used for context in barbican.
+ Required when not using auth_plugin. When not provided,
+ the client will try to get this from the auth_plugin.
"""
-
- LOG.debug(_("Establishing connection"))
+ LOG.debug(_("Creating Client object"))
self._session = requests.Session()
+ self.auth_plugin = auth_plugin
- # headers = {"Client-Id": self._client_id}
- # self._session.headers.update(headers)
- self._session.verify = True
-
- if token:
- self.auth_token = token
- else:
- LOG.debug(_("Authenticating token"))
- endpoint, self.auth_token = self.authenticate(
- self._auth_endpoint,
- self._user,
- self._key,
- self._tenant,
- service_type='key-store',
- endpoint=self._endpoint,
- cacert=self._cacert
+ if self.auth_plugin is not None:
+ self._barbican_url = self.auth_plugin.barbican_url
+ self._tenant_id = self.auth_plugin.tenant_id
+ self._session.headers.update(
+ {'X-Auth-Token': self.auth_plugin.auth_token}
)
- if self.endpoint is None:
- self.endpoint = endpoint
-
- @property
- def auth_token(self):
- try:
- return self._session.headers['X-Auth-Token']
- except KeyError:
- return None
-
- @auth_token.setter
- def auth_token(self, value):
- self._token = value
- self._session.headers['X-Auth-Token'] = value
-
- def list_secrets(self, limit=10, offset=0):
- """
- Returns a tuple containing three items: a list of secrets pertaining
- to the given offset and limit, a reference to the previous set of
- secrets, and a reference to the next set of secrets. Either of the
- references may be None.
-
- :param limit: The limit to the number of secrets to list
- :param offset: The offset from the beginning to start listing
- """
- LOG.debug(_("Listing secrets - offset: {0}, limit: {1}").format(offset,
- limit))
- href = "{0}/{1}?limit={2}&offset={3}".format(self._tenant,
- self.SECRETS_PATH,
- limit, offset)
- return self.list_secrets_by_href(href)
-
- def list_secrets_by_href(self, href):
- """
- Returns a tuple containing three items: a list of secrets pertaining
- to the offset and limit within href, a reference to the previous set
- of secrets, and a reference to the next set of secrets. Either of the
- references may be None.
-
- :param href: The full secrets URI
- """
- LOG.debug(_("Listing secrets by href"))
- LOG.debug("href: {0}".format(href))
- if href is None:
- return [], None, None
-
- hdrs, body = self._perform_http(href=href, method='GET')
- LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body))
-
- secrets_dict = body['secrets']
- secrets = [Secret(self._conn, s) for s in secrets_dict]
-
- prev_ref = body.get('previous')
-
- next_ref = body.get('next')
-
- return secrets, prev_ref, next_ref
-
- def create_secret(self,
- name=None,
- payload=None,
- payload_content_type=None,
- payload_content_encoding=None,
- algorithm=None,
- bit_length=None,
- cypher_type=None,
- expiration=None):
- """
- Creates and returns a Secret object with all of its metadata filled in.
-
- :param name: A friendly name for the secret
- :param payload: The unencrypted secret
- :param payload_content_type: The format/type of the secret
- :param payload_content_encoding: The encoding of the secret
- :param algorithm: The algorithm the secret is used with
- :param bit_length: The bit length of the secret
- :param cypher_type: The cypher type (e.g. block cipher mode)
- :param expiration: The expiration time of the secret in ISO 8601 format
- """
- LOG.debug(_("Creating secret of payload content type {0}").format(
- payload_content_type))
- href = "{0}/{1}".format(self._tenant, self.SECRETS_PATH)
- LOG.debug(_("href: {0}").format(href))
- secret_dict = {}
- secret_dict['name'] = name
- secret_dict['payload'] = payload
- secret_dict['payload_content_type'] = payload_content_type
- secret_dict['payload_content_encoding'] = payload_content_encoding
- secret_dict['algorithm'] = algorithm
- secret_dict['cypher_type'] = cypher_type
- secret_dict['bit_length'] = bit_length
- secret_dict['expiration'] = expiration
- self._remove_empty_keys(secret_dict)
- LOG.debug(_("Request body: {0}").format(secret_dict))
- hdrs, body = self._perform_http(href=href,
- method='POST',
- request_body=json.dumps(secret_dict))
-
- LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body))
-
- return self.get_secret(body['secret_ref'])
-
- def delete_secret_by_id(self, secret_id):
- """
- Deletes a secret
-
- :param secret_id: The UUID of the secret
- """
- href = "{0}/{1}/{2}".format(self._tenant, self.SECRETS_PATH, secret_id)
- LOG.info(_("Deleting secret - Secret ID: {0}").format(secret_id))
- return self.delete_secret(href)
-
- def delete_secret(self, href):
- """
- Deletes a secret
-
- :param href: The full URI of the secret
- """
- hdrs, body = self._perform_http(href=href, method='DELETE')
- LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body))
-
- def get_secret_by_id(self, secret_id):
- """
- Returns a Secret object
-
- :param secret_id: The UUID of the secret
- """
- LOG.debug(_("Getting secret - Secret ID: {0}").format(secret_id))
- href = "{0}/{1}/{2}".format(self._tenant, self.SECRETS_PATH, secret_id)
- return self.get_secret(href)
-
- def get_secret(self, href):
- """
- Returns a Secret object
-
- :param href: The full URI of the secret
- """
- hdrs, body = self._perform_http(href=href, method='GET')
- LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body))
- return Secret(self._conn, body)
-
- def get_raw_secret_by_id(self, secret_id, payload_content_type):
- """
- Returns the raw secret
-
- :param secret_id: The UUID of the secret
- :param payload_content_type: The data type of the secret
- """
- LOG.debug(_("Getting raw secret - Secret ID: {0}").format(secret_id))
- href = "{0}/{1}/{2}".format(self._tenant, self.SECRETS_PATH, secret_id)
- return self.get_raw_secret(href, payload_content_type)
-
- def get_raw_secret(self, href, payload_content_type):
- """
- Returns the raw secret
-
- :param href: The reference to the secret
- :param payload_content_type: The data type of the secret
- """
- hdrs = {"Accept": payload_content_type}
- hdrs, body = self._perform_http(href=href, method='GET', headers=hdrs,
- parse_json=False)
- LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body))
- return body
-
- def list_orders(self, limit=10, offset=0):
- """
- Returns a tuple containing three items: a list of orders pertaining
- to the given offset and limit, a reference to the previous set of
- orders, and a reference to the next set of orders. Either of the
- references may be None.
-
- :param limit: The limit to the number of orders to list
- :param offset: The offset from the beginning to start listing
- """
- LOG.debug(_("Listing orders - offset: {0}, limit: {1}").format(offset,
- limit))
- href = "{0}/{1}?limit={2}&offset={3}".format(self._tenant,
- self.ORDERS_PATH,
- limit, offset)
- return self.list_orders_by_href(href)
-
- def list_orders_by_href(self, href):
- """
- Returns a tuple containing three items: a list of orders pertaining
- to the offset and limit within href, a reference to the previous set
- of orders, and a reference to the next set of orders. Either of the
- references may be None.
-
- :param href: The full orders URI
- """
- LOG.debug(_("Listing orders by href"))
- LOG.debug("href: {0}".format(href))
- if href is None:
- return [], None, None
-
- hdrs, body = self._perform_http(href=href, method='GET')
- LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body))
-
- orders_dict = body['orders']
- orders = [Order(self._conn, o) for o in orders_dict]
-
- prev_ref = body.get('previous')
-
- next_ref = body.get('next')
-
- return orders, prev_ref, next_ref
-
- def create_order(self,
- name=None,
- payload_content_type='application/octet-stream',
- algorithm='aes',
- bit_length=256,
- cypher_type='cbc',
- expiration=None):
- """
- Creates and returns an Order object with all of its metadata filled in.
-
- :param name: A friendly name for the secret
- :param algorithm: The algorithm the secret is used with
- :param bit_length: The bit length of the secret
- :param cypher_type: The cypher type (e.g. block cipher mode)
- :param expiration: The expiration time of the secret in ISO 8601 format
- """
- LOG.debug(_("Creating order"))
- href = "{0}/{1}".format(self._tenant, self.ORDERS_PATH)
- LOG.debug("href: {0}".format(href))
- order_dict = {'secret': {}}
- order_dict['secret']['name'] = name
- order_dict['secret'][
- 'payload_content_type'] = payload_content_type
- order_dict['secret']['algorithm'] = algorithm
- order_dict['secret']['bit_length'] = bit_length
- order_dict['secret']['cypher_type'] = cypher_type
- order_dict['secret']['expiration'] = expiration
- self._remove_empty_keys(order_dict['secret'])
- LOG.debug(_("Request body: {0}").format(order_dict['secret']))
- hdrs, body = self._perform_http(href=href,
- method='POST',
- request_body=json.dumps(order_dict))
-
- LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body))
-
- return self.get_order(body['order_ref'])
-
- def delete_order_by_id(self, order_id):
- """
- Deletes an order
-
- :param order_id: The UUID of the order
- """
- LOG.info(_("Deleting order - Order ID: {0}").format(order_id))
- href = "{0}/{1}/{2}".format(self._tenant, self.ORDERS_PATH, order_id)
- return self.delete_order(href)
-
- def delete_order(self, href):
- """
- Deletes an order
-
- :param href: The full URI of the order
- """
- hdrs, body = self._perform_http(href=href, method='DELETE')
- LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body))
-
- def get_order_by_id(self, order_id):
- """
- Returns an Order object
-
- :param order_id: The UUID of the order
- """
- LOG.debug(_("Getting order - Order ID: {0}").format(order_id))
- href = "{0}/{1}/{2}".format(self._tenant, self.ORDERS_PATH, order_id)
- return self.get_order(href)
-
- def get_order(self, href):
- """
- Returns an Order object
-
- :param href: The full URI of the order
- """
- hdrs, body = self._perform_http(href=href, method='GET')
- LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body))
- return Order(self._conn, body)
-
- def _remove_empty_keys(self, dictionary):
- for k in dictionary.keys():
- if dictionary[k] is None:
- dictionary.pop(k)
-
- def _perform_http(self, method, href, request_body='', headers={},
- parse_json=True):
- """
- Perform an HTTP operation, checking for appropriate
- errors, etc. and returns the response
-
- Returns the headers and body as a tuple.
-
- :param method: The http method to use (GET, PUT, etc)
- :param body: The optional body to submit
- :param headers: Any additional headers to submit
- :param parse_json: Whether the response body should be parsed as json
- """
- if not isinstance(request_body, str):
- request_body = json.dumps(request_body)
-
- if not self.endpoint.endswith('/'):
- self.endpoint += '/'
-
- url = urljoin(self.endpoint, href)
-
- headers['X-Auth-Token'] = self.auth_token
-
- response = self.request(method=method, url=url, data=request_body,
- headers=headers)
- # Check if the status code is 2xx class
- if not response.ok:
- LOG.error('Bad response: {0}'.format(response.status_code))
- raise ClientException(href=href, method=method,
- http_status=response.status_code,
- http_response_content=response.content)
-
- if response.content and parse_json is True:
- resp_body = json.loads(response.content)
- elif response.content and parse_json is False:
- resp_body = response.content
else:
- resp_body = ''
-
- return response.headers, resp_body
+ if endpoint is None:
+ raise ValueError('Barbican endpoint url must be provided, or '
+ 'must be available from auth_plugin')
+ if tenant_id is None:
+ raise ValueError('Tenant ID must be provided, or must be'
+ ' available from the auth_plugin')
+ if endpoint.endswith('/'):
+ self._barbican_url = endpoint[:-1]
+ else:
+ self._barbican_url = endpoint
+ self._tenant_id = tenant_id
+
+ self.base_url = '{0}/{1}'.format(self._barbican_url, self._tenant_id)
+ self.secrets = secrets.SecretManager(self)
+ self.orders = orders.OrderManager(self)
+
+ def get(self, href, params=None):
+ headers = {'Accept': 'application/json'}
+ resp = self._session.get(href, params=params, headers=headers)
+ self._check_status_code(resp)
+ return resp.json()
+
+ def get_raw(self, href, headers):
+ resp = self._session.get(href, headers=headers)
+ self._check_status_code(resp)
+ return resp.content
+
+ def delete(self, href):
+ resp = self._session.delete(href)
+ self._check_status_code(resp)
+
+ def post(self, path, data):
+ url = '{0}/{1}/'.format(self.base_url, path)
+ headers = {'content-type': 'application/json'}
+ resp = self._session.post(url, data=json.dumps(data), headers=headers)
+ self._check_status_code(resp)
+ return resp.json()
+
+ def _check_status_code(self, resp):
+ status = resp.status_code
+ LOG.debug('Response status {0}'.format(status))
+ if status == 401:
+ LOG.error('Auth error: {0}'.format(self._get_error_message(resp)))
+ raise HTTPAuthError('{0}'.format(self._get_error_message(resp)))
+ if status >= 500:
+ LOG.error('5xx Server error: {0}'.format(
+ self._get_error_message(resp)
+ ))
+ raise HTTPServerError('{0}'.format(self._get_error_message(resp)))
+ if status >= 400:
+ LOG.error('4xx Client error: {0}'.format(
+ self._get_error_message(resp)
+ ))
+ raise HTTPClientError('{0}'.format(self._get_error_message(resp)))
+
+ def _get_error_message(self, resp):
+ try:
+ message = resp.json()['title']
+ except ValueError:
+ message = resp.content
+ return message
def env(*vars, **kwargs):
diff --git a/barbicanclient/common/auth.py b/barbicanclient/common/auth.py
index 891f9d6..d49900a 100644
--- a/barbicanclient/common/auth.py
+++ b/barbicanclient/common/auth.py
@@ -1,70 +1,63 @@
-
-from exceptions import ClientException
-
+# Copyright (c) 2013 Rackspace, Inc.
+#
+# 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 keystoneclient.v2_0 import client as ksclient
from keystoneclient import exceptions
-def authenticate(auth_url, user, key, tenant, **kwargs):
- """Authenticates against the endpoint to use. The correct
- endpoint to use is looked up in the service catalog. The
- caller can override this lookup by passing the endpoint
- as a parameter.
-
- :param auth_url: The keystone auth endpoint to use
- :param user: The username to use for auth
- :param key: The apikey to use for authentiation
- :param endpoint: The Barbican endpoint to use. IOW, don't
- look up an endpoint in the service catalog, just use
- this one instead.
- :param tenant_name: The optional tenant-name to use
- :param tenant_id: The optional tenant ID toi use
- :param cacert: The cacert PEM file to use
- :param service_type: The service type to look for in
- the service catalog
- :param endpoint_type The endpoint type to reference in
- the service catalog
- :param region_name The region to pass for authentication
-
- :returns: Tuple containing Barbican endpoint and token
-
- :raises: ClientException
- """
- insecure = kwargs.get('insecure', False)
- endpoint = kwargs.get('endpoint')
- cacert = kwargs.get('cacert')
-
- try:
- _ksclient = ksclient.Client(username=user,
- password=key,
- tenant_name=tenant,
- cacert=cacert,
- auth_url=auth_url,
- insecure=insecure)
-
- except exceptions.Unauthorized:
- raise ClientException('Unauthorized. Check username, password'
- ' and tenant name/id')
-
- except exceptions.AuthorizationFailure:
- raise ClientException('Authorization Failure. %s')
-
- if not endpoint:
- # The user did not pass in an endpoint, so we need to
- # look one up on their behalf in the service catalog
-
- # TODO(jdp): Ensure that this is the correct service_type field
- service_type = kwargs.get('service_type', 'queueing')
- endpoint_type = kwargs.get('endpoint_type', 'publicURL')
- region = kwargs.get('region_name')
-
- try:
- endpoint = _ksclient.service_catalog.url_for(
- attr='region',
- filter_value=region,
- service_type=service_type,
- endpoint_type=endpoint_type)
- except exceptions.EndpointNotFound:
- raise ClientException('Endpoint not found in service catalog')
-
- return endpoint, _ksclient.auth_token
+class AuthException(Exception):
+ """Raised when authorization fails."""
+ def __init__(self, message):
+ self.message = message
+
+
+class KeystoneAuthV2(object):
+ def __init__(self, auth_url='', username='', password='',
+ tenant_name='', tenant_id=''):
+ if not all([auth_url, username, password, tenant_name or tenant_id]):
+ raise ValueError('Please provide auht_url, username, password,'
+ ' and tenant_id or tenant_name)')
+ self._keystone = ksclient.Client(username=username,
+ password=password,
+ tenant_name=tenant_name,
+ auth_url=auth_url)
+ self._barbican_url = None
+ #TODO(dmend): make these configurable
+ self._service_type = 'keystore'
+ self._endpoint_type = 'publicURL'
+
+ self.tenant_name = self._keystone.tenant_name
+ self.tenant_id = self._keystone.tenant_id
+
+ @property
+ def auth_token(self):
+ return self._keystone.auth_token
+
+ @property
+ def barbican_url(self):
+ if not self._barbican_url:
+ try:
+ self._barbican_url = self._keystone.service_catalog.url_for(
+ attr='region',
+ filter_value=self._keystone.region_name,
+ service_type=self._service_type,
+ endpoint_type=self._endpoint_type
+ )
+ except exceptions.EmptyCatalog:
+ LOG.error('Keystone is reporting an empty catalog.')
+ raise AuthException('Empty keystone catalog.')
+ except exceptions.EndpointNotFound:
+ LOG.error('Barbican endpoint not found in keystone catalog.')
+ raise AuthException('Barbican endpoint not found.')
+ return self._barbican_url
diff --git a/barbicanclient/common/exceptions.py b/barbicanclient/common/exceptions.py
deleted file mode 100644
index 49c5f6a..0000000
--- a/barbicanclient/common/exceptions.py
+++ /dev/null
@@ -1,15 +0,0 @@
-class ClientException(Exception):
- """Exception for wrapping up Barbican client errors"""
- def __init__(self, href='', http_status=0,
- method='', http_response_content=''):
-
- self.method = method
- self.href = href
- self.http_status = http_status
- self.http_response_content = http_response_content
-
- msg = "%s %s returned %d with msg: %s" % (self.method,
- self.href,
- self.http_status,
- self.http_response_content)
- Exception.__init__(self, msg)
diff --git a/barbicanclient/keep.py b/barbicanclient/keep.py
index b939aef..f81ff42 100644
--- a/barbicanclient/keep.py
+++ b/barbicanclient/keep.py
@@ -1,106 +1,163 @@
+# Copyright (c) 2013 Rackspace, Inc.
+#
+# 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.
+"""
+Command-line interface to the Barbican API.
+"""
import argparse
+from barbicanclient.common import auth
from barbicanclient import client
class Keep:
def __init__(self):
- self.parser = self.get_main_parser()
- self.subparsers = self.parser.add_subparsers(title='subcommands',
- description=
- 'Action to perform')
- self.add_create_args()
- self.add_delete_args()
- self.add_get_args()
- self.add_list_args()
-
- def get_main_parser(self):
- parser = argparse.ArgumentParser(description='Access the Barbican'
- ' key management sevice.')
- parser.add_argument('type',
- choices=["order", "secret"],
- help="type to operate on")
- parser.add_argument('--auth_endpoint', '-A',
- default=client.env('OS_AUTH_URL'),
- help='the URL to authenticate against (default: '
- '%(default)s)')
- parser.add_argument('--user', '-U', default=client.env('OS_USERNAME'),
- help='the user to authenticate as (default: %(de'
- 'fault)s)')
- parser.add_argument('--password', '-P',
+ self.parser = self._get_main_parser()
+ self.subparsers = self.parser.add_subparsers(
+ title='subcommands',
+ metavar='<action>',
+ description='Action to perform'
+ )
+ self._add_create_args()
+ self._add_store_args()
+ self._add_get_args()
+ self._add_list_args()
+ self._add_delete_args()
+
+ def _get_main_parser(self):
+ parser = argparse.ArgumentParser(
+ description=__doc__.strip()
+ )
+ parser.add_argument('command',
+ metavar='<entity>',
+ choices=['order', 'secret'],
+ help='Entity used for command, e.g.,'
+ ' order, secret.')
+ auth_group = parser.add_mutually_exclusive_group()
+ auth_group.add_argument('--no-auth', '-N', action='store_true',
+ help='Do not use authentication.')
+ auth_group.add_argument('--os-auth-url', '-A',
+ metavar='<auth-url>',
+ default=client.env('OS_AUTH_URL'),
+ help='Defaults to env[OS_AUTH_URL].')
+ parser.add_argument('--os-username', '-U',
+ metavar='<auth-user-name>',
+ default=client.env('OS_USERNAME'),
+ help='Defaults to env[OS_USERNAME].')
+ parser.add_argument('--os-password', '-P',
+ metavar='<auth-password>',
default=client.env('OS_PASSWORD'),
- help='the API key or password to authenticate with'
- ' (default: %(default)s)')
- parser.add_argument('--tenant', '-T',
+ help='Defaults to env[OS_PASSWORD].')
+ parser.add_argument('--os-tenant-name', '-T',
+ metavar='<auth-tenant-name>',
default=client.env('OS_TENANT_NAME'),
- help='the tenant ID (default: %(default)s)')
+ help='Defaults to env[OS_TENANT_NAME].')
+ parser.add_argument('--os-tenant-id', '-I',
+ metavar='<tenant-id>',
+ default=client.env('OS_TENANT_ID'),
+ help='Defaults to env[OS_TENANT_ID].')
parser.add_argument('--endpoint', '-E',
+ metavar='<barbican-url>',
default=client.env('BARBICAN_ENDPOINT'),
- help='the URL of the barbican server (default: %'
- '(default)s)')
- parser.add_argument('--token', '-K',
- default=client.env('AUTH_TOKEN'), help='the au'
- 'thentication token (default: %(default)s)')
+ help='Defaults to env[BARBICAN_ENDPOINT].')
return parser
- def add_create_args(self):
- create_parser = self.subparsers.add_parser('create', help='Create a '
- 'secret or an order')
+ def _add_create_args(self):
+ create_parser = self.subparsers.add_parser('create',
+ help='Create a new order.')
create_parser.add_argument('--name', '-n',
- help='a human-friendly name')
- create_parser.add_argument('--algorithm', '-a', default='aes', help='t'
- 'he algorithm; used only for reference (def'
- 'ault: %(default)s)')
- create_parser.add_argument('--bit_length', '-b', default=256,
- help='the bit length of the secret; used '
- 'only for reference (default: %(default)s)',
+ help='a human-friendly name.')
+ create_parser.add_argument('--algorithm', '-a', default='aes',
+ help='the algorithm to be used with the '
+ 'requested key (default: '
+ '%(default)s).')
+ create_parser.add_argument('--bit-length', '-b', default=256,
+ help='the bit length of the requested'
+ ' secret key (default: %(default)s).',
type=int)
- create_parser.add_argument('--cypher_type', '-c', default="cbc",
- help='the cypher type; used only for refere'
- 'nce (default: %(default)s)')
- create_parser.add_argument('--payload', '-p', help='the unencrypted'
- ' secret; if provided, you must also provid'
- 'e a payload_content_type (only used for se'
- 'crets)')
- create_parser.add_argument('--payload_content_type', '-t',
- help='the type/format of the provided '
- 'secret data; "text/plain" is assumed to be'
- ' UTF-8; required when --payload is su'
- 'pplied and when creating orders')
- create_parser.add_argument('--payload_content_encoding', '-d',
- help='required if --payload_content_type is'
- ' "application/octet-stream" (only used for'
- ' secrets)')
-
- create_parser.add_argument('--expiration', '-e', help='the expiration '
- 'time for the secret in ISO 8601 format')
+ create_parser.add_argument('--mode', '-m', default='cbc',
+ help='the algorithmm mode to be used with '
+ 'the rquested key (default: %(default)s).')
+ create_parser.add_argument('--payload-content-type', '-t',
+ default='application/octet-stream',
+ help='the type/format of the secret to be'
+ ' generated (default: %(default)s).')
+ create_parser.add_argument('--expiration', '-x', help='the expiration '
+ 'time for the secret in ISO 8601 format.')
create_parser.set_defaults(func=self.create)
- def add_delete_args(self):
- delete_parser = self.subparsers.add_parser('delete', help='Delete a se'
- 'cret or an order by provid'
- 'ing its UUID')
- delete_parser.add_argument('UUID', help='the universally unique identi'
- 'fier of the the secret or order')
+ def _add_store_args(self):
+ store_parser = self.subparsers.add_parser(
+ 'store',
+ help='Store a secret in barbican.'
+ )
+ store_parser.add_argument('--name', '-n',
+ help='a human-friendly name.')
+ store_parser.add_argument('--payload', '-p', help='the unencrypted'
+ ' secret; if provided, you must also provide'
+ ' a payload_content_type')
+ store_parser.add_argument('--payload-content-type', '-t',
+ help='the type/format of the provided '
+ 'secret data; "text/plain" is assumed to be'
+ ' UTF-8; required when --payload is'
+ ' supplied.')
+ store_parser.add_argument('--payload-content-encoding', '-e',
+ help='required if --payload-content-type is'
+ ' "application/octet-stream".')
+ store_parser.add_argument('--algorithm', '-a', default='aes',
+ help='the algorithm (default: %(default)s).')
+ store_parser.add_argument('--bit-length', '-b', default=256,
+ help='the bit length '
+ '(default: %(default)s).',
+ type=int)
+ store_parser.add_argument('--mode', '-m', default='cbc',
+ help='the algorithmm mode; used only for '
+ 'reference (default: %(default)s)')
+ store_parser.add_argument('--expiration', '-x', help='the expiration '
+ 'time for the secret in ISO 8601 format.')
+ store_parser.set_defaults(func=self.store)
+
+ def _add_delete_args(self):
+ delete_parser = self.subparsers.add_parser(
+ 'delete',
+ help='Delete a secret or an order by providing its href.'
+ )
+ delete_parser.add_argument('URI', help='The URI reference for the'
+ ' secret or order')
delete_parser.set_defaults(func=self.delete)
- def add_get_args(self):
- get_parser = self.subparsers.add_parser('get', help='Retrieve a secret'
- ' or an order by providing its'
- ' UUID.')
- get_parser.add_argument('UUID', help='the universally unique identi'
- 'fier of the the secret or order')
- get_parser.add_argument('--raw', '-r', help='if specified, gets the ra'
- 'w secret of type specified with --payload_con'
- 'tent_type (only used for secrets)',
+ def _add_get_args(self):
+ get_parser = self.subparsers.add_parser(
+ 'get',
+ help='Retrieve a secret or an order by providing its URI.'
+ )
+ get_parser.add_argument('URI', help='The URI reference for the secret'
+ ' or order.')
+ get_parser.add_argument('--decrypt', '-d', help='if specified, keep'
+ ' will retrieve the unencrypted secret data;'
+ ' the data type can be specified with'
+ ' --payload-content-type (only used for'
+ ' secrets).',
action='store_true')
get_parser.add_argument('--payload_content_type', '-t',
default='text/plain',
- help='the content type of the raw secret (defa'
- 'ult: %(default)s; only used for secrets)')
+ help='the content type of the decrypted'
+ ' secret (default: %(default)s; only used for'
+ ' secrets)')
get_parser.set_defaults(func=self.get)
- def add_list_args(self):
+ def _add_list_args(self):
list_parser = self.subparsers.add_parser('list',
help='List secrets or orders')
list_parser.add_argument('--limit', '-l', default=10, help='specify t'
@@ -110,70 +167,83 @@ class Keep:
list_parser.add_argument('--offset', '-o', default=0, help='specify t'
'he page offset (default: %(default)s)',
type=int)
- list_parser.add_argument('--URI', '-u', help='the full reference to '
- 'what is to be listed; put in quotes to avoid'
- ' backgrounding when \'&\' is in the URI')
- list_parser.set_defaults(func=self.lst)
+ list_parser.set_defaults(func=self.list)
- def create(self, args):
- if args.type == 'secret':
- secret = self.conn.create_secret(args.name,
- args.payload,
- args.payload_content_type,
- args.payload_content_encoding,
- args.algorithm,
- args.bit_length,
- args.cypher_type,
- args.expiration)
+ def store(self, args):
+ if args.command == 'secret':
+ secret = self.client.secrets.store(args.name,
+ args.payload,
+ args.payload_content_type,
+ args.payload_content_encoding,
+ args.algorithm,
+ args.bit_length,
+ args.mode,
+ args.expiration)
print secret
else:
- order = self.conn.create_order(args.name,
- args.payload_content_type,
- args.algorithm,
- args.bit_length,
- args.cypher_type,
- args.expiration)
+ self.parser.exit(status=1, message='ERROR: store is only supported'
+ ' for secrets\n')
+
+ def create(self, args):
+ if args.command == 'order':
+ order = self.client.orders.create(args.name,
+ args.payload_content_type,
+ args.algorithm,
+ args.bit_length,
+ args.mode,
+ args.expiration)
print order
+ else:
+ self.parser.exit(status=1, message='ERROR: create is only '
+ 'supported for orders\n')
def delete(self, args):
- if args.type == 'secret':
- self.conn.delete_secret_by_id(args.UUID)
+ if args.command == 'secret':
+ self.client.secret.delete(args.URI)
else:
- self.conn.delete_order_by_id(args.UUID)
+ self.client.orders.delete(args.URI)
def get(self, args):
- if args.type == 'secret':
- if args.raw:
- print self.conn.get_raw_secret_by_id(args.UUID,
- args.payload_content_type)
+ if args.command == 'secret':
+ if args.decrypt:
+ print self.client.secrets.raw(args.URI,
+ args.payload_content_type)
else:
- print self.conn.get_secret_by_id(args.UUID)
+ print self.client.secrets.get(args.URI)
else:
- print self.conn.get_order_by_id(args.UUID)
+ print self.client.orders.get(args.URI)
- def lst(self, args):
- if args.type == 'secret':
- if args.URI:
- l = self.conn.list_secrets_by_href(args.URI)
- else:
- l = self.conn.list_secrets(args.limit, args.offset)
+ def list(self, args):
+ if args.command == 'secret':
+ ls = self.client.secrets.list(args.limit, args.offset)
else:
- if args.URI:
- l = self.conn.list_orders_by_href(args.URI)
- else:
- l = self.conn.list_orders(args.limit, args.offset)
- for i in l[0]:
- print i
- print '{0}s displayed: {1} - offset: {2}'.format(args.type, len(l[0]),
+ ls = self.client.orders.list(args.limit, args.offset)
+ for obj in ls:
+ print obj
+ print '{0}s displayed: {1} - offset: {2}'.format(args.command, len(ls),
args.offset)
def execute(self, **kwargs):
args = self.parser.parse_args(kwargs.get('argv'))
- self.conn = client.Connection(args.auth_endpoint, args.user,
- args.password, args.tenant,
- args.token,
- endpoint=args.endpoint)
-
+ if args.no_auth:
+ self.client = client.Client(endpoint=args.endpoint,
+ tenant_id=args.os_tenant_id)
+ elif all([args.os_auth_url, args.os_username, args.os_password,
+ args.os_tenant_name]):
+ self._keystone = auth.KeystoneAuthV2(
+ auth_url=args.os_auth_url,
+ username=args.os_username,
+ password=args.os_password,
+ tenant_name=args.os_tenant_name
+ )
+ self.client = client.Client(auth_plugin=self._keystone,
+ endpoint=args.endpoint,
+ tenant_id=args.tenant_id)
+ else:
+ self.parser.exit(
+ status=1,
+ message='ERROR: please specify authentication credentials\n'
+ )
args.func(args)
diff --git a/barbicanclient/orders.py b/barbicanclient/orders.py
index c117184..27d52a5 100644
--- a/barbicanclient/orders.py
+++ b/barbicanclient/orders.py
@@ -1,47 +1,129 @@
-from urlparse import urlparse
-from openstack.common.timeutils import parse_isotime
+# Copyright (c) 2013 Rackspace, Inc.
+#
+# 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 barbicanclient import base
+from barbicanclient.openstack.common.gettextutils import _
+from barbicanclient.openstack.common import log as logging
+from barbicanclient.openstack.common import timeutils
+
+
+LOG = logging.getLogger(__name__)
class Order(object):
- def __init__(self, connection, order_dict):
+ def __init__(self, order_dict):
"""
- Builds an order object from a json representation. Includes the
- connection object for subtasks.
+ Builds an order object from a dictionary.
"""
- self.connection = connection
- self.secret = order_dict['secret']
self.order_ref = order_dict['order_ref']
- self.created = parse_isotime(order_dict['created'])
- self.secret_ref = order_dict.get('secret_ref')
self.status = order_dict.get('status')
-
+ self.created = timeutils.parse_isotime(order_dict['created'])
if order_dict.get('updated') is not None:
- self.updated = parse_isotime(order_dict['updated'])
+ self.updated = timeutils.parse_isotime(order_dict['updated'])
else:
self.updated = None
+ self.secret_ref = order_dict.get('secret_ref')
+
+ def __str__(self):
+ return ("Order - order href: {0}\n"
+ " secret href: {1}\n"
+ " created: {2}\n"
+ " status: {3}\n"
+ .format(self.order_ref, self.secret_ref,
+ self.created, self.status)
+ )
- self._id = urlparse(self.order_ref).path.split('/').pop()
+ def __repr__(self):
+ return 'Order(order_ref={0})'.format(self.order_ref)
- @property
- def id(self):
- return self._id
- def get_secret(self):
- return self.connection.get_secret(self.secret_ref)
+class OrderManager(base.BaseEntityManager):
- def save(self):
- self.connection.update_order(self)
+ def __init__(self, api):
+ super(OrderManager, self).__init__(api, 'orders')
- def delete(self):
- self.connection.delete_order(self)
+ def create(self,
+ name=None,
+ payload_content_type='application/octet-stream',
+ algorithm=None,
+ bit_length=None,
+ mode=None,
+ expiration=None):
+ """
+ Creates a new Order in Barbican
- def __str__(self):
- return ("Order - ID: {0}\n"
- " order reference: {1}\n"
- " secret reference: {2}\n"
- " created: {3}\n"
- " status: {4}\n"
- .format(self.id, self.order_ref, self.secret_ref, self.created,
- self.status)
- )
+ :param name: A friendly name for the secret
+ :param payload_content_type: The format/type of the secret data
+ :param algorithm: The algorithm the secret associated with
+ :param bit_length: The bit length of the secret
+ :param mode: The algorithm mode (e.g. CBC or CTR mode)
+ :param expiration: The expiration time of the secret in ISO 8601
+ format
+ :returns: Order href for the created order
+ """
+ LOG.debug(_("Creating order"))
+
+ order_dict = {'secret': {}}
+ order_dict['secret']['name'] = name
+ order_dict['secret'][
+ 'payload_content_type'] = payload_content_type
+ order_dict['secret']['algorithm'] = algorithm
+ order_dict['secret']['bit_length'] = bit_length
+ order_dict['secret']['mode'] = mode
+ order_dict['secret']['expiration'] = expiration
+ self._remove_empty_keys(order_dict['secret'])
+
+ LOG.debug(_("Request body: {0}").format(order_dict['secret']))
+
+ resp = self.api.post(self.entity, order_dict)
+ return resp['order_ref']
+
+ def get(self, order_ref):
+ """
+ Returns an Order object
+
+ :param order_ref: The href for the order
+ """
+ LOG.debug(_("Getting order - Order href: {0}").format(order_ref))
+ if not order_ref:
+ raise ValueError('order_ref is required.')
+ resp = self.api.get(order_ref)
+ return Order(resp)
+
+ def delete(self, order_ref):
+ """
+ Deletes an order
+
+ :param order_ref: The href for the order
+ """
+ if not order_ref:
+ raise ValueError('order_ref is required.')
+ self.api.delete(order_ref)
+
+ def list(self, limit=10, offset=0):
+ """
+ Lists all orders for the tenant
+
+ :param limit: Max number of orders returned
+ :param offset: Offset orders to begin list
+ :returns: list of Order objects
+ """
+ LOG.debug('Listing orders - offest {0} limit {1}'.format(offset,
+ limit))
+ href = '{0}/{1}'.format(self.api.base_url, self.entity)
+ params = {'limit': limit, 'offset': offset}
+ resp = self.api.get(href, params)
+
+ return [Order(o) for o in resp['orders']]
diff --git a/barbicanclient/secrets.py b/barbicanclient/secrets.py
index d5ad472..f4dda80 100644
--- a/barbicanclient/secrets.py
+++ b/barbicanclient/secrets.py
@@ -1,62 +1,170 @@
-from urlparse import urlparse
-from openstack.common.timeutils import parse_isotime
+# Copyright (c) 2013 Rackspace, Inc.
+#
+# 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 barbicanclient import base
+from barbicanclient.openstack.common import log as logging
+from barbicanclient.openstack.common.timeutils import parse_isotime
-class Secret(object):
+LOG = logging.getLogger(__name__)
+
+class Secret(object):
"""
- A secret is any data the user has stored in the key management system.
+ Secrets are used to keep track of the data stored in Barbican.
"""
- def __init__(self, connection, secret_dict):
+ def __init__(self, secret_dict):
"""
- Builds a secret object from a json representation. Includes the
- connection object for subtasks.
+ Builds a secret object from a dictionary.
"""
- self.connection = connection
self.secret_ref = secret_dict.get('secret_ref')
- self.created = parse_isotime(secret_dict.get('created'))
- self.status = secret_dict.get('status')
-
- self.algorithm = secret_dict.get('algorithm')
- self.bit_length = secret_dict.get('bit_length')
- self.payload_content_type = secret_dict.get('payload_content_type')
- self.payload_content_encoding = secret_dict.get(
- 'payload_content_encoding')
-
- self.cypher_type = secret_dict.get('cypher_type')
self.name = secret_dict.get('name')
+ self.status = secret_dict.get('status')
+ self.content_types = secret_dict.get('content_types')
+ self.created = parse_isotime(secret_dict.get('created'))
if secret_dict.get('expiration') is not None:
self.expiration = parse_isotime(secret_dict['expiration'])
else:
self.expiration = None
-
if secret_dict.get('updated') is not None:
self.updated = parse_isotime(secret_dict['updated'])
else:
self.updated = None
- self._id = urlparse(self.secret_ref).path.split('/').pop()
-
- @property
- def id(self):
- return self._id
+ self.algorithm = secret_dict.get('algorithm')
+ self.bit_length = secret_dict.get('bit_length')
+ self.mode = secret_dict.get('mode')
def __str__(self):
- return ("Secret - ID: {0}\n"
- " reference: {1}\n"
- " name: {2}\n"
- " created: {3}\n"
- " status: {4}\n"
- " payload content type: {5}\n"
- " payload content encoding: {6}\n"
- " bit length: {7}\n"
- " algorithm: {8}\n"
- " cypher type: {9}\n"
- " expiration: {10}\n"
- .format(self.id, self.secret_ref, self.name, self.created,
- self.status, self.payload_content_type,
- self.payload_content_encoding, self.bit_length,
- self.algorithm, self.cypher_type, self.expiration)
+ return ("Secret - href: {0}\n"
+ " name: {1}\n"
+ " created: {2}\n"
+ " status: {3}\n"
+ " content types: {4}\n"
+ " algorithm: {5}\n"
+ " bit length: {6}\n"
+ " mode: {7}\n"
+ " expiration: {8}\n"
+ .format(self.secret_ref, self.name, self.created,
+ self.status, self.content_types, self.algorithm,
+ self.bit_length, self.mode, self.expiration)
)
+
+ def __repr__(self):
+ return 'Secret(name="{0}")'.format(self.name)
+
+
+class SecretManager(base.BaseEntityManager):
+
+ def __init__(self, api):
+ super(SecretManager, self).__init__(api, 'secrets')
+
+ def store(self,
+ name=None,
+ payload=None,
+ payload_content_type=None,
+ payload_content_encoding=None,
+ algorithm=None,
+ bit_length=None,
+ mode=None,
+ expiration=None):
+ """
+ Stores a new Secret in Barbican
+
+ :param name: A friendly name for the secret
+ :param payload: The unencrypted secret data
+ :param payload_content_type: The format/type of the secret data
+ :param payload_content_encoding: The encoding of the secret data
+ :param algorithm: The algorithm associated with this secret key
+ :param bit_length: The bit length of this secret key
+ :param mode: The algorithm mode used with this secret key
+ :param expiration: The expiration time of the secret in ISO 8601
+ format
+ :returns: Secret href for the stored secret
+ """
+ LOG.debug("Creating secret of payload content type {0}".format(
+ payload_content_type))
+
+ secret_dict = dict()
+ secret_dict['name'] = name
+ secret_dict['payload'] = payload
+ secret_dict['payload_content_type'] = payload_content_type
+ secret_dict['payload_content_encoding'] = payload_content_encoding
+ secret_dict['algorithm'] = algorithm
+ secret_dict['mode'] = mode
+ secret_dict['bit_length'] = bit_length
+ secret_dict['expiration'] = expiration
+ self._remove_empty_keys(secret_dict)
+
+ LOG.debug("Request body: {0}".format(secret_dict))
+
+ resp = self.api.post(self.entity, secret_dict)
+ return resp['secret_ref']
+
+ def get(self, secret_ref):
+ """
+ Returns a Secret object with metadata about the secret.
+
+ :param secret_ref: The href for the secret
+ """
+ if not secret_ref:
+ raise ValueError('secret_ref is required.')
+ resp = self.api.get(secret_ref)
+ return Secret(resp)
+
+ def decrypt(self, secret_ref, content_type=None):
+ """
+ Returns the actual secret data stored in Barbican.
+
+ :param secret_ref: The href for the secret
+ :param content_type: The content_type of the secret, if not
+ provided, the client will fetch the secret meta and use the
+ default content_type to decrypt the secret
+ :returns: secret data
+ """
+ if not secret_ref:
+ raise ValueError('secret_ref is required.')
+ if not content_type:
+ secret = self.get(secret_ref)
+ content_type = secret.content_types['default']
+ headers = {'Accept': content_type}
+ return self.api.get_raw(secret_ref, headers)
+
+ def delete(self, secret_ref):
+ """
+ Deletes a secret
+
+ :param secret_ref: The href for the secret
+ """
+ if not secret_ref:
+ raise ValueError('secret_ref is required.')
+ self.api.delete(secret_ref)
+
+ def list(self, limit=10, offset=0):
+ """
+ List all secrets for the tenant
+
+ :param limit: Max number of secrets returned
+ :param offset: Offset secrets to begin list
+ :returns: list of Secret metadata objects
+ """
+ LOG.debug('Listing secrets - offset {0} limit {1}'.format(offset,
+ limit))
+ href = '{0}/{1}'.format(self.api.base_url, self.entity)
+ params = {'limit': limit, 'offset': offset}
+ resp = self.api.get(href, params)
+
+ return [Secret(s) for s in resp['secrets']]
diff --git a/tests/__init__.py b/barbicanclient/test/__init__.py
index e69de29..e69de29 100644
--- a/tests/__init__.py
+++ b/barbicanclient/test/__init__.py
diff --git a/barbicanclient/test/common/__init__.py b/barbicanclient/test/common/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/barbicanclient/test/common/__init__.py
diff --git a/barbicanclient/test/common/test_auth.py b/barbicanclient/test/common/test_auth.py
new file mode 100644
index 0000000..640301e
--- /dev/null
+++ b/barbicanclient/test/common/test_auth.py
@@ -0,0 +1,23 @@
+# Copyright (c) 2013 Rackspace, Inc.
+#
+# 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 unittest2 as unittest
+
+from barbicanclient.common import auth
+
+
+class WhenTestingKeystoneAuthentication(unittest.TestCase):
+ def test_endpoint_username_password_tenant_are_required(self):
+ with self.assertRaises(ValueError):
+ keystone = auth.KeystoneAuthV2()
diff --git a/barbicanclient/test/test_client.py b/barbicanclient/test/test_client.py
new file mode 100644
index 0000000..c464568
--- /dev/null
+++ b/barbicanclient/test/test_client.py
@@ -0,0 +1,93 @@
+# Copyright (c) 2013 Rackspace, Inc.
+#
+# 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 json
+
+import mock
+import unittest2 as unittest
+
+from barbicanclient import client
+from barbicanclient.common import auth
+
+
+class FakeAuth(object):
+ def __init__(self, auth_token, barbican_url, tenant_name, tenant_id):
+ self.auth_token = auth_token
+ self.barbican_url = barbican_url
+ self.tenant_name = tenant_name
+ self.tenant_id = tenant_id
+
+
+class WhenTestingClient(unittest.TestCase):
+ def setUp(self):
+ self.auth_endpoint = 'https://localhost:5000/v2.0/'
+ self.auth_token = 'fake_auth_token'
+ self.user = 'user'
+ self.password = 'password'
+ self.tenant_name = 'tenant'
+ self.tenant_id = 'tenant_id'
+
+ self.endpoint = 'http://localhost:9311/v1/'
+
+ self.fake_auth = FakeAuth(self.auth_token, self.endpoint,
+ self.tenant_name, self.tenant_id)
+
+ def test_can_be_used_without_auth_plugin(self):
+ c = client.Client(auth_plugin=None, endpoint=self.endpoint,
+ tenant_id=self.tenant_id)
+ self.assertNotIn('X-Auth-Token', c._session.headers)
+
+ def test_auth_token_header_is_set_when_using_auth_plugin(self):
+ c = client.Client(auth_plugin=self.fake_auth)
+ self.assertIn('X-Auth-Token', c._session.headers)
+ self.assertEqual(c._session.headers.get('X-Auth-Token'),
+ self.auth_token)
+
+ def test_error_thrown_when_no_auth_and_no_endpoint(self):
+ with self.assertRaises(ValueError):
+ c = client.Client(tenant_id=self.tenant_id)
+
+ def test_error_thrown_when_no_auth_and_no_tenant_id(self):
+ with self.assertRaises(ValueError):
+ c = client.Client(endpoint=self.endpoint)
+
+ def test_client_strips_trailing_slash_from_endpoint(self):
+ c = client.Client(endpoint=self.endpoint, tenant_id=self.tenant_id)
+ self.assertEqual(c._barbican_url, self.endpoint.strip('/'))
+
+ def test_base_url_ends_with_tenant_id(self):
+ c = client.Client(auth_plugin=self.fake_auth)
+ self.assertTrue(c.base_url.endswith(self.tenant_id))
+
+ def test_should_raise_for_unauthorized_response(self):
+ resp = mock.MagicMock()
+ resp.status_code = 401
+ c = client.Client(auth_plugin=self.fake_auth)
+ with self.assertRaises(client.HTTPAuthError):
+ c._check_status_code(resp)
+
+ def test_should_raise_for_server_error(self):
+ resp = mock.MagicMock()
+ resp.status_code = 500
+ c = client.Client(auth_plugin=self.fake_auth)
+ with self.assertRaises(client.HTTPServerError):
+ c._check_status_code(resp)
+
+ def test_should_raise_for_client_errors(self):
+ resp = mock.MagicMock()
+ resp.status_code = 400
+ c = client.Client(auth_plugin=self.fake_auth)
+ with self.assertRaises(client.HTTPClientError):
+ c._check_status_code(resp)
diff --git a/tests/keep_test.py b/barbicanclient/test/test_keep.py
index 0bcdea6..0bcdea6 100644
--- a/tests/keep_test.py
+++ b/barbicanclient/test/test_keep.py
diff --git a/barbicanclient/version.py b/barbicanclient/version.py
index 63159c1..54b6a44 100644
--- a/barbicanclient/version.py
+++ b/barbicanclient/version.py
@@ -17,5 +17,5 @@
Cloudkeep's Barbican Client version
"""
-__version__ = '0.3.0'
+__version__ = '0.4.0'
__version_info__ = tuple(__version__.split('.'))
diff --git a/setup.py b/setup.py
index 86b4fd6..96584ec 100644
--- a/setup.py
+++ b/setup.py
@@ -18,6 +18,7 @@
import os
import setuptools
+
name = 'python-barbicanclient'
@@ -46,15 +47,16 @@ setuptools.setup(
keywords="openstack encryption key-management secret",
url='https://github.com/cloudkeep/barbican',
license='Apache License (2.0)',
- author='OpenStack, LLC.',
- author_email='openstack-admins@lists.launchpad.net',
- packages=setuptools.find_packages(exclude=['tests', 'tests.*', 'examples', 'examples.*']),
+ author='Rackspace, Inc.',
+ author_email='openstack-dev@lists.openstack.org',
+ packages=setuptools.find_packages(
+ exclude=['tests', 'tests.*', 'examples', 'examples.*']
+ ),
install_requires=[
- 'eventlet>=0.12.1',
- 'httplib2>=0.7.7',
'argparse>=1.2.1',
- 'python-keystoneclient>=0.2.3',
- 'iso8601>=0.1.4'
+ 'eventlet>=0.13.0',
+ 'requests>=1.2.3',
+ 'python-keystoneclient>=0.3.2',
],
test_suite='nose.collector',
tests_require=['nose'],
diff --git a/tests/client_test.py b/tests/client_test.py
deleted file mode 100644
index fcc24e5..0000000
--- a/tests/client_test.py
+++ /dev/null
@@ -1,359 +0,0 @@
-# Copyright (c) 2013 Rackspace, Inc.
-#
-# 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 json
-import unittest2 as unittest
-
-from mock import MagicMock
-
-from barbicanclient import client
-from barbicanclient.common.exceptions import ClientException
-
-
-def suite():
- suite = unittest.TestSuite()
-
- suite.addTest(WhenTestingConnection())
-
- return suite
-
-
-class WhenTestingConnection(unittest.TestCase):
- def setUp(self):
- self.auth_endpoint = 'https://keystone.com/v2'
- self.user = 'user'
- self.key = 'key'
- self.tenant = 'tenant'
- self.endpoint = 'http://localhost:9311/v1/'
- self.auth_token = 'token'
- self.href = 'http://localhost:9311/v1/12345/orders'
-
- self.fake_env = MagicMock()
- self.fake_env.return_value = None
- self.authenticate = MagicMock()
- self.authenticate.return_value = (self.endpoint, self.auth_token)
- self.request = MagicMock()
- self.request.return_value.content = json.dumps(
- {
- "secret_ref": "http://localhost:9311/None/secrets"
- "/8502cea9-9d35-46d7-96f5-80e43905e4c5"
- }
- )
- self.request.return_value.headers = {
- 'content-length': '92',
- 'content-type': 'application/json; charset=utf-8',
- 'location': 'http://localhost:9311/None/'
- 'secrets/8502cea9-9d35-46d7-96f5-80e43905e4c5',
- 'x-openstack-request-id':
- 'req-6c19d09e-1167-445c-b435-d6b0818b59b9'
- }
- self.request.return_value.ok = True
- self.connection = client.Connection(self.auth_endpoint, self.user,
- self.key, self.tenant,
- token=self.auth_token,
- authenticate=self.authenticate,
- request=self.request,
- endpoint=self.endpoint)
-
- def test_should_connect_with_token(self):
- self.assertFalse(self.authenticate.called)
-
- def test_should_connect_without_token(self):
- self.connection = client.Connection(self.auth_endpoint,
- self.user,
- self.key,
- self.tenant,
- authenticate=self.authenticate,
- endpoint=self.endpoint)
- self.authenticate\
- .assert_called_once_with(self.auth_endpoint,
- self.user,
- self.key,
- self.tenant,
- service_type='key-store',
- endpoint=self.endpoint,
- cacert=None
- )
- self.assertEqual(self.auth_token, self.connection.auth_token)
- self.assertEqual(self.auth_endpoint, self.connection._auth_endpoint)
- self.assertEqual(self.user, self.connection._user)
- self.assertEqual(self.key, self.connection._key)
- self.assertEqual(self.tenant, self.connection._tenant)
- self.assertEqual(self.endpoint, self.connection._endpoint)
-
- def test_should_raise_for_bad_args(self):
- with self.assertRaises(ClientException):
- self.connection = client.Connection(None, self.user,
- self.key, self.tenant,
- fake_env=self.fake_env,
- token=self.auth_token,
- authenticate=self.authenticate,
- request=self.request,
- endpoint=self.endpoint)
-
- def test_should_create_secret(self):
- body = {'status': "ACTIVE",
- 'updated': '2013-06-07T16:13:38.889857',
- 'cypher_type': 'cbc',
- 'name': 'test_secret',
- 'algorithm': 'aes',
- 'created': '2013-06-07T16:13:38.889851',
- 'secret_ref': 'http://localhost:9311/v1/None/secrets/e6e7d'
- 'b5e-3738-408e-aaba-05a7177cade5',
- 'expiration': '2015-06-07T16:13:38.889851',
- 'bit_length': 256,
- 'payload_content_type': 'text/plain'
- }
-
- secret = client.Secret(self.connection, body)
- self.request.return_value.content = json.dumps(body)
- created = self.connection.create_secret(name='test_secret',
- payload='Test secret',
- algorithm='aes',
- bit_length=256,
- cypher_type='cbc',
- expiration='2015-06-07T16:13'
- ':38.889851',
- payload_content_type=
- 'text/plain')
- self.assertTrue(self._are_equivalent(secret, created))
-
- def test_should_create_order(self):
- body = {"status": "ACTIVE",
- "secret_ref": "http://localhost:9311/v1/12345/secrets/5706054"
- "9-2fcf-46eb-92bb-bf49fcf5d089",
- "updated": "2013-06-07T19:00:37.338386",
- "created": "2013-06-07T19:00:37.298704",
- "secret": {
- 'cypher_type': 'cbc',
- 'name': 'test_secret',
- 'algorithm': 'aes',
- 'created': '2013-06-07T16:13:38.889851',
- 'expiration': '2015-06-07T16:13:38.889851',
- 'bit_length': 256,
- 'payload_content_type': 'application/octet-stream'
- },
- "order_ref": "http://localhost:9311/v1/12345/orders/003f2b91-"
- "2f53-4c0a-a0f3-33796671efc3"
- }
-
- order = client.Order(self.connection, body)
- self.request.return_value.content = json.dumps(body)
- created = self.connection.create_order(name='test_secret',
- payload_content_type='application/octet-stream',
- algorithm='aes',
- bit_length=256,
- cypher_type='cbc')
- self.assertTrue(self._are_equivalent(order, created))
-
- def test_list_no_secrets(self):
- body0 = {'secrets': []}
- secrets = []
- self.request.return_value.content = json.dumps(body0)
- secret_list, prev_ref, next_ref = self.connection.list_secrets(0, 0)
- self.assertTrue(self._are_equivalent(secrets, secret_list))
- self.assertIsNone(prev_ref)
- self.assertIsNone(next_ref)
-
- def test_list_single_secret(self):
- limit = 1
- body1 = {'secrets': [{'status': 'ACTIVE',
- 'content_types': {'default': 'text/plain'},
- 'updated': '2013-06-03T21:16:58.349230',
- 'cypher_type': None,
- 'name': 'test_1',
- 'algorithm': None,
- 'created': '2013-06-03T21:16:58.349222',
- 'secret_ref': 'http://localhost:9311/v1/'
- 'None/secrets/bbd2036f-730'
- '7-4090-bbef-bbb6025e5e7b',
- 'expiration': None,
- 'bit_length': None,
- 'mime_type': 'text/plain'}],
- 'next': "{0}/{1}?limit={2}&offset={2}".format(self.connection.
- _tenant,
- self.connection.
- SECRETS_PATH,
- limit)}
- secrets = [client.Secret(self.connection, body1['secrets'][0])]
- self.request.return_value.content = json.dumps(body1)
- secret_list, prev_ref, next_ref = self.connection.list_secrets(limit,
- 0)
- self.assertTrue(self._are_equivalent(secrets, secret_list))
- self.assertIsNone(prev_ref)
- self.assertEqual(body1['next'], next_ref)
-
- def test_list_multiple_secrets(self):
- limit = 2
- body1 = {'secrets': [{'status': 'ACTIVE',
- 'content_types': {'default': 'text/plain'},
- 'updated': '2013-06-03T21:16:58.349230',
- 'cypher_type': None,
- 'name': 'test_1',
- 'algorithm': None,
- 'created': '2013-06-03T21:16:58.349222',
- 'secret_ref': 'http://localhost:9311/v1/'
- 'None/secrets/bbd2036f-730'
- '7-4090-bbef-bbb6025e5e7b',
- 'expiration': None,
- 'bit_length': None,
- 'mime_type': 'text/plain'}],
- 'previous': "{0}/{1}?limit={2}&offset={2}".format(
- self.connection._tenant,
- self.connection.
- SECRETS_PATH,
- limit)}
-
- body2 = body1
- body2['secrets'][0]['name'] = 'test_2'
- body2['secrets'][0]['secret_ref'] = 'http://localhost:9311/v1/No'\
- + 'ne/secrets/bbd2036f-7307-'\
- + '4090-bbef-bbb6025eabcd'
- body2['previous'] = 'http://localhost:9311/v1/None/secrets/19106'\
- + 'b6e-4ef1-48d1-8950-170c1a5838e1'
- body2['next'] = None
-
- secrets = [client.Secret(self.connection, b['secrets'][0])
- for b in (body1, body2)]
- body2['secrets'].insert(0, body1['secrets'][0])
- self.request.return_value.content = json.dumps(body2)
- secret_list, prev_ref, next_ref = self.connection.list_secrets(limit,
- 1)
- self.assertTrue(self._are_equivalent(secrets, secret_list))
- self.assertEqual(body2['previous'], prev_ref)
- self.assertIsNone(next_ref)
-
- def test_list_no_orders(self):
- body0 = {'orders': []}
- orders = []
- self.request.return_value.content = json.dumps(body0)
- order_list, prev_ref, next_ref = self.connection.list_orders(0, 0)
- self.assertTrue(self._are_equivalent(orders, order_list))
- self.assertIsNone(prev_ref)
- self.assertIsNone(next_ref)
-
- def test_list_single_order(self):
- limit = 1
- body1 = {'orders': [{'status': 'PENDING',
- 'updated': '2013-06-05T15:15:30.904760',
- 'created': '2013-06-05T15:15:30.904752',
- 'order_ref': 'http://localhost:9311/v1/'
- 'None/orders/9f651441-3ccd'
- '-45b3-bc60-3051656d5168',
- 'secret_ref': 'http://localhost:9311/'
- 'v1/None/secrets/????',
- 'secret': {'cypher_type': None,
- 'name': 'test_1',
- 'algorithm': None,
- 'expiration': None,
- 'bit_length': None,
- 'mime_type': 'text/plain'}}],
- 'next': "{0}/{1}?limit={2}&offset={2}".format(self.connection.
- _tenant,
- self.connection.
- ORDERS_PATH,
- limit)}
- orders = [client.Order(self.connection, body1['orders'][0])]
- self.request.return_value.content = json.dumps(body1)
- order_list, prev_ref, next_ref = self.connection.list_orders(limit, 0)
- self.assertTrue(self._are_equivalent(orders, order_list))
- self.assertIsNone(prev_ref)
- self.assertEqual(body1['next'], next_ref)
-
- def test_list_multiple_orders(self):
- limit = 2
- body1 = {'orders': [{'status': 'PENDING',
- 'updated': '2013-06-05T15:15:30.904760',
- 'created': '2013-06-05T15:15:30.904752',
- 'order_ref': 'http://localhost:9311/v1/'
- 'None/orders/9f651441-3ccd'
- '-45b3-bc60-3051656d5168',
- 'secret_ref': 'http://localhost:9311/'
- 'v1/None/secrets/????',
- 'secret': {'cypher_type': None,
- 'name': 'test_1',
- 'algorithm': None,
- 'expiration': None,
- 'bit_length': None,
- 'mime_type': 'text/plain'}}],
- 'previous': "{0}/{1}?limit={2}&offset={2}".format(
- self.connection._tenant,
- self.connection.
- SECRETS_PATH,
- limit)}
- body2 = body1
- body2['orders'][0]['order_ref'] = 'http://localhost:9311/v1/No'\
- + 'ne/orders/9f651441-3ccd-4'\
- + '5b3-bc60-3051656382fj'
- body2['orders'][0]['secret']['name'] = 'test_2'
-
- body2['orders'][0]['name'] = 'test_2'
- body2['orders'][0]['secret_ref'] = 'http://localhost:9311/v1/No'\
- + 'ne/secrets/bbd2036f-7307-'\
- + '4090-bbef-bbb6025eabcd'
- body2['previous'] = 'http://localhost:9311/v1/None/orders/19106'\
- + 'b6e-4ef1-48d1-8950-170c1a5838e1'
- body2['next'] = None
-
- orders = [client.Order(self.connection, b['orders'][0])
- for b in (body1, body2)]
- body2['orders'].insert(0, body1['orders'][0])
- self.request.return_value.content = json.dumps(body2)
- order_list, prev_ref, next_ref = self.connection.list_orders(limit, 1)
- self.assertTrue(self._are_equivalent(orders, order_list))
- self.assertEqual(body2['previous'], prev_ref)
- self.assertIsNone(next_ref)
-
- def test_should_get_response(self):
- self._setup_request()
- headers, body = self.connection._perform_http('GET', self.href)
- self.assertEqual(self.request.return_value.headers, headers)
- self.assertEqual(json.loads(self.request.return_value.content), body)
-
- def test_should_parse_json(self):
- self._setup_request()
- headers, body = self.connection._perform_http('GET', self.href,
- parse_json=True)
- self.assertEqual(json.loads(self.request.return_value.content), body)
-
- def test_should_not_parse_json(self):
- self._setup_request()
- headers, body = self.connection._perform_http('GET', self.href,
- parse_json=False)
- self.assertEqual(self.request.return_value.content, body)
-
- def test_should_raise_for_bad_response(self):
- self._setup_request()
- self.request.return_value.ok = False
- self.request.return_value.status_code = 404
- with self.assertRaises(ClientException) as e:
- self.connection._perform_http('GET', self.href)
- exception = e.exception
- self.assertEqual(404, exception.http_status)
-
- def _setup_request(self):
- self.request.return_value.headers = {'Accept': 'application/json'}
- self.request.return_value.content = '{"test": "response"}'
- self.href = 'http://localhost:9311/v1/12345/orders'
-
- def _are_equivalent(self, a, b):
- if isinstance(a, list) and isinstance(b, list):
- return all([self._are_equivalent(x, y) for x, y in zip(a, b)])
- else:
- return (a.__dict__ == b.__dict__)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/tools/hacking.sh b/tools/hacking.sh
new file mode 100755
index 0000000..967902b
--- /dev/null
+++ b/tools/hacking.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+flake8 barbicanclient | tee flake8.log
+exit ${PIPESTATUS[0]}
diff --git a/tools/pip-requires b/tools/pip-requires
index 9f26f41..3b29530 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -1,6 +1,4 @@
-httplib2>=0.7.7
argparse>=1.2.1
-python-keystoneclient>=0.2.3
-eventlet>=0.12.1
-oslo.config>=1.1.0
-iso8601>=0.1.4 \ No newline at end of file
+eventlet>=0.13.0
+requests>=1.2.3
+python-keystoneclient>=0.3.2
diff --git a/tools/test-requires b/tools/test-requires
index c3f8fb2..3c5872c 100644
--- a/tools/test-requires
+++ b/tools/test-requires
@@ -1,20 +1,6 @@
-distribute>=0.6.24
-
-# Install bounded pep8/pyflakes first, then let flake8 install
-pep8==1.4.5
-pyflakes==0.7.2
-flake8==2.0
-hacking>=0.5.3,<0.6
-
-
-coverage
-discover
-mox
+hacking>=0.7.0
mock>=1.0.1
-sphinx>=1.1.2
-
-nose>=1.2.1
-nosexcover>=1.0.7
-openstack.nose_plugin>=0.11
+nose>=1.3.0
+nosexcover>=1.0.8
+tox>=1.6.0
unittest2>=0.5.1
-tox
diff --git a/tox.ini b/tox.ini
index e61c727..2f35b9a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,7 @@
# and then run "tox" from this directory.
[tox]
-envlist = py26, py27
+envlist = py26, py27, pep8
[testenv]
setenv = VIRTUAL_ENV={envdir}
@@ -18,7 +18,7 @@ deps = -r{toxinidir}/tools/pip-requires
-r{toxinidir}/tools/test-requires
[testenv:pep8]
-commands = flake8
+commands = {toxinidir}/tools/hacking.sh
[testenv:venv]
commands = {posargs}
@@ -35,7 +35,7 @@ show-source = True
exclude = .venv,.tox,dist,doc,*egg
[testenv:py26]
-commands = nosetests {posargs:--with-xcoverage --all-modules --cover-inclusive --traverse-namespace --with-xunit --cover-package=barbican}
+commands = nosetests {posargs:--with-xcoverage --all-modules --cover-inclusive --traverse-namespace --with-xunit --cover-package=barbicanclient}
[testenv:py27]
-commands = nosetests {posargs:--with-xcoverage --all-modules --cover-inclusive --traverse-namespace --with-xunit --cover-package=barbican}
+commands = nosetests {posargs:--with-xcoverage --all-modules --cover-inclusive --traverse-namespace --with-xunit --cover-package=barbicanclient}