Makes novaclient use the volumes endpoint

* Depends on https://review.openstack.org/#change,4479
 * Adds support to change service type including tests
 * Adds decorator for methods that need to use another service type
 * Changes volume and snapshots to use the volume endpoint
 * These extensions will move into the volume client once it exists
 * Fixes bug 940017

Change-Id: I683e4ca6c67e278d8aa8a9acec3dc0f1872f43f2
This commit is contained in:
Vishvananda Ishaya 2012-02-24 02:30:48 +00:00
parent 39bfd0585b
commit 03f54c57e1
10 changed files with 89 additions and 32 deletions

View File

@ -36,7 +36,8 @@ class HTTPClient(httplib2.Http):
def __init__(self, user, password, projectid, auth_url, insecure=False, def __init__(self, user, password, projectid, auth_url, insecure=False,
timeout=None, token=None, region_name=None, timeout=None, token=None, region_name=None,
endpoint_type='publicURL', service_name=None): endpoint_type='publicURL', service_type=None,
service_name=None):
super(HTTPClient, self).__init__(timeout=timeout) super(HTTPClient, self).__init__(timeout=timeout)
self.user = user self.user = user
self.password = password self.password = password
@ -45,6 +46,7 @@ class HTTPClient(httplib2.Http):
self.version = 'v1.1' self.version = 'v1.1'
self.region_name = region_name self.region_name = region_name
self.endpoint_type = endpoint_type self.endpoint_type = endpoint_type
self.service_type = service_type
self.service_name = service_name self.service_name = service_name
self.management_url = None self.management_url = None
@ -158,6 +160,7 @@ class HTTPClient(httplib2.Http):
attr='region', attr='region',
filter_value=self.region_name, filter_value=self.region_name,
endpoint_type=self.endpoint_type, endpoint_type=self.endpoint_type,
service_type=self.service_type,
service_name=self.service_name) service_name=self.service_name)
self.management_url = management_url.rstrip('/') self.management_url = management_url.rstrip('/')
return None return None

View File

@ -29,7 +29,7 @@ class ServiceCatalog(object):
return self.catalog['access']['token']['id'] return self.catalog['access']['token']['id']
def url_for(self, attr=None, filter_value=None, def url_for(self, attr=None, filter_value=None,
service_type='compute', endpoint_type='publicURL', service_type=None, endpoint_type='publicURL',
service_name=None): service_name=None):
"""Fetch the public URL from the Compute service for """Fetch the public URL from the Compute service for
a particular endpoint attribute. If none given, return a particular endpoint attribute. If none given, return

View File

@ -36,6 +36,7 @@ from novaclient.v1_1 import shell as shell_v1_1
DEFAULT_NOVA_VERSION = "1.1" DEFAULT_NOVA_VERSION = "1.1"
DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL'
DEFAULT_NOVA_SERVICE_TYPE = 'compute'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -102,6 +103,9 @@ class OpenStackComputeShell(object):
default=utils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'), default=utils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'),
help='Defaults to env[OS_REGION_NAME].') help='Defaults to env[OS_REGION_NAME].')
parser.add_argument('--service_type',
help='Defaults to compute for most actions')
parser.add_argument('--service_name', parser.add_argument('--service_name',
default=utils.env('NOVA_SERVICE_NAME'), default=utils.env('NOVA_SERVICE_NAME'),
help='Defaults to env[NOVA_SERVICE_NAME]') help='Defaults to env[NOVA_SERVICE_NAME]')
@ -267,15 +271,20 @@ class OpenStackComputeShell(object):
return 0 return 0
(user, apikey, password, projectid, tenant_name, url, auth_url, (user, apikey, password, projectid, tenant_name, url, auth_url,
region_name, endpoint_type, insecure, service_name) = ( region_name, endpoint_type, insecure, service_type,
service_name) = (
args.username, args.apikey, args.password, args.username, args.apikey, args.password,
args.projectid, args.tenant_name, args.url, args.projectid, args.tenant_name, args.url,
args.auth_url, args.region_name, args.endpoint_type, args.auth_url, args.region_name, args.endpoint_type,
args.insecure, args.service_name) args.insecure, args.service_type, args.service_name)
if not endpoint_type: if not endpoint_type:
endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE
if not service_type:
service_type = DEFAULT_NOVA_SERVICE_TYPE
service_type = utils.get_service_type(args.func) or service_type
#FIXME(usrleon): Here should be restrict for project id same as #FIXME(usrleon): Here should be restrict for project id same as
# for username or password but for compatibility it is not. # for username or password but for compatibility it is not.
@ -319,6 +328,7 @@ class OpenStackComputeShell(object):
region_name=region_name, region_name=region_name,
endpoint_type=endpoint_type, endpoint_type=endpoint_type,
extensions=self.extensions, extensions=self.extensions,
service_type=service_type,
service_name=service_name) service_name=service_name)
try: try:

