From 5e4032150d360a305397e0220e51c5a66f2f5313 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Wed, 2 May 2012 17:02:08 -0400 Subject: [PATCH] Fix "help" command and implement "list server" and "show server" blueprint client-manager blueprint nova-client bug 992841 Move the authentication logic into a new ClientManager class so that only commands that need to authenticate will trigger that code. Implement "list server" and "show server" commands as examples of using the ClientManager, Lister, and ShowOne classes. Change-Id: I9845b70b33bae4b193dbe41871bf0ca8e286a727 --- .gitignore | 2 + openstackclient/common/clientmanager.py | 107 +++++++++++++++ openstackclient/compute/client.py | 32 +++++ openstackclient/compute/v2/server.py | 173 ++++++++++++++++++------ openstackclient/shell.py | 77 +++-------- tests/test_clientmanager_clientcache.py | 22 +++ tools/pip-requires | 1 + 7 files changed, 316 insertions(+), 98 deletions(-) create mode 100644 openstackclient/common/clientmanager.py create mode 100644 openstackclient/compute/client.py create mode 100644 tests/test_clientmanager_clientcache.py diff --git a/.gitignore b/.gitignore index 7b6d518c6a..159851432e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.log +*.log.* *.pyc *.swp *~ @@ -10,3 +11,4 @@ dist python_openstackclient.egg-info .tox/ ChangeLog +TAGS diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py new file mode 100644 index 0000000000..66d3ce7685 --- /dev/null +++ b/openstackclient/common/clientmanager.py @@ -0,0 +1,107 @@ +"""Manage access to the clients, including authenticating when needed. +""" + +import logging + +from openstackclient.common import exceptions as exc +from openstackclient.compute import client as compute_client + +from keystoneclient.v2_0 import client as keystone_client + +LOG = logging.getLogger(__name__) + + +class ClientCache(object): + """Descriptor class for caching created client handles. + """ + + def __init__(self, factory): + self.factory = factory + self._handle = None + + def __get__(self, instance, owner): + # Tell the ClientManager to login to keystone + if self._handle is None: + instance.init_token() + self._handle = self.factory(instance) + return self._handle + + +class ClientManager(object): + """Manages access to API clients, including authentication. + """ + + compute = ClientCache(compute_client.make_client) + + def __init__(self, token=None, url=None, + auth_url=None, + tenant_name=None, tenant_id=None, + username=None, password=None, + region_name=None, + identity_api_version=None, + compute_api_version=None, + image_api_version=None, + ): + self._token = token + self._url = url + self._auth_url = auth_url + self._tenant_name = tenant_name + self._tenant_id = tenant_id + self._username = username + self._password = password + self._region_name = region_name + self._identity_api_version = identity_api_version + self._compute_api_version = compute_api_version + self._image_api_version = image_api_version + + def init_token(self): + """Return the auth token and endpoint. + """ + if self._token: + LOG.debug('using existing auth token') + return + + LOG.debug('validating authentication options') + if not self._username: + raise exc.CommandError( + "You must provide a username via" + " either --os-username or env[OS_USERNAME]") + + if not self._password: + raise exc.CommandError( + "You must provide a password via" + " either --os-password or env[OS_PASSWORD]") + + if not (self._tenant_id or self._tenant_name): + raise exc.CommandError( + "You must provide a tenant_id via" + " either --os-tenant-id or via env[OS_TENANT_ID]") + + if not self._auth_url: + raise exc.CommandError( + "You must provide an auth url via" + " either --os-auth-url or via env[OS_AUTH_URL]") + + kwargs = { + 'username': self._username, + 'password': self._password, + 'tenant_id': self._tenant_id, + 'tenant_name': self._tenant_name, + 'auth_url': self._auth_url + } + self._auth_client = keystone_client.Client(**kwargs) + self._token = self._auth_client.auth_token + return + + def get_endpoint_for_service_type(self, service_type): + """Return the endpoint URL for the service type. + """ + # See if we are using password flow auth, i.e. we have a + # service catalog to select endpoints from + if self._auth_client and self._auth_client.service_catalog: + endpoint = self._auth_client.service_catalog.url_for( + service_type=service_type) + else: + # Hope we were given the correct URL. + endpoint = self._url + return endpoint diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py new file mode 100644 index 0000000000..ef0ceb3816 --- /dev/null +++ b/openstackclient/compute/client.py @@ -0,0 +1,32 @@ +import logging + +from novaclient import client as nova_client + +LOG = logging.getLogger(__name__) + + +def make_client(instance): + """Returns a compute service client. + """ + LOG.debug('instantiating compute client') + # FIXME(dhellmann): Where is the endpoint value used? + # url = instance.get_endpoint_for_service_type('compute') + client = nova_client.Client( + version=instance._compute_api_version, + username=instance._username, + api_key=instance._password, + project_id=instance._tenant_name, + auth_url=instance._auth_url, + # FIXME(dhellmann): add constructor argument for this + insecure=False, + region_name=instance._region_name, + # FIXME(dhellmann): get endpoint_type from option? + endpoint_type='publicURL', + # FIXME(dhellmann): add extension discovery + extensions=[], + service_type='compute', + # FIXME(dhellmann): what is service_name? + service_name='', + ) + client.authenticate() + return client diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index c7c6add07d..69bfc7e882 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -20,45 +20,55 @@ Server action implementations """ import logging +import os + +from cliff import lister +from cliff import show from openstackclient.common import command from openstackclient.common import utils -def _find_server(cs, server): - """Get a server by name or ID.""" - return utils.find_resource(cs.servers, server) +def _format_servers_list_networks(server): + """Return a string containing the networks a server is attached to. + + :param server: a single Server resource + """ + output = [] + for (network, addresses) in server.networks.items(): + if not addresses: + continue + addresses_csv = ', '.join(addresses) + group = "%s=%s" % (network, addresses_csv) + output.append(group) + return '; '.join(output) -def _print_server(cs, server): - # By default when searching via name we will do a - # findall(name=blah) and due a REST /details which is not the same - # as a .get() and doesn't get the information about flavors and - # images. This fix it as we redo the call with the id which does a - # .get() to get all informations. - if not 'flavor' in server._info: - server = _find_server(cs, server.id) +def get_server_properties(server, fields, formatters={}): + """Return a tuple containing the server properties. - networks = server.networks - info = server._info.copy() - for network_label, address_list in networks.items(): - info['%s network' % network_label] = ', '.join(address_list) + :param server: a single Server resource + :param fields: tuple of strings with the desired field names + :param formatters: dictionary mapping field names to callables + to format the values + """ + row = [] + mixed_case_fields = ['serverId'] - flavor = info.get('flavor', {}) - flavor_id = flavor.get('id', '') - info['flavor'] = _find_flavor(cs, flavor_id).name - - image = info.get('image', {}) - image_id = image.get('id', '') - info['image'] = _find_image(cs, image_id).name - - info.pop('links', None) - info.pop('addresses', None) - - utils.print_dict(info) + for field in fields: + if field in formatters: + row.append(formatters[field](server)) + else: + if field in mixed_case_fields: + field_name = field.replace(' ', '_') + else: + field_name = field.lower().replace(' ', '_') + data = getattr(server, field_name, '') + row.append(data) + return tuple(row) -class List_Server(command.OpenStackCommand): +class List_Server(command.OpenStackCommand, lister.Lister): "List server command." api = 'compute' @@ -67,17 +77,79 @@ class List_Server(command.OpenStackCommand): def get_parser(self, prog_name): parser = super(List_Server, self).get_parser(prog_name) parser.add_argument( - '--long', + '--reservation-id', + help='only return instances that match the reservation', + ) + parser.add_argument( + '--ip', + help='regular expression to match IP address', + ) + parser.add_argument( + '--ip6', + help='regular expression to match IPv6 address', + ) + parser.add_argument( + '--name', + help='regular expression to match name', + ) + parser.add_argument( + '--instance-name', + help='regular expression to match instance name', + ) + parser.add_argument( + '--status', + help='search by server status', + # FIXME(dhellmann): Add choices? + ) + parser.add_argument( + '--flavor', + help='search by flavor ID', + ) + parser.add_argument( + '--image', + help='search by image ID', + ) + parser.add_argument( + '--host', + metavar='HOSTNAME', + help='search by hostname', + ) + parser.add_argument( + '--all-tenants', action='store_true', - default=False, - help='Additional fields are listed in output') + default=bool(int(os.environ.get("ALL_TENANTS", 0))), + help='display information from all tenants (admin only)', + ) return parser - def run(self, parsed_args): - self.log.info('v2.List_Server.run(%s)' % parsed_args) + def get_data(self, parsed_args): + self.log.debug('v2.List_Server.run(%s)' % parsed_args) + nova_client = self.app.client_manager.compute + search_opts = { + 'all_tenants': parsed_args.all_tenants, + 'reservation_id': parsed_args.reservation_id, + 'ip': parsed_args.ip, + 'ip6': parsed_args.ip6, + 'name': parsed_args.name, + 'image': parsed_args.image, + 'flavor': parsed_args.flavor, + 'status': parsed_args.status, + 'host': parsed_args.host, + 'instance_name': parsed_args.instance_name, + } + self.log.debug('search options: %s', search_opts) + # FIXME(dhellmann): Consider adding other columns + columns = ('ID', 'Name', 'Status', 'Networks') + data = nova_client.servers.list(search_opts=search_opts) + return (columns, + (get_server_properties( + s, columns, + formatters={'Networks': _format_servers_list_networks}, + ) for s in data), + ) -class Show_Server(command.OpenStackCommand): +class Show_Server(command.OpenStackCommand, show.ShowOne): "Show server command." api = 'compute' @@ -91,7 +163,32 @@ class Show_Server(command.OpenStackCommand): help='Name or ID of server to display') return parser - def run(self, parsed_args): - self.log.info('v2.Show_Server.run(%s)' % parsed_args) - #s = _find_server(cs, args.server) - #_print_server(cs, s) + def get_data(self, parsed_args): + self.log.debug('v2.Show_Server.run(%s)' % parsed_args) + nova_client = self.app.client_manager.compute + server = utils.find_resource(nova_client.servers, parsed_args.server) + + info = {} + info.update(server._info) + + # Convert the flavor blob to a name + flavor_info = info.get('flavor', {}) + flavor_id = flavor_info.get('id', '') + flavor = utils.find_resource(nova_client.flavors, flavor_id) + info['flavor'] = flavor.name + + # Convert the image blob to a name + image_info = info.get('image', {}) + image_id = image_info.get('id', '') + image = utils.find_resource(nova_client.images, image_id) + info['image'] = image.name + + # Format addresses in a useful way + info['addresses'] = _format_servers_list_networks(server) + + # Remove a couple of values that are long and not too useful + info.pop('links', None) + + columns = sorted(info.keys()) + values = [info[c] for c in columns] + return (columns, values) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index c7e63b807d..fb5d07272b 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -19,7 +19,6 @@ Command-line interface to the OpenStack APIs """ -import argparse import logging import os import sys @@ -27,9 +26,7 @@ import sys from cliff.app import App from cliff.commandmanager import CommandManager -from keystoneclient.v2_0 import client as ksclient - -from openstackclient.common import exceptions as exc +from openstackclient.common import clientmanager from openstackclient.common import utils @@ -144,72 +141,32 @@ class OpenStackShell(App): 'image': self.options.os_image_api_version, } + self.client_manager = clientmanager.ClientManager( + token=self.options.os_token, + url=self.options.os_url, + auth_url=self.options.os_auth_url, + tenant_name=self.options.os_tenant_name, + tenant_id=self.options.os_tenant_id, + username=self.options.os_username, + password=self.options.os_password, + region_name=self.options.os_region_name, + identity_api_version=self.options.os_identity_api_version, + compute_api_version=self.options.os_compute_api_version, + image_api_version=self.options.os_image_api_version, + ) + self.log.debug("API: Identity=%s Compute=%s Image=%s" % ( self.api_version['identity'], self.api_version['compute'], self.api_version['image']) ) - # do checking of os_username, etc here - if (self.options.os_token and self.options.os_url): - # do token auth - self.endpoint = self.options.os_url - self.token = self.options.os_token - else: - if not self.options.os_username: - raise exc.CommandError("You must provide a username via" - " either --os-username or env[OS_USERNAME]") - - if not self.options.os_password: - raise exc.CommandError("You must provide a password via" - " either --os-password or env[OS_PASSWORD]") - - if not (self.options.os_tenant_id or self.options.os_tenant_name): - raise exc.CommandError("You must provide a tenant_id via" - " either --os-tenant-id or via env[OS_TENANT_ID]") - - if not self.options.os_auth_url: - raise exc.CommandError("You must provide an auth url via" - " either --os-auth-url or via env[OS_AUTH_URL]") - kwargs = { - 'username': self.options.os_username, - 'password': self.options.os_password, - 'tenant_id': self.options.os_tenant_id, - 'tenant_name': self.options.os_tenant_name, - 'auth_url': self.options.os_auth_url - } - self.auth_client = ksclient.Client( - username=kwargs.get('username'), - password=kwargs.get('password'), - tenant_id=kwargs.get('tenant_id'), - tenant_name=kwargs.get('tenant_name'), - auth_url=kwargs.get('auth_url'), - ) - self.token = self.auth_client.auth_token - # Since we don't know which command is being executed yet, defer - # selection of a service API until later - self.endpoint = None - - self.log.debug("token: %s" % self.token) - self.log.debug("endpoint: %s" % self.endpoint) - def prepare_to_run_command(self, cmd): """Set up auth and API versions""" self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__) - self.log.debug("api: %s" % cmd.api) - - # See if we are using password flow auth, i.e. we have a - # service catalog to select endpoints from - if self.auth_client and self.auth_client.service_catalog: - self.endpoint = self.auth_client.service_catalog.url_for( - service_type=cmd.api) - - # self.endpoint == None here is an error... - if not self.endpoint: - raise RuntimeError('no endpoint found') - - # get a client for the desired api here + self.log.debug("api: %s" % cmd.api if hasattr(cmd, 'api') else None) + return def clean_up(self, cmd, result, err): self.log.debug('clean_up %s', cmd.__class__.__name__) diff --git a/tests/test_clientmanager_clientcache.py b/tests/test_clientmanager_clientcache.py new file mode 100644 index 0000000000..200da01fd3 --- /dev/null +++ b/tests/test_clientmanager_clientcache.py @@ -0,0 +1,22 @@ + +from openstackclient.common import clientmanager + + +def factory(inst): + return object() + + +class Container(object): + + attr = clientmanager.ClientCache(factory) + + def init_token(self): + return + + +def test_singleton(): + # Verify that the ClientCache descriptor only + # invokes the factory one time and always + # returns the same value after that. + c = Container() + assert c.attr is c.attr diff --git a/tools/pip-requires b/tools/pip-requires index f45be56321..738b5f5348 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -6,3 +6,4 @@ mock prettytable simplejson -e git://github.com/openstack/python-keystoneclient.git#egg=python-keystoneclient +-e git+https://github.com/openstack/python-novaclient.git#egg=python_novaclient \ No newline at end of file