Introduce cliff for cli framework

This patch introduces the cliff framework from oslo.
The same functionality from the old command line interface was
replicated, and a few orthographic errors where corrected from the
command help.

The application structure was made so that it resembles the way
python-openstackclient was done, this will enable the code to be
integrated to that client in a more straight forward way. Same with
the naming of classes within the sub-commands. For example, the list
commands are List<Entity> and not List<Entities>; this was done in
such way because it is done that way in python-openstack client;
and the aim of this commit is to produce functional code, and at the
same time, follow their standards to be able to integrate this
there without much hassle.

NOTE: Only secrets and orders were added, since verifications are no
      longer in use

Implements: blueprint cliff-for-python-barbicanclient
Change-Id: Ice2dddf418dfb76a616b65f22dc8dfd7ef4df36f
This commit is contained in:
Juan Antonio Osorio
2014-07-16 16:29:37 +03:00
parent ac30643631
commit 9f0452fdc0
8 changed files with 388 additions and 292 deletions

View File

@@ -19,43 +19,39 @@ Command-line interface to the Barbican API.
import argparse
import logging
import sys
from cliff import app
from cliff import commandmanager
from barbicanclient.common import auth
from barbicanclient import client
from barbicanclient import version
class Barbican:
class Barbican(app.App):
"""Barbican comand line interface."""
def __init__(self):
self.parser = self._get_main_parser()
self.subparsers = self.parser.add_subparsers(
title='subcommands',
metavar='<action>',
description='Action to perform'
def __init__(self, **kwargs):
super(Barbican, self).__init__(
description=__doc__.strip(),
version=version.__version__,
command_manager=commandmanager.CommandManager('barbican.client'),
**kwargs
)
self._add_create_args()
self._add_store_args()
self._add_get_args()
self._add_list_args()
self._add_verify_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', 'verification'],
help='Entity used for command, e.g.,'
' order, secret, verification.')
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].')
def build_option_parser(self, description, version, argparse_kwargs=None):
"""Introduces global arguments for the application.
This is inherited from the framework.
"""
parser = super(Barbican, self).build_option_parser(
description, version, argparse_kwargs)
parser.add_argument('--no-auth', '-N', action='store_true',
help='Do not use authentication.')
parser.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'),
@@ -120,242 +116,27 @@ class Barbican:
'option should be used with caution.')
return parser
def _add_verify_args(self):
verify_parser = self.subparsers.add_parser('verify',
help='Create a new '
'verification.')
verify_parser.add_argument('--type', '-t', default='image',
help='resource type to verify, '
'such as "image".')
def _assert_no_auth_and_auth_url_mutually_exclusive(self, no_auth,
auth_url):
if no_auth and auth_url:
raise Exception("ERROR: argument --os-auth-url/-A: not allowed "
"with argument --no-auth/-N")
verify_parser.add_argument('--ref', '-r',
help='reference URI to '
'resource to verify.')
verify_parser.add_argument('--action', '-a', default='vm_attach',
help='action to perform on '
'resource, such as "vm_attach".')
verify_parser.add_argument('--impersonation', '-i', default=True,
help='is impersonation allowed '
'for the resource.')
verify_parser.set_defaults(func=self.verify)
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='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('--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_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, order or '
'verification by providing its href.'
)
delete_parser.add_argument('URI', help='The URI reference for the'
' secret, order '
'or verification')
delete_parser.set_defaults(func=self.delete)
def _add_get_args(self):
get_parser = self.subparsers.add_parser(
'get',
help='Retrieve a secret, order or '
'verification by providing its URI.'
)
get_parser.add_argument('URI', help='The URI reference '
'for the secret, '
'order or verification.')
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 decrypted'
' secret (default: %(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, '
'orders or '
'verifications')
list_parser.add_argument('--limit', '-l', default=10, help='specify '
'the 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('--name', '-n', default=None, help='specify t'
'he secret name (default: %(default)s)')
list_parser.add_argument('--algorithm', '-a', default=None,
help='the algorithm filter for the list'
'(default: %(default)s).')
list_parser.add_argument('--bit-length', '-b', default=0,
help='the bit length filter for the list'
' (default: %(default)s).',
type=int)
list_parser.add_argument('--mode', '-m', default=None,
help='the algorithmm mode filter for the'
' list (default: %(default)s).')
list_parser.set_defaults(func=self.list)
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:
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.command == 'secret':
self.client.secrets.delete(args.URI)
elif args.command == 'verification':
self.client.verifications.delete(args.URI)
elif args.command == 'order':
self.client.orders.delete(args.URI)
else:
self.parser.exit(status=1, message='ERROR: delete is only '
'supported for secrets, '
'orders or verifications\n')
def get(self, args):
if args.command == 'secret':
if args.decrypt:
print(self.client.secrets.decrypt(args.URI,
args.payload_content_type))
else:
print(self.client.secrets.get(args.URI))
elif args.command == 'verification':
print(self.client.verifications.get(args.URI))
elif args.command == 'order':
print(self.client.orders.get(args.URI))
else:
self.parser.exit(status=1, message='ERROR: get is only '
'supported for secrets, '
'orders or verifications\n')
def list(self, args):
if args.command == 'secret':
ls = self.client.secrets.list(limit=args.limit,
offset=args.offset,
name=args.name,
mode=args.mode,
algorithm=args.algorithm,
bits=args.bit_length)
elif args.command == 'verification':
ls = self.client.verifications.list(args.limit, args.offset)
elif args.command == 'order':
ls = self.client.orders.list(args.limit, args.offset)
else:
self.parser.exit(status=1, message='ERROR: get list is only '
'supported for secrets, '
'orders or verifications\n')
for obj in ls:
print(obj)
print('{0}s displayed: {1} - offset: {2}'.format(args.command, len(ls),
args.offset))
def verify(self, args):
if args.command == 'verification':
verify = self.client.verifications\
.create(resource_type=args.type,
resource_ref=args.ref,
resource_action=args.action,
impersonation_allowed=args.impersonation)
print(verify)
else:
self.parser.exit(status=1, message='ERROR: verify is only '
'supported for verifications\n')
def execute(self, **kwargs):
args = self.parser.parse_args(kwargs.get('argv'))
def initialize_app(self, argv):
"""Initializes the application.
Checks if the minimal parameters are provided and creates the client
interface.
This is inherited from the framework.
"""
args = self.options
self._assert_no_auth_and_auth_url_mutually_exclusive(args.no_auth,
args.os_auth_url)
if args.no_auth:
if not all([args.endpoint, args.os_tenant_id or
args.os_project_id]):
self.parser.exit(
status=1,
message='ERROR: please specify --endpoint and '
'--os-project-id(or --os-tenant-id)\n')
raise Exception(
'ERROR: please specify --endpoint and '
'--os-project-id(or --os-tenant-id)')
self.client = client.Client(endpoint=args.endpoint,
tenant_id=args.os_tenant_id or
args.os_project_id,
@@ -370,17 +151,14 @@ class Barbican:
args.os_project_id,
insecure=args.insecure)
else:
self.parser.exit(
status=1,
message='ERROR: please specify authentication credentials\n'
)
args.func(args)
self.stderr.write(self.parser.format_usage())
raise Exception('ERROR: please specify authentication credentials')
def main():
k = Barbican()
k.execute()
def main(argv=sys.argv[1:]):
barbican_app = Barbican()
return barbican_app.run(argv)
if __name__ == '__main__':
main()
sys.exit(main(sys.argv[1:]))

