diff --git a/barbicanclient/client.py b/barbicanclient/client.py index 1020f5a4..acf77240 100644 --- a/barbicanclient/client.py +++ b/barbicanclient/client.py @@ -4,7 +4,6 @@ eventlet.monkey_patch(socket=True, select=True) import json import requests -from barbicanclient.common import config from barbicanclient.secrets import Secret from barbicanclient.orders import Order from barbicanclient.common import auth @@ -15,9 +14,8 @@ from openstack.common.timeutils import parse_isotime from urlparse import urljoin -config.parse_args() -log.setup('barbicanclient') LOG = log.getLogger(__name__) +log.setup('barbicanclient') class Connection(object): @@ -80,7 +78,6 @@ class Connection(object): self._session.verify = True if token: - LOG.warn(_("Bypassing authentication - using provided token")) self.auth_token = token else: LOG.debug(_("Authenticating token")) @@ -105,22 +102,43 @@ class Connection(object): self._token = value self._session.headers['X-Auth-Token'] = value - def list_secrets(self): + def list_secrets(self, limit=10, offset=0): """ - Returns the list of secrets for the auth'd tenant + 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. """ - LOG.debug(_("Listing secrets")) - href = "{0}/{1}?limit=100".format(self._tenant, self.SECRETS_PATH) + 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. + """ + 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 = [] - for s in secrets_dict: - secrets.append(Secret(self._conn, s)) + secrets = [Secret(self._conn, s) for s in secrets_dict] - return secrets + prev_ref = body.get('previous') + + next_ref = body.get('next') + + return secrets, prev_ref, next_ref def create_secret(self, mime_type, @@ -130,6 +148,18 @@ class Connection(object): bit_length=None, cypher_type=None, expiration=None): + """ + Creates and returns a Secret object with all of its metadata filled in. + + arguments: + mime_type - The MIME type of the secret + plain_text - The unencrypted secret + name - A friendly name for the secret + algorithm - The algorithm the secret is used with + bit_length - The bit length of the secret + cypher_type - The cypher type (e.g. block cipher mode of operation) + expiration - The expiration time for the secret in ISO 8601 format + """ LOG.debug(_("Creating secret of mime_type {0}").format(mime_type)) href = "{0}/{1}".format(self._tenant, self.SECRETS_PATH) LOG.debug(_("href: {0}").format(href)) @@ -154,52 +184,91 @@ class Connection(object): return self.get_secret(body['secret_ref']) def delete_secret_by_id(self, secret_id): + """ + Deletes a secret using its UUID + """ 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 using its full reference + """ 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 using the secret's UUID + """ 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 using the secret's full reference + """ 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, mime_type): + """ + Returns the raw secret using the secret's UUID and MIME type + """ 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, mime_type) def get_raw_secret(self, href, mime_type): + """ + Returns the raw secret using the secret's UUID and MIME type + """ hdrs = {"Accept": mime_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): + def list_orders(self, limit=10, offset=0): """ - Returns the list of orders + 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. """ - LOG.debug(_("Listing orders")) - href = "{0}/{1}?limit=100".format(self._tenant, self.ORDERS_PATH) + 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. + """ + 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 = [] - for o in orders_dict: - orders.append(Order(self._conn, o)) + orders = [Order(self._conn, o) for o in orders_dict] - return orders + prev_ref = body.get('previous') + + next_ref = body.get('next') + + return orders, prev_ref, next_ref def create_order(self, mime_type, @@ -207,6 +276,16 @@ class Connection(object): algorithm=None, bit_length=None, cypher_type=None): + """ + Creates and returns an Order object with all of its metadata filled in. + + arguments: + mime_type - The MIME type of the secret + name - A friendly name for the secret + algorithm - The algorithm the secret is used with + bit_length - The bit length of the secret + cypher_type - The cypher type (e.g. block cipher mode of operation) + """ LOG.debug(_("Creating order of mime_type {0}").format(mime_type)) href = "{0}/{1}".format(self._tenant, self.ORDERS_PATH) LOG.debug("href: {0}".format(href)) @@ -227,20 +306,32 @@ class Connection(object): return self.get_order(body['order_ref']) def delete_order_by_id(self, order_id): + """ + Deletes an order using its UUID + """ 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 using its full reference + """ 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 using the order's UUID + """ 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 using the order's full reference + """ hdrs, body = self._perform_http(href=href, method='GET') LOG.debug(_("Response - headers: {0}\nbody: {1}").format(hdrs, body)) return Order(self._conn, body) diff --git a/barbicanclient/orders.py b/barbicanclient/orders.py index b23e329a..3345e458 100644 --- a/barbicanclient/orders.py +++ b/barbicanclient/orders.py @@ -36,4 +36,11 @@ class Order(object): self.connection.delete_order(self) def __str__(self): - return "" % self.id + 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) + ) diff --git a/barbicanclient/secrets.py b/barbicanclient/secrets.py index 293771cf..53e69736 100644 --- a/barbicanclient/secrets.py +++ b/barbicanclient/secrets.py @@ -39,4 +39,16 @@ class Secret(object): return self._id def __str__(self): - return "" % self.id + return ("Secret - ID: {0}\n" + " reference: {1}\n" + " name: {2}\n" + " created: {3}\n" + " MIME type: {4}\n" + " status: {5}\n" + " bit length: {6}\n" + " algorithm: {7}\n" + " cypher type: {8}\n" + .format(self.id, self.secret_ref, self.name, self.created, + self.mime_type, self.status, self.bit_length, + self.algorithm, self.cypher_type) + ) diff --git a/keep b/keep new file mode 100755 index 00000000..adcd4a14 --- /dev/null +++ b/keep @@ -0,0 +1,185 @@ +#!/usr/bin/env python + +import argparse +import os + +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=env('OS_AUTH_URL'), + help='the URL to authenticate against (default: ' + '%(default)s)') + parser.add_argument('--user', '-U', default=env('OS_USERNAME'), + help='the user to authenticate as (default: %(de' + 'fault)s)') + parser.add_argument('--password', '-P', default=env('OS_PASSWORD'), + help='the API key or password to authenticate with' + ' (default: %(default)s)') + parser.add_argument('--tenant', '-T', default=env('OS_TENANT_NAME'), + help='the tenant ID (default: %(default)s)') + parser.add_argument('--endpoint', '-E', default=env('SERVICE_ENDPOINT') + , help='the URL of the barbican server (default: %' + '(default)s)') + parser.add_argument('--token', '-K', default=env('SERVICE_TOKEN'), + help='the authentication token (default: %(default' + ')s)') + return parser + + def add_create_args(self): + create_parser = self.subparsers.add_parser('create', help='Create a ' + 'secret or an order') + create_parser.add_argument('--mime_type', '-m', default='text/plain', + help='the MIME type of the raw secret (defa' + 'ult: %(default)s)') + create_parser.add_argument('--name', '-n', help='a human-friendly name' + ' used only for reference') + create_parser.add_argument('--algorithm', '-a', help='the algorithm us' + 'ed only for reference') + create_parser.add_argument('--bit_length', '-b', default=256, + help='the bit length of the secret used ' + 'only for reference (default: %(default)s)', + type=int) + create_parser.add_argument('--cypher_type', '-c', help='the cypher typ' + 'e used only for reference') + create_parser.add_argument('--plain_text', '-p', help='the unencrypted' + ' secret (only used for secrets)') + create_parser.add_argument('--expiration', '-e', help='the expiration ' + 'time for the secret in ISO 8601 format ' + '(only used for secrets)') + 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') + 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 --mime_type (' + 'only used for secrets)', action='store_true') + get_parser.add_argument('--mime_type', '-m', default='text/plain', + help='the MIME type of the raw secret (defa' + 'ult: %(default)s; only used for secrets)') + get_parser.set_defaults(func=self.get) + + 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' + 'he limit to the number of items to list per' + ' page (default: %(default)s; maximum: 100)', + type=int) + 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) + + def create(self, args): + if args.type == 'secret': + secret = self.conn.create_secret(args.mime_type, + args.plain_text, + args.name, + args.algorithm, + args.bit_length, + args.cypher_type, + args.expiration) + print secret + else: + order = self.conn.create_order(args.mime_type, + args.name, + args.algorithm, + args.bit_length, + args.cypher_type) + print order + + def delete(self, args): + if args.type == 'secret': + self.conn.delete_secret_by_id(args.UUID) + else: + self.conn.delete_order_by_id(args.UUID) + + def get(self, args): + if args.type == 'secret': + if args.raw: + print self.conn.get_raw_secret_by_id(args.UUID, args.mime_type) + else: + print self.conn.get_secret_by_id(args.UUID) + else: + print self.conn.get_order_by_id(args.UUID) + + 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) + 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 'Displayed {0} {1}s - offset: {2}'.format(len(l[0]), args.type, + args.offset) + + def execute(self): + args = self.parser.parse_args() + self.conn = client.Connection(args.auth_endpoint, args.user, + args.password, args.tenant, + args.token, + endpoint=args.endpoint) + args.func(args) + + +def env(*vars, **kwargs): + """Search for the first defined of possibly many env vars + + Returns the first environment variable defined in vars, or + returns the default defined in kwargs. + + Source: Keystone's shell.py + """ + for v in vars: + value = os.environ.get(v, None) + if value: + return value + return kwargs.get('default', '') + + +def main(): + k = Keep() + k.execute() + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 23a56464..6081cad9 100644 --- a/setup.py +++ b/setup.py @@ -54,5 +54,6 @@ setuptools.setup( 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', 'Environment :: No Input/Output (Daemon)', - ] + ], + scripts = ['keep'] ) diff --git a/tests/client_test.py b/tests/client_test.py index 28f08f63..44205c3b 100644 --- a/tests/client_test.py +++ b/tests/client_test.py @@ -14,7 +14,7 @@ # limitations under the License. import json -import unittest +import unittest2 as unittest from mock import MagicMock @@ -146,10 +146,13 @@ class WhenTestingConnection(unittest.TestCase): body0 = {'secrets': []} secrets = [] self.request.return_value.content = json.dumps(body0) - self.assertTrue(self._are_equivalent(secrets, - self.connection.list_secrets())) + 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', @@ -162,13 +165,22 @@ class WhenTestingConnection(unittest.TestCase): '7-4090-bbef-bbb6025e5e7b', 'expiration': None, 'bit_length': None, - 'mime_type': 'text/plain'}]} + '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) - self.assertTrue(self._are_equivalent(secrets, - self.connection.list_secrets())) + 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', @@ -181,29 +193,43 @@ class WhenTestingConnection(unittest.TestCase): '7-4090-bbef-bbb6025e5e7b', 'expiration': None, 'bit_length': None, - 'mime_type': 'text/plain'}]} + '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) - self.assertTrue(self._are_equivalent(secrets, - self.connection.list_secrets())) + 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) - self.assertTrue(self._are_equivalent(orders, - self.connection.list_orders())) + 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', @@ -217,13 +243,21 @@ class WhenTestingConnection(unittest.TestCase): 'algorithm': None, 'expiration': None, 'bit_length': None, - 'mime_type': 'text/plain'}}]} + '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) - self.assertTrue(self._are_equivalent(orders, - self.connection.list_orders())) + 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', @@ -237,19 +271,34 @@ class WhenTestingConnection(unittest.TestCase): 'algorithm': None, 'expiration': None, 'bit_length': None, - 'mime_type': 'text/plain'}}]} + '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) - self.assertTrue(self._are_equivalent(orders, - self.connection.list_orders())) + 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() @@ -272,8 +321,11 @@ class WhenTestingConnection(unittest.TestCase): def test_should_raise_exception(self): self._setup_request() self.request.return_value.ok = False - with self.assertRaises(ClientException): + 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'} diff --git a/tox.ini b/tox.ini index 2a3e4941..e61c7277 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py26, py27, py33, pep8 +envlist = py26, py27 [testenv] setenv = VIRTUAL_ENV={envdir} @@ -33,3 +33,9 @@ downloadcache = ~/cache/pip ignore = F,H 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} + +[testenv:py27] +commands = nosetests {posargs:--with-xcoverage --all-modules --cover-inclusive --traverse-namespace --with-xunit --cover-package=barbican}