View File

@ -96,6 +96,27 @@ def isunauthenticated(f):
return getattr(f, 'unauthenticated', False) return getattr(f, 'unauthenticated', False)
def service_type(stype):
"""
Adds 'service_type' attribute to decorated function.
Usage:
@service_type('volume')
def mymethod(f):
...
"""
def inner(f):
f.service_type = stype
return f
return inner
def get_service_type(f):
"""
Retrieves service type from function
"""
return getattr(f, 'service_type', None)
def pretty_choice_list(l): def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l) return ', '.join("'%s'" % i for i in l)

View File

@ -40,7 +40,7 @@ class Client(object):
def __init__(self, username, api_key, project_id, auth_url, def __init__(self, username, api_key, project_id, auth_url,
insecure=False, timeout=None, token=None, region_name=None, insecure=False, timeout=None, token=None, region_name=None,
endpoint_type='publicURL', extensions=None, endpoint_type='publicURL', extensions=None,
service_name=None): service_type=None, service_name=None):
# FIXME(comstud): Rename the api_key argument above when we # FIXME(comstud): Rename the api_key argument above when we
# know it's not being used as keyword argument # know it's not being used as keyword argument
password = api_key password = api_key
@ -84,6 +84,7 @@ class Client(object):
token=token, token=token,
region_name=region_name, region_name=region_name,
endpoint_type=endpoint_type, endpoint_type=endpoint_type,
service_type=service_type,
service_name=service_name) service_name=service_name)
def authenticate(self): def authenticate(self):

View File