View File

View File

@@ -0,0 +1,23 @@
# 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.
class EntityFormatter(object):
""" Helper class with the purpose of formatting entities for display."""
def _list_objects(self, obj_list):
data = (self._get_formatted_data(obj) for obj in obj_list)
return (self.columns, data)
def _get_formatted_entity(self, entity):
return (self.columns, self._get_formatted_data(entity))

View File

@@ -0,0 +1,121 @@
# 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 sub-commands related to orders.
"""
from cliff import command
from cliff import lister
from cliff import show
from barbicanclient.barbican_cli.formatter import EntityFormatter
class OrderFormatter(EntityFormatter):
columns = ("Order href",
"Secret href",
"Created",
"Status",
)
def _get_formatted_data(self, entity):
data = (entity.order_ref,
entity.secret_ref,
entity.created,
entity.status,
)
return data
class CreateOrder(show.ShowOne, OrderFormatter):
"""Create a new order."""
def get_parser(self, prog_name):
parser = super(CreateOrder, self).get_parser(prog_name)
parser.add_argument('--name', '-n',
help='a human-friendly name.')
parser.add_argument('--algorithm', '-a', default='aes',
help='the algorithm to be used with the '
'requested key (default: '
'%(default)s).')
parser.add_argument('--bit-length', '-b', default=256,
help='the bit length of the requested'
' secret key (default: %(default)s).',
type=int)
parser.add_argument('--mode', '-m', default='cbc',
help='the algorithm mode to be used with '
'the requested key (default: %(default)s).')
parser.add_argument('--payload-content-type', '-t',
default='application/octet-stream',
help='the type/format of the secret to be'
' generated (default: %(default)s).')
parser.add_argument('--expiration', '-x',
help='the expiration '
'time for the secret in ISO 8601 format.')
return parser
def take_action(self, args):
entity = self.app.client.orders.create(args.name,
args.payload_content_type,
args.algorithm,
args.bit_length,
args.mode,
args.expiration)
return self._get_formatted_entity(entity)
class DeleteOrder(command.Command):
"""Delete an order by providing its href."""
def get_parser(self, prog_name):
parser = super(DeleteOrder, self).get_parser(prog_name)
parser.add_argument('URI', help='The URI reference for the order')
return parser
def take_action(self, args):
self.app.client.orders.delete(args.URI)
class GetOrder(show.ShowOne, OrderFormatter):
"""Retrieve an order by providing its URI."""
def get_parser(self, prog_name):
parser = super(GetOrder, self).get_parser(prog_name)
parser.add_argument('URI', help='The URI reference order.')
return parser
def take_action(self, args):
entity = self.app.client.orders.get(args.URI)
return self._get_formatted_entity(entity)
class ListOrder(lister.Lister, OrderFormatter):
"""List orders."""
def get_parser(self, prog_name):
parser = super(ListOrder, self).get_parser(prog_name)
parser.add_argument('--limit', '-l', default=10,
help='specify the limit to the number of items '
'to list per page (default: %(default)s; '
'maximum: 100)',
type=int)
parser.add_argument('--offset', '-o', default=0,
help='specify the page offset '
'(default: %(default)s)',
type=int)
return parser
def take_action(self, args):
obj_list = self.app.client.orders.list(args.limit, args.offset)
return self._list_objects(obj_list)

View File

@@ -0,0 +1,169 @@
# 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 sub-commands related to secrets.
"""
from cliff import command
from cliff import lister
from cliff import show
from barbicanclient.barbican_cli.formatter import EntityFormatter
class SecretFormatter(EntityFormatter):
columns = ("Secret href",
"Name",
"Created",
"Status",
"Content types",
"Algorithm",
"Bit length",
"Mode",
"Expiration",
)
def _get_formatted_data(self, entity):
data = (entity.secret_ref,
entity.name,
entity.created,
entity.status,
entity.content_types,
entity.algorithm,
entity.bit_length,
entity.mode,
entity.expiration,
)
return data
class DeleteSecret(command.Command):
"""Delete an secret by providing its href."""
def get_parser(self, prog_name):
parser = super(DeleteSecret, self).get_parser(prog_name)
parser.add_argument('URI', help='The URI reference for the secret')
return parser
def take_action(self, args):
self.app.client.secrets.delete(args.URI)
class GetSecret(show.ShowOne, SecretFormatter):
"""Retrieve a secret by providing its URI."""
def get_parser(self, prog_name):
parser = super(GetSecret, self).get_parser(prog_name)
parser.add_argument('URI', help='The URI reference for the secret.')
parser.add_argument('--decrypt', '-d',
help='if specified, retrieve the '
'unencrypted secret data; '
'the data type can be specified with '
'--payload-content-type.',
action='store_true')
parser.add_argument('--payload_content_type', '-t',
default='text/plain',
help='the content type of the decrypted'
' secret (default: %(default)s.')
return parser
def take_action(self, args):
if args.decrypt:
entity = self.app.client.secrets.decrypt(args.URI,
args.payload_content_type)
return (('Secret',),
(entity,))
else:
entity = self.app.client.secrets.get(args.URI)
return self._get_formatted_entity(entity)
class ListSecret(lister.Lister, SecretFormatter):
"""List secrets."""
def get_parser(self, prog_name):
parser = super(ListSecret, self).get_parser(prog_name)
parser.add_argument('--limit', '-l', default=10,
help='specify the limit to the number of items '
'to list per page (default: %(default)s; '
'maximum: 100)',
type=int)
parser.add_argument('--offset', '-o', default=0,
help='specify the page offset '
'(default: %(default)s)',
type=int)
parser.add_argument('--name', '-n', default=None,
help='specify the secret name '
'(default: %(default)s)')
parser.add_argument('--algorithm', '-a', default=None,
help='the algorithm filter for the list'
'(default: %(default)s).')
parser.add_argument('--bit-length', '-b', default=0,
help='the bit length filter for the list'
' (default: %(default)s).',
type=int)
parser.add_argument('--mode', '-m', default=None,
help='the algorithm mode filter for the'
' list (default: %(default)s).')
return parser
def take_action(self, args):
obj_list = self.app.client.secrets.list(args.limit, args.offset,
args.name, args.mode,
args.algorithm,
args.bit_length)
return self._list_objects(obj_list)
class StoreSecret(show.ShowOne, SecretFormatter):
"""Store a secret in Barbican."""
def get_parser(self, prog_name):
parser = super(StoreSecret, self).get_parser(prog_name)
parser.add_argument('--name', '-n',
help='a human-friendly name.')
parser.add_argument('--payload', '-p',
help='the unencrypted secret; if provided, '
'you must also provide a '
'payload_content_type')
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.')
parser.add_argument('--payload-content-encoding', '-e',
help='required if --payload-content-type is '
'"application/octet-stream".')
parser.add_argument('--algorithm', '-a', default='aes',
help='the algorithm (default: '
'%(default)s).')
parser.add_argument('--bit-length', '-b', default=256,
help='the bit length '
'(default: %(default)s).',
type=int)
parser.add_argument('--mode', '-m', default='cbc',
help='the algorithm mode; used only for '
'reference (default: %(default)s)')
parser.add_argument('--expiration', '-x',
help='the expiration time for the secret in '
'ISO 8601 format.')
return parser
def take_action(self, args):
entity = self.app.client.secrets.store(
args.name, args.payload, args.payload_content_type,
args.payload_content_encoding, args.algorithm,
args.bit_length, args.mode, args.expiration)
return (('Secret',),
(entity,))

