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