Merge pull request #8 from Aghoreyshi/master

Add Paging and Keep
This commit is contained in:
John Wood
2013-06-20 07:30:19 -07:00
7 changed files with 396 additions and 42 deletions

View File

@@ -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)

View File

@@ -36,4 +36,11 @@ class Order(object):
self.connection.delete_order(self)
def __str__(self):
return "<Order %s>" % 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)
)

View File

@@ -39,4 +39,16 @@ class Secret(object):
return self._id
def __str__(self):
return "<Secret %s>" % 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)
)

185
keep Executable file
View File

@@ -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()

View File

@@ -54,5 +54,6 @@ setuptools.setup(
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.7',
'Environment :: No Input/Output (Daemon)',
]
],
scripts = ['keep']
)

View File

@@ -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'}

View File

@@ -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}