Expand support for command extensions

Allows client libraries to have complete access to the rest of the
OSC ClientManager.  In addition, extension libraries can define
global options (for API version options/env vars) and define
versioned API entry points similar to the in-repo commands.

The changes to ClientManager exposed some issues in the existing
object api tests that needed to be cleaned up.

Change-Id: Ic9662edf34c5dd130a2f1a69d2454adefc1f8a95
This commit is contained in:
Dean Troyer 2013-11-20 18:02:09 -06:00
parent d45187a0c1
commit 9062811d10
14 changed files with 172 additions and 119 deletions

View File

@ -16,12 +16,10 @@
"""Manage access to the clients, including authenticating when needed.""" """Manage access to the clients, including authenticating when needed."""
import logging import logging
import pkg_resources
import sys
from openstackclient.compute import client as compute_client
from openstackclient.identity import client as identity_client from openstackclient.identity import client as identity_client
from openstackclient.image import client as image_client
from openstackclient.object import client as object_client
from openstackclient.volume import client as volume_client
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -42,11 +40,7 @@ class ClientCache(object):
class ClientManager(object): class ClientManager(object):
"""Manages access to API clients, including authentication.""" """Manages access to API clients, including authentication."""
compute = ClientCache(compute_client.make_client)
identity = ClientCache(identity_client.make_client) identity = ClientCache(identity_client.make_client)
image = ClientCache(image_client.make_client)
object = ClientCache(object_client.make_client)
volume = ClientCache(volume_client.make_client)
def __init__(self, token=None, url=None, auth_url=None, project_name=None, def __init__(self, token=None, url=None, auth_url=None, project_name=None,
project_id=None, username=None, password=None, project_id=None, username=None, password=None,
@ -93,3 +87,26 @@ class ClientManager(object):
# Hope we were given the correct URL. # Hope we were given the correct URL.
endpoint = self._url endpoint = self._url
return endpoint return endpoint
def get_extension_modules(group):
"""Add extension clients"""
mod_list = []
for ep in pkg_resources.iter_entry_points(group):
LOG.debug('found extension %r' % ep.name)
__import__(ep.module_name)
module = sys.modules[ep.module_name]
mod_list.append(module)
init_func = getattr(module, 'Initialize', None)
if init_func:
init_func('x')
setattr(
ClientManager,
ep.name,
ClientCache(
getattr(sys.modules[ep.module_name], 'make_client', None)
),
)
return mod_list

View File

@ -19,6 +19,8 @@ from openstackclient.common import utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
DEFAULT_COMPUTE_API_VERSION = '2'
API_VERSION_OPTION = 'os_compute_api_version'
API_NAME = 'compute' API_NAME = 'compute'
API_VERSIONS = { API_VERSIONS = {
'1.1': 'novaclient.v1_1.client.Client', '1.1': 'novaclient.v1_1.client.Client',
@ -60,3 +62,17 @@ def make_client(instance):
client.client.service_catalog = instance._service_catalog client.client.service_catalog = instance._service_catalog
client.client.auth_token = instance._token client.client.auth_token = instance._token
return client return client
def build_option_parser(parser):
"""Hook to add global options"""
parser.add_argument(
'--os-compute-api-version',
metavar='<compute-api-version>',
default=utils.env(
'OS_COMPUTE_API_VERSION',
default=DEFAULT_COMPUTE_API_VERSION),
help='Compute API version, default=' +
DEFAULT_COMPUTE_API_VERSION +
' (Env: OS_COMPUTE_API_VERSION)')
return parser

View File

@ -21,6 +21,8 @@ from openstackclient.common import utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
DEFAULT_IDENTITY_API_VERSION = '2.0'
API_VERSION_OPTION = 'os_identity_api_version'
API_NAME = 'identity' API_NAME = 'identity'
API_VERSIONS = { API_VERSIONS = {
'2.0': 'openstackclient.identity.client.IdentityClientv2_0', '2.0': 'openstackclient.identity.client.IdentityClientv2_0',

View File

@ -23,6 +23,8 @@ from openstackclient.common import utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
DEFAULT_IMAGE_API_VERSION = '1'
API_VERSION_OPTION = 'os_image_api_version'
API_NAME = "image" API_NAME = "image"
API_VERSIONS = { API_VERSIONS = {
"1": "openstackclient.image.client.Client_v1", "1": "openstackclient.image.client.Client_v1",
@ -48,6 +50,20 @@ def make_client(instance):
) )
def build_option_parser(parser):
"""Hook to add global options"""
parser.add_argument(
'--os-image-api-version',
metavar='<image-api-version>',
default=utils.env(
'OS_IMAGE_API_VERSION',
default=DEFAULT_IMAGE_API_VERSION),
help='Image API version, default=' +
DEFAULT_IMAGE_API_VERSION +
' (Env: OS_IMAGE_API_VERSION)')
return parser
# NOTE(dtroyer): glanceclient.v1.image.ImageManager() doesn't have a find() # NOTE(dtroyer): glanceclient.v1.image.ImageManager() doesn't have a find()
# method so add one here until the common client libs arrive # method so add one here until the common client libs arrive
# A similar subclass will be required for v2 # A similar subclass will be required for v2

View File

@ -21,7 +21,9 @@ from openstackclient.common import utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
API_NAME = 'object-store' DEFAULT_OBJECT_API_VERSION = '1'
API_VERSION_OPTION = 'os_object_api_version'
API_NAME = 'object'
API_VERSIONS = { API_VERSIONS = {
'1': 'openstackclient.object.client.ObjectClientv1', '1': 'openstackclient.object.client.ObjectClientv1',
} }
@ -45,6 +47,20 @@ def make_client(instance):
return client return client
def build_option_parser(parser):
"""Hook to add global options"""
parser.add_argument(
'--os-object-api-version',
metavar='<object-api-version>',
default=utils.env(
'OS_OBJECT_API_VERSION',
default=DEFAULT_OBJECT_API_VERSION),
help='Object API version, default=' +
DEFAULT_OBJECT_API_VERSION +
' (Env: OS_OBJECT_API_VERSION)')
return parser
class ObjectClientv1(object): class ObjectClientv1(object):
def __init__( def __init__(

View File

@ -33,15 +33,11 @@ from openstackclient.common import exceptions as exc
from openstackclient.common import openstackkeyring from openstackclient.common import openstackkeyring
from openstackclient.common import restapi from openstackclient.common import restapi
from openstackclient.common import utils from openstackclient.common import utils
from openstackclient.identity import client as identity_client
KEYRING_SERVICE = 'openstack' KEYRING_SERVICE = 'openstack'
DEFAULT_COMPUTE_API_VERSION = '2'
DEFAULT_IDENTITY_API_VERSION = '2.0'
DEFAULT_IMAGE_API_VERSION = '1'
DEFAULT_OBJECT_API_VERSION = '1'
DEFAULT_VOLUME_API_VERSION = '1'
DEFAULT_DOMAIN = 'default' DEFAULT_DOMAIN = 'default'
@ -86,6 +82,15 @@ class OpenStackShell(app.App):
# Assume TLS host certificate verification is enabled # Assume TLS host certificate verification is enabled
self.verify = True self.verify = True
# Get list of extension modules
self.ext_modules = clientmanager.get_extension_modules(
'openstack.cli.extension',
)
# Loop through extensions to get parser additions
for mod in self.ext_modules:
self.parser = mod.build_option_parser(self.parser)
# NOTE(dtroyer): This hack changes the help action that Cliff # NOTE(dtroyer): This hack changes the help action that Cliff
# automatically adds to the parser so we can defer # automatically adds to the parser so we can defer
# its execution until after the api-versioned commands # its execution until after the api-versioned commands
@ -202,51 +207,6 @@ class OpenStackShell(app.App):
help='Default domain ID, default=' + help='Default domain ID, default=' +
DEFAULT_DOMAIN + DEFAULT_DOMAIN +
' (Env: OS_DEFAULT_DOMAIN)') ' (Env: OS_DEFAULT_DOMAIN)')
parser.add_argument(
'--os-identity-api-version',
metavar='<identity-api-version>',
default=env(
'OS_IDENTITY_API_VERSION',
default=DEFAULT_IDENTITY_API_VERSION),
help='Identity API version, default=' +
DEFAULT_IDENTITY_API_VERSION +
' (Env: OS_IDENTITY_API_VERSION)')
parser.add_argument(
'--os-compute-api-version',
metavar='<compute-api-version>',
default=env(
'OS_COMPUTE_API_VERSION',
default=DEFAULT_COMPUTE_API_VERSION),
help='Compute API version, default=' +
DEFAULT_COMPUTE_API_VERSION +
' (Env: OS_COMPUTE_API_VERSION)')
parser.add_argument(
'--os-image-api-version',
metavar='<image-api-version>',
default=env(
'OS_IMAGE_API_VERSION',
default=DEFAULT_IMAGE_API_VERSION),
help='Image API version, default=' +
DEFAULT_IMAGE_API_VERSION +
' (Env: OS_IMAGE_API_VERSION)')
parser.add_argument(
'--os-object-api-version',
metavar='<object-api-version>',
default=env(
'OS_OBJECT_API_VERSION',
default=DEFAULT_OBJECT_API_VERSION),
help='Object API version, default=' +
DEFAULT_OBJECT_API_VERSION +
' (Env: OS_OBJECT_API_VERSION)')
parser.add_argument(
'--os-volume-api-version',
metavar='<volume-api-version>',
default=env(
'OS_VOLUME_API_VERSION',
default=DEFAULT_VOLUME_API_VERSION),
help='Volume API version, default=' +
DEFAULT_VOLUME_API_VERSION +
' (Env: OS_VOLUME_API_VERSION)')
parser.add_argument( parser.add_argument(
'--os-token', '--os-token',
metavar='<token>', metavar='<token>',
@ -270,6 +230,16 @@ class OpenStackShell(app.App):
help='Use keyring to store password, ' help='Use keyring to store password, '
'default=False (Env: OS_USE_KEYRING)') 'default=False (Env: OS_USE_KEYRING)')
parser.add_argument(
'--os-identity-api-version',
metavar='<identity-api-version>',
default=env(
'OS_IDENTITY_API_VERSION',
default=identity_client.DEFAULT_IDENTITY_API_VERSION),
help='Identity API version, default=' +
identity_client.DEFAULT_IDENTITY_API_VERSION +
' (Env: OS_IDENTITY_API_VERSION)')
return parser return parser
def authenticate_user(self): def authenticate_user(self):
@ -391,17 +361,20 @@ class OpenStackShell(app.App):
# Stash selected API versions for later # Stash selected API versions for later
self.api_version = { self.api_version = {
'compute': self.options.os_compute_api_version,
'identity': self.options.os_identity_api_version, 'identity': self.options.os_identity_api_version,
'image': self.options.os_image_api_version,
'object-store': self.options.os_object_api_version,
'volume': self.options.os_volume_api_version,
} }
# Loop through extensions to get API versions
for mod in self.ext_modules:
ver = getattr(self.options, mod.API_VERSION_OPTION, None)
if ver:
self.api_version[mod.API_NAME] = ver
self.log.debug('%s API version %s' % (mod.API_NAME, ver))
# Add the API version-specific commands # Add the API version-specific commands
for api in self.api_version.keys(): for api in self.api_version.keys():
version = '.v' + self.api_version[api].replace('.', '_') version = '.v' + self.api_version[api].replace('.', '_')
cmd_group = 'openstack.' + api.replace('-', '_') + version cmd_group = 'openstack.' + api.replace('-', '_') + version
self.log.debug('command group %s' % cmd_group)
self.command_manager.add_command_group(cmd_group) self.command_manager.add_command_group(cmd_group)
# Commands that span multiple APIs # Commands that span multiple APIs
@ -420,6 +393,8 @@ class OpenStackShell(app.App):
# } # }
self.command_manager.add_command_group( self.command_manager.add_command_group(
'openstack.extension') 'openstack.extension')
# call InitializeXxx() here
# set up additional clients to stuff in to client_manager??
# Handle deferred help and exit # Handle deferred help and exit
if self.options.deferred_help: if self.options.deferred_help:

View File

@ -49,6 +49,7 @@ class FakeClientManager(object):
self.compute = None self.compute = None
self.identity = None self.identity = None
self.image = None self.image = None
self.object = None
self.volume = None self.volume = None
self.auth_ref = None self.auth_ref = None

View File

@ -13,6 +13,10 @@
# under the License. # under the License.
# #
from openstackclient.tests import fakes
from openstackclient.tests import utils
container_name = 'bit-bucket' container_name = 'bit-bucket'
container_bytes = 1024 container_bytes = 1024
container_count = 1 container_count = 1
@ -65,3 +69,19 @@ OBJECT_2 = {
'content_type': object_content_type_2, 'content_type': object_content_type_2,
'last_modified': object_modified_2, 'last_modified': object_modified_2,
} }
class FakeObjectv1Client(object):
def __init__(self, **kwargs):
self.endpoint = kwargs['endpoint']
self.token = kwargs['token']
class TestObjectv1(utils.TestCommand):
def setUp(self):
super(TestObjectv1, self).setUp()
self.app.client_manager.object = FakeObjectv1Client(
endpoint=fakes.AUTH_URL,
token=fakes.AUTH_TOKEN,
)

View File

@ -19,8 +19,7 @@ import mock
from openstackclient.object.v1.lib import container as lib_container from openstackclient.object.v1.lib import container as lib_container
from openstackclient.tests.common import test_restapi as restapi from openstackclient.tests.common import test_restapi as restapi
from openstackclient.tests import fakes from openstackclient.tests.object.v1 import fakes as object_fakes
from openstackclient.tests import utils
fake_account = 'q12we34r' fake_account = 'q12we34r'
@ -36,12 +35,10 @@ class FakeClient(object):
self.token = fake_auth self.token = fake_auth
class TestContainer(utils.TestCommand): class TestContainer(object_fakes.TestObjectv1):
def setUp(self): def setUp(self):
super(TestContainer, self).setUp() super(TestContainer, self).setUp()
self.app.client_manager = fakes.FakeClientManager()
self.app.client_manager.object = FakeClient()
self.app.restapi = mock.MagicMock() self.app.restapi = mock.MagicMock()
@ -53,7 +50,7 @@ class TestContainerList(TestContainer):
data = lib_container.list_containers( data = lib_container.list_containers(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
) )
# Check expected values # Check expected values
@ -69,7 +66,7 @@ class TestContainerList(TestContainer):
data = lib_container.list_containers( data = lib_container.list_containers(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
marker='next', marker='next',
) )
@ -86,7 +83,7 @@ class TestContainerList(TestContainer):
data = lib_container.list_containers( data = lib_container.list_containers(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
limit=5, limit=5,
) )
@ -103,7 +100,7 @@ class TestContainerList(TestContainer):
data = lib_container.list_containers( data = lib_container.list_containers(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
end_marker='last', end_marker='last',
) )
@ -120,7 +117,7 @@ class TestContainerList(TestContainer):
data = lib_container.list_containers( data = lib_container.list_containers(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
prefix='foo/', prefix='foo/',
) )
@ -147,7 +144,7 @@ class TestContainerList(TestContainer):
data = lib_container.list_containers( data = lib_container.list_containers(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
full_listing=True, full_listing=True,
) )
@ -171,7 +168,7 @@ class TestContainerShow(TestContainer):
data = lib_container.show_container( data = lib_container.show_container(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
'is-name', 'is-name',
) )

View File

@ -19,8 +19,7 @@ import mock
from openstackclient.object.v1.lib import object as lib_object from openstackclient.object.v1.lib import object as lib_object
from openstackclient.tests.common import test_restapi as restapi from openstackclient.tests.common import test_restapi as restapi
from openstackclient.tests import fakes from openstackclient.tests.object.v1 import fakes as object_fakes
from openstackclient.tests import utils
fake_account = 'q12we34r' fake_account = 'q12we34r'
@ -37,12 +36,10 @@ class FakeClient(object):
self.token = fake_auth self.token = fake_auth
class TestObject(utils.TestCommand): class TestObject(object_fakes.TestObjectv1):
def setUp(self): def setUp(self):
super(TestObject, self).setUp() super(TestObject, self).setUp()
self.app.client_manager = fakes.FakeClientManager()
self.app.client_manager.object = FakeClient()
self.app.restapi = mock.MagicMock() self.app.restapi = mock.MagicMock()
@ -54,7 +51,7 @@ class TestObjectListObjects(TestObject):
data = lib_object.list_objects( data = lib_object.list_objects(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
fake_container, fake_container,
) )
@ -71,7 +68,7 @@ class TestObjectListObjects(TestObject):
data = lib_object.list_objects( data = lib_object.list_objects(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
fake_container, fake_container,
marker='next', marker='next',
) )
@ -89,7 +86,7 @@ class TestObjectListObjects(TestObject):
data = lib_object.list_objects( data = lib_object.list_objects(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
fake_container, fake_container,
limit=5, limit=5,
) )
@ -107,7 +104,7 @@ class TestObjectListObjects(TestObject):
data = lib_object.list_objects( data = lib_object.list_objects(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
fake_container, fake_container,
end_marker='last', end_marker='last',
) )
@ -125,7 +122,7 @@ class TestObjectListObjects(TestObject):
data = lib_object.list_objects( data = lib_object.list_objects(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
fake_container, fake_container,
delimiter='|', delimiter='|',
) )
@ -146,7 +143,7 @@ class TestObjectListObjects(TestObject):
data = lib_object.list_objects( data = lib_object.list_objects(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
fake_container, fake_container,
prefix='foo/', prefix='foo/',
) )
@ -164,7 +161,7 @@ class TestObjectListObjects(TestObject):
data = lib_object.list_objects( data = lib_object.list_objects(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
fake_container, fake_container,
path='next', path='next',
) )
@ -192,7 +189,7 @@ class TestObjectListObjects(TestObject):
data = lib_object.list_objects( data = lib_object.list_objects(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
fake_container, fake_container,
full_listing=True, full_listing=True,
) )
@ -216,7 +213,7 @@ class TestObjectShowObjects(TestObject):
data = lib_object.show_object( data = lib_object.show_object(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
fake_container, fake_container,
fake_object, fake_object,
) )
@ -250,7 +247,7 @@ class TestObjectShowObjects(TestObject):
data = lib_object.show_object( data = lib_object.show_object(
self.app.restapi, self.app.restapi,
self.app.client_manager.object.endpoint, fake_url,
fake_container, fake_container,
fake_object, fake_object,
) )

View File

@ -16,10 +16,8 @@
import copy import copy
import mock import mock
from openstackclient.common import clientmanager
from openstackclient.object.v1 import container from openstackclient.object.v1 import container
from openstackclient.tests.object.v1 import fakes as object_fakes from openstackclient.tests.object.v1 import fakes as object_fakes
from openstackclient.tests import utils
AUTH_TOKEN = "foobar" AUTH_TOKEN = "foobar"
@ -32,18 +30,10 @@ class FakeClient(object):
self.token = AUTH_TOKEN self.token = AUTH_TOKEN
class TestObject(utils.TestCommand): class TestObject(object_fakes.TestObjectv1):
def setUp(self): def setUp(self):
super(TestObject, self).setUp() super(TestObject, self).setUp()
api_version = {"object-store": "1"}
self.app.client_manager = clientmanager.ClientManager(
token=AUTH_TOKEN,
url=AUTH_URL,
auth_url=AUTH_URL,
api_version=api_version,
)
class TestObjectClient(TestObject): class TestObjectClient(TestObject):

View File

@ -16,34 +16,18 @@
import copy import copy
import mock import mock
from openstackclient.common import clientmanager
from openstackclient.object.v1 import object as obj from openstackclient.object.v1 import object as obj
from openstackclient.tests.object.v1 import fakes as object_fakes from openstackclient.tests.object.v1 import fakes as object_fakes
from openstackclient.tests import utils
AUTH_TOKEN = "foobar" AUTH_TOKEN = "foobar"
AUTH_URL = "http://0.0.0.0" AUTH_URL = "http://0.0.0.0"
class FakeClient(object): class TestObject(object_fakes.TestObjectv1):
def __init__(self, endpoint=None, **kwargs):
self.endpoint = AUTH_URL
self.token = AUTH_TOKEN
class TestObject(utils.TestCommand):
def setUp(self): def setUp(self):
super(TestObject, self).setUp() super(TestObject, self).setUp()
api_version = {"object-store": "1"}
self.app.client_manager = clientmanager.ClientManager(
token=AUTH_TOKEN,
url=AUTH_URL,
auth_url=AUTH_URL,
api_version=api_version,
)
class TestObjectClient(TestObject): class TestObjectClient(TestObject):

View File

@ -20,6 +20,8 @@ from openstackclient.common import utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
DEFAULT_VOLUME_API_VERSION = '1'
API_VERSION_OPTION = 'os_volume_api_version'
API_NAME = "volume" API_NAME = "volume"
API_VERSIONS = { API_VERSIONS = {
"1": "cinderclient.v1.client.Client" "1": "cinderclient.v1.client.Client"
@ -45,3 +47,17 @@ def make_client(instance):
) )
return client return client
def build_option_parser(parser):
"""Hook to add global options"""
parser.add_argument(
'--os-volume-api-version',
metavar='<volume-api-version>',
default=utils.env(
'OS_VOLUME_API_VERSION',
default=DEFAULT_VOLUME_API_VERSION),
help='Volume API version, default=' +
DEFAULT_VOLUME_API_VERSION +
' (Env: OS_VOLUME_API_VERSION)')
return parser

View File

@ -27,6 +27,12 @@ console_scripts =
openstack.cli = openstack.cli =
openstack.cli.extension =
compute = openstackclient.compute.client
image = openstackclient.image.client
object = openstackclient.object.client
volume = openstackclient.volume.client
openstack.common = openstack.common =
limits_show = openstackclient.common.limits:ShowLimits limits_show = openstackclient.common.limits:ShowLimits
quota_set = openstackclient.common.quota:SetQuota quota_set = openstackclient.common.quota:SetQuota
@ -240,7 +246,7 @@ openstack.image.v2 =
image_save = openstackclient.image.v2.image:SaveImage image_save = openstackclient.image.v2.image:SaveImage
image_show = openstackclient.image.v2.image:ShowImage image_show = openstackclient.image.v2.image:ShowImage
openstack.object_store.v1 = openstack.object.v1 =
container_list = openstackclient.object.v1.container:ListContainer container_list = openstackclient.object.v1.container:ListContainer
container_show = openstackclient.object.v1.container:ShowContainer container_show = openstackclient.object.v1.container:ShowContainer
object_list = openstackclient.object.v1.object:ListObject object_list = openstackclient.object.v1.object:ListObject