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
This commit is contained in:
Doug Hellmann 2012-05-02 17:02:08 -04:00
parent b5a809d8e3
commit 5e4032150d
7 changed files with 316 additions and 98 deletions

2
.gitignore vendored
View File

@ -1,4 +1,5 @@
*.log
*.log.*
*.pyc
*.swp
*~
@ -10,3 +11,4 @@ dist
python_openstackclient.egg-info
.tox/
ChangeLog
TAGS

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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