@ -740,6 +740,7 @@ def _translate_volume_snapshot_keys(collection):
setattr(item, to_key, item._info[from_key]) setattr(item, to_key, item._info[from_key])
@utils.service_type('volume')
def do_volume_list(cs, args): def do_volume_list(cs, args):
"""List all the volumes.""" """List all the volumes."""
volumes = cs.volumes.list() volumes = cs.volumes.list()
@ -754,6 +755,7 @@ def do_volume_list(cs, args):
@utils.arg('volume', metavar='<volume>', help='ID of the volume.') @utils.arg('volume', metavar='<volume>', help='ID of the volume.')
@utils.service_type('volume')
def do_volume_show(cs, args): def do_volume_show(cs, args):
"""Show details about a volume.""" """Show details about a volume."""
volume = _find_volume(cs, args.volume) volume = _find_volume(cs, args.volume)
@ -774,6 +776,7 @@ def do_volume_show(cs, args):
@utils.arg('--display_description', metavar='<display_description>', @utils.arg('--display_description', metavar='<display_description>',
help='Optional volume description. (Default=None)', help='Optional volume description. (Default=None)',
default=None) default=None)
@utils.service_type('volume')
def do_volume_create(cs, args): def do_volume_create(cs, args):
"""Add a new volume.""" """Add a new volume."""
cs.volumes.create(args.size, cs.volumes.create(args.size,
@ -783,6 +786,7 @@ def do_volume_create(cs, args):
@utils.arg('volume', metavar='<volume>', help='ID of the volume to delete.') @utils.arg('volume', metavar='<volume>', help='ID of the volume to delete.')
@utils.service_type('volume')
def do_volume_delete(cs, args): def do_volume_delete(cs, args):
"""Remove a volume.""" """Remove a volume."""
volume = _find_volume(cs, args.volume) volume = _find_volume(cs, args.volume)
@ -818,6 +822,7 @@ def do_volume_detach(cs, args):
args.attachment_id) args.attachment_id)
@utils.service_type('volume')
def do_volume_snapshot_list(cs, args): def do_volume_snapshot_list(cs, args):
"""List all the snapshots.""" """List all the snapshots."""
snapshots = cs.volume_snapshots.list() snapshots = cs.volume_snapshots.list()
@ -827,6 +832,7 @@ def do_volume_snapshot_list(cs, args):
@utils.arg('snapshot', metavar='<snapshot>', help='ID of the snapshot.') @utils.arg('snapshot', metavar='<snapshot>', help='ID of the snapshot.')
@utils.service_type('volume')
def do_volume_snapshot_show(cs, args): def do_volume_snapshot_show(cs, args):
"""Show details about a snapshot.""" """Show details about a snapshot."""
snapshot = _find_volume_snapshot(cs, args.snapshot) snapshot = _find_volume_snapshot(cs, args.snapshot)
@ -848,6 +854,7 @@ def do_volume_snapshot_show(cs, args):
@utils.arg('--display_description', metavar='<display_description>', @utils.arg('--display_description', metavar='<display_description>',
help='Optional snapshot description. (Default=None)', help='Optional snapshot description. (Default=None)',
default=None) default=None)
@utils.service_type('volume')
def do_volume_snapshot_create(cs, args): def do_volume_snapshot_create(cs, args):
"""Add a new snapshot.""" """Add a new snapshot."""
cs.volume_snapshots.create(args.volume_id, cs.volume_snapshots.create(args.volume_id,
@ -859,6 +866,7 @@ def do_volume_snapshot_create(cs, args):
@utils.arg('snapshot_id', @utils.arg('snapshot_id',
metavar='<snapshot_id>', metavar='<snapshot_id>',
help='ID of the snapshot to delete.') help='ID of the snapshot to delete.')
@utils.service_type('volume')
def do_volume_snapshot_delete(cs, args): def do_volume_snapshot_delete(cs, args):
"""Remove a snapshot.""" """Remove a snapshot."""
snapshot = _find_volume_snapshot(cs, args.snapshot_id) snapshot = _find_volume_snapshot(cs, args.snapshot_id)

View File

@ -57,7 +57,7 @@ class SnapshotManager(base.ManagerWithFind):
'force': force, 'force': force,
'display_name': display_name, 'display_name': display_name,
'display_description': display_description}} 'display_description': display_description}}
return self._create('/os-snapshots', body, 'snapshot') return self._create('/snapshots', body, 'snapshot')
def get(self, snapshot_id): def get(self, snapshot_id):
""" """
@ -66,7 +66,7 @@ class SnapshotManager(base.ManagerWithFind):
:param snapshot_id: The ID of the snapshot to get. :param snapshot_id: The ID of the snapshot to get.
:rtype: :class:`Snapshot` :rtype: :class:`Snapshot`
""" """
return self._get("/os-snapshots/%s" % snapshot_id, "snapshot") return self._get("/snapshots/%s" % snapshot_id, "snapshot")
def list(self, detailed=True): def list(self, detailed=True):
""" """
@ -75,9 +75,9 @@ class SnapshotManager(base.ManagerWithFind):
:rtype: list of :class:`Snapshot` :rtype: list of :class:`Snapshot`
""" """
if detailed is True: if detailed is True:
return self._list("/os-snapshots/detail", "snapshots") return self._list("/snapshots/detail", "snapshots")
else: else:
return self._list("/os-snapshots", "snapshots") return self._list("/snapshots", "snapshots")
def delete(self, snapshot): def delete(self, snapshot):
""" """
@ -85,4 +85,4 @@ class SnapshotManager(base.ManagerWithFind):
:param snapshot: The :class:`Snapshot` to delete. :param snapshot: The :class:`Snapshot` to delete.
""" """
self._delete("/os-snapshots/%s" % base.getid(snapshot)) self._delete("/snapshots/%s" % base.getid(snapshot))

View File

@ -55,7 +55,7 @@ class VolumeManager(base.ManagerWithFind):
'snapshot_id': snapshot_id, 'snapshot_id': snapshot_id,
'display_name': display_name, 'display_name': display_name,
'display_description': display_description}} 'display_description': display_description}}
return self._create('/os-volumes', body, 'volume') return self._create('/volumes', body, 'volume')
def get(self, volume_id): def get(self, volume_id):
""" """
@ -64,7 +64,7 @@ class VolumeManager(base.ManagerWithFind):
:param volume_id: The ID of the volume to delete. :param volume_id: The ID of the volume to delete.
:rtype: :class:`Volume` :rtype: :class:`Volume`
""" """
return self._get("/os-volumes/%s" % volume_id, "volume") return self._get("/volumes/%s" % volume_id, "volume")
def list(self, detailed=True): def list(self, detailed=True):
""" """
@ -73,9 +73,9 @@ class VolumeManager(base.ManagerWithFind):
:rtype: list of :class:`Volume` :rtype: list of :class:`Volume`
""" """
if detailed is True: if detailed is True:
return self._list("/os-volumes/detail", "volumes") return self._list("/volumes/detail", "volumes")
else: else:
return self._list("/os-volumes", "volumes") return self._list("/volumes", "volumes")
def delete(self, volume): def delete(self, volume):
""" """
@ -83,7 +83,7 @@ class VolumeManager(base.ManagerWithFind):
:param volume: The :class:`Volume` to delete. :param volume: The :class:`Volume` to delete.
""" """
self._delete("/os-volumes/%s" % base.getid(volume)) self._delete("/volumes/%s" % base.getid(volume))
def create_server_volume(self, server_id, volume_id, device): def create_server_volume(self, server_id, volume_id, device):
""" """