View File

@@ -31,37 +31,29 @@ class WhenTestingBarbicanCLI(test_client.BaseEntityResource):
def setUp(self):
self._setUp('barbican')
self.global_file = six.StringIO()
def barbican(self, argstr):
"""Source: Keystone client's shell method in test_shell.py"""
orig = sys.stdout
orig_err = sys.stderr
clean_env = {}
_old_env, os.environ = os.environ, clean_env.copy()
exit_code = 0
exit_code = 1
try:
sys.stdout = six.StringIO()
sys.stderr = sys.stdout
_barbican = barbicanclient.barbican.Barbican()
_barbican.execute(argv=argstr.split())
except SystemExit:
exc_type, exc_value, exc_traceback = sys.exc_info()
exit_code = exc_value.code
stdout = self.global_file
_barbican = barbicanclient.barbican.Barbican(stdout=stdout,
stderr=stdout)
exit_code = _barbican.run(argv=argstr.split())
except Exception as exception:
exit_message = exception.message
finally:
if exit_code == 0:
out = sys.stdout.getvalue()
else:
out = sys.stderr.getvalue()
sys.stdout.close()
sys.stdout = orig
sys.stderr = orig_err
out = stdout.getvalue()
os.environ = _old_env
return exit_code, out
def test_should_show_usage_error_with_no_args(self):
args = ""
exit_code, out = self.barbican(args)
self.assertEqual(2, exit_code)
self.assertEqual(1, exit_code)
self.assertIn('usage:', out)
def test_should_show_usage_with_help_flag(self):
@@ -73,9 +65,9 @@ class WhenTestingBarbicanCLI(test_client.BaseEntityResource):
def test_should_error_if_noauth_and_authurl_both_specified(self):
args = "--no-auth --os-auth-url http://localhost:5000/v3"
exit_code, out = self.barbican(args)
self.assertEqual(2, exit_code)
self.assertEqual(1, exit_code)
self.assertIn(
'error: argument --os-auth-url/-A: not allowed with '
'ERROR: argument --os-auth-url/-A: not allowed with '
'argument --no-auth/-N', out)
def _expect_error_with_invalid_noauth_args(self, args):
@@ -175,7 +167,7 @@ class TestBarbicanWithKeystoneClient(testtools.TestCase):
barbican_url,
v2_token['access']['token']['tenant']['id']),
status=200)
self.barbican.execute(argv=argv)
self.barbican.run(argv=argv)
@httpretty.activate
def test_v3_auth(self):
@@ -202,4 +194,4 @@ class TestBarbicanWithKeystoneClient(testtools.TestCase):
barbican_url,
v3_token['token']['project']['id']),
status=200)
self.barbican.execute(argv=argv)
self.barbican.run(argv=argv)

View File

@@ -3,3 +3,4 @@ argparse
requests>=1.2.3
six>=1.5.2
python-keystoneclient>=0.9.0
cliff==1.6.1

View File

@@ -26,6 +26,18 @@ packages =
console_scripts =
barbican = barbicanclient.barbican:main
barbican.client =
order_create = barbicanclient.barbican_cli.orders:CreateOrder
order_delete = barbicanclient.barbican_cli.orders:DeleteOrder
order_get = barbicanclient.barbican_cli.orders:GetOrder
order_list = barbicanclient.barbican_cli.orders:ListOrder
secret_delete = barbicanclient.barbican_cli.secrets:DeleteSecret
secret_get = barbicanclient.barbican_cli.secrets:GetSecret
secret_list = barbicanclient.barbican_cli.secrets:ListSecret
secret_store = barbicanclient.barbican_cli.secrets:StoreSecret
[build_sphinx]
source-dir = doc/source
build-dir = doc/build