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:
parent
39bfd0585b
commit
03f54c57e1
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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))
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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')
|
||||||
|
@ -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": {
|
||||||
|
Loading…
Reference in New Issue
Block a user