diff options
author | John Wood <john.wood@rackspace.com> | 2013-09-05 14:43:19 -0700 |
---|---|---|
committer | John Wood <john.wood@rackspace.com> | 2013-09-05 14:43:19 -0700 |
commit | f93580b5c3bc3f0341395d98d49dc44f421f61b9 (patch) | |
tree | 9866cb815d20ddedaa8338d606114231c232858c | |
parent | 35e1ceb205d8078dfb9cb81cfe294c94b8400c8d (diff) | |
parent | ac5078433f237f2be09cb995c8440f61c6052335 (diff) | |
download | python-barbicanclient-f93580b5c3bc3f0341395d98d49dc44f421f61b9.tar.gz |
Merge pull request #17 from dmend/master
Client refactor for M3
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | README.md | 74 | ||||
-rw-r--r-- | barbicanclient/base.py | 38 | ||||
-rw-r--r-- | barbicanclient/client.py | 528 | ||||
-rw-r--r-- | barbicanclient/common/auth.py | 125 | ||||
-rw-r--r-- | barbicanclient/common/exceptions.py | 15 | ||||
-rw-r--r-- | barbicanclient/keep.py | 322 | ||||
-rw-r--r-- | barbicanclient/orders.py | 142 | ||||
-rw-r--r-- | barbicanclient/secrets.py | 186 | ||||
-rw-r--r-- | barbicanclient/test/__init__.py (renamed from tests/__init__.py) | 0 | ||||
-rw-r--r-- | barbicanclient/test/common/__init__.py | 0 | ||||
-rw-r--r-- | barbicanclient/test/common/test_auth.py | 23 | ||||
-rw-r--r-- | barbicanclient/test/test_client.py | 93 | ||||
-rw-r--r-- | barbicanclient/test/test_keep.py (renamed from tests/keep_test.py) | 0 | ||||
-rw-r--r-- | barbicanclient/version.py | 2 | ||||
-rw-r--r-- | setup.py | 16 | ||||
-rw-r--r-- | tests/client_test.py | 359 | ||||
-rwxr-xr-x | tools/hacking.sh | 3 | ||||
-rw-r--r-- | tools/pip-requires | 8 | ||||
-rw-r--r-- | tools/test-requires | 22 | ||||
-rw-r--r-- | tox.ini | 8 |
21 files changed, 878 insertions, 1089 deletions
@@ -26,6 +26,9 @@ pip-log.txt .tox nosetests.xml +# pyenv +.python-version + # Translations *.mo @@ -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('.')) @@ -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 @@ -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} |