View File

@ -59,13 +59,13 @@ SERVICE_CATALOG = {
"endpoints_links": [], "endpoints_links": [],
}, },
{ {
"name": "Cloud Files", "name": "Nova Volumes",
"type": "object-store", "type": "volume",
"endpoints": [ "endpoints": [
{ {
"tenantId": "11", "tenantId": "1",
"publicURL": "https://compute1.host/v1/blah-blah", "publicURL": "https://volume1.host/v1/1234",
"internalURL": "https://compute1.host/v1/blah-blah", "internalURL": "https://volume1.host/v1/1234",
"region": "South", "region": "South",
"versionId": "1.0", "versionId": "1.0",
"versionInfo": "uri", "versionInfo": "uri",
@ -73,12 +73,12 @@ SERVICE_CATALOG = {
}, },
{ {
"tenantId": "2", "tenantId": "2",
"publicURL": "https://compute1.host/v1.1/blah-blah", "publicURL": "https://volume1.host/v1.1/3456",
"internalURL": "https://compute1.host/v1.1/blah-blah", "internalURL": "https://volume1.host/v1.1/3456",
"region": "South", "region": "South",
"versionId": "1.1", "versionId": "1.1",
"versionInfo": "https://compute1.host/v1.1/", "versionInfo": "https://volume1.host/v1.1/",
"versionList": "https://compute1.host/" "versionList": "https://volume1.host/"
}, },
], ],
"endpoints_links": [ "endpoints_links": [
@ -103,11 +103,25 @@ class ServiceCatalogTest(utils.TestCase):
def test_building_a_service_catalog(self): def test_building_a_service_catalog(self):
sc = service_catalog.ServiceCatalog(SERVICE_CATALOG) sc = service_catalog.ServiceCatalog(SERVICE_CATALOG)
self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for) self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for,
self.assertEquals(sc.url_for('tenantId', '1'), service_type='compute')
self.assertEquals(sc.url_for('tenantId', '1', service_type='compute'),
"https://compute1.host/v1/1234") "https://compute1.host/v1/1234")
self.assertEquals(sc.url_for('tenantId', '2'), self.assertEquals(sc.url_for('tenantId', '2', service_type='compute'),
"https://compute1.host/v1.1/3456") "https://compute1.host/v1.1/3456")
self.assertRaises(exceptions.EndpointNotFound, self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
sc.url_for, "region", "South") "region", "South", service_type='compute')
def test_alternate_service_type(self):
sc = service_catalog.ServiceCatalog(SERVICE_CATALOG)
self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for,
service_type='volume')
self.assertEquals(sc.url_for('tenantId', '1', service_type='volume'),
"https://volume1.host/v1/1234")
self.assertEquals(sc.url_for('tenantId', '2', service_type='volume'),
"https://volume1.host/v1.1/3456")
self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
"region", "North", service_type='volume')

View File

@ -18,7 +18,7 @@ def to_http_response(resp_dict):
class AuthenticateAgainstKeystoneTests(utils.TestCase): class AuthenticateAgainstKeystoneTests(utils.TestCase):
def test_authenticate_success(self): def test_authenticate_success(self):
cs = client.Client("username", "password", "project_id", cs = client.Client("username", "password", "project_id",
"auth_url/v2.0") "auth_url/v2.0", service_type='compute')
resp = { resp = {
"access": { "access": {
"token": { "token": {
@ -99,7 +99,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
def test_auth_redirect(self): def test_auth_redirect(self):
cs = client.Client("username", "password", "project_id", cs = client.Client("username", "password", "project_id",
"auth_url/v1.0") "auth_url/v1.0", service_type='compute')
dict_correct_response = { dict_correct_response = {
"access": { "access": {
"token": { "token": {
@ -180,7 +180,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
def test_ambiguous_endpoints(self): def test_ambiguous_endpoints(self):
cs = client.Client("username", "password", "project_id", cs = client.Client("username", "password", "project_id",
"auth_url/v2.0") "auth_url/v2.0", service_type='compute')
resp = { resp = {
"access": { "access": {
"token": { "token": {