Merge "Support using the Keystone V3 API from the Nova CLI"

This commit is contained in:
Jenkins 2014-11-18 01:52:42 +00:00 committed by Gerrit Code Review
commit 99a4439947
9 changed files with 234 additions and 86 deletions

View File

@ -137,20 +137,35 @@ class CompletionCache(object):
class SessionClient(adapter.LegacyJsonAdapter):
def __init__(self, *args, **kwargs):
self.times = []
super(SessionClient, self).__init__(*args, **kwargs)
def request(self, url, method, **kwargs):
# NOTE(jamielennox): The standard call raises errors from
# keystoneclient, where we need to raise the novaclient errors.
raise_exc = kwargs.pop('raise_exc', True)
start_time = time.time()
resp, body = super(SessionClient, self).request(url,
method,
raise_exc=False,
**kwargs)
end_time = time.time()
self.times.append(('%s %s' % (method, url),
start_time, end_time))
if raise_exc and resp.status_code >= 400:
raise exceptions.from_response(resp, body, url, method)
return resp, body
def get_timings(self):
return self.times
def reset_timings(self):
self.times = []
def _original_only(f):
"""Indicates and enforces that this function can only be used if we are

View File

@ -28,7 +28,11 @@ import logging
import os
import pkgutil
import sys
import time
from keystoneclient.auth.identity.generic import password
from keystoneclient.auth.identity.generic import token
from keystoneclient.auth.identity import v3 as identity
from keystoneclient import session as ksession
from oslo.utils import encodeutils
from oslo.utils import strutils
@ -232,6 +236,7 @@ class NovaClientArgumentParser(argparse.ArgumentParser):
class OpenStackComputeShell(object):
times = []
def _append_global_identity_args(self, parser):
# Register the CLI arguments that have moved to the session object.
@ -240,6 +245,14 @@ class OpenStackComputeShell(object):
parser.set_defaults(insecure=utils.env('NOVACLIENT_INSECURE',
default=False))
identity.Password.register_argparse_arguments(parser)
parser.set_defaults(os_username=utils.env('OS_USERNAME',
'NOVA_USERNAME'))
parser.set_defaults(os_password=utils.env('OS_PASSWORD',
'NOVA_PASSWORD'))
parser.set_defaults(os_auth_url=utils.env('OS_AUTH_URL', 'NOVA_URL'))
def get_base_parser(self):
parser = NovaClientArgumentParser(
prog='nova',
@ -281,22 +294,9 @@ class OpenStackComputeShell(object):
default=utils.env('OS_AUTH_TOKEN'),
help='Defaults to env[OS_AUTH_TOKEN]')
parser.add_argument('--os-username',
metavar='<auth-user-name>',
default=utils.env('OS_USERNAME', 'NOVA_USERNAME'),
help=_('Defaults to env[OS_USERNAME].'))
parser.add_argument('--os_username',
help=argparse.SUPPRESS)
parser.add_argument('--os-user-id',
metavar='<auth-user-id>',
default=utils.env('OS_USER_ID'),
help=_('Defaults to env[OS_USER_ID].'))
parser.add_argument('--os-password',
metavar='<auth-password>',
default=utils.env('OS_PASSWORD', 'NOVA_PASSWORD'),
help=_('Defaults to env[OS_PASSWORD].'))
parser.add_argument('--os_password',
help=argparse.SUPPRESS)
@ -312,10 +312,6 @@ class OpenStackComputeShell(object):
default=utils.env('OS_TENANT_ID'),
help=_('Defaults to env[OS_TENANT_ID].'))
parser.add_argument('--os-auth-url',
metavar='<auth-url>',
default=utils.env('OS_AUTH_URL', 'NOVA_URL'),
help=_('Defaults to env[OS_AUTH_URL].'))
parser.add_argument('--os_auth_url',
help=argparse.SUPPRESS)
@ -511,6 +507,19 @@ class OpenStackComputeShell(object):
logging.basicConfig(level=logging.DEBUG,
format=streamformat)
def _get_keystone_auth(self, session, auth_url, **kwargs):
auth_token = kwargs.pop('auth_token', None)
if auth_token:
return token.Token(auth_url, auth_token, **kwargs)
else:
return password.Password(auth_url,
username=kwargs.pop('username'),
user_id=kwargs.pop('user_id'),
password=kwargs.pop('password'),
user_domain_id=kwargs.pop('user_domain_id'),
user_domain_name=kwargs.pop('user_domain_name'),
**kwargs)
def main(self, argv):
# Parse args once to find version and debug settings
parser = self.get_base_parser()
@ -570,6 +579,9 @@ class OpenStackComputeShell(object):
cacert = args.os_cacert
timeout = args.timeout
keystone_session = None
keystone_auth = None
# We may have either, both or none of these.
# If we have both, we don't need USERNAME, PASSWORD etc.
# Fill in the blanks from the SecretsHelper if possible.
@ -603,6 +615,13 @@ class OpenStackComputeShell(object):
must_auth = not (cliutils.isunauthenticated(args.func)
or (auth_token and management_url))
# Do not use Keystone session for cases with no session support. The
# presence of auth_plugin means os_auth_system is present and is not
# keystone.
use_session = True
if auth_plugin or bypass_url or os_cache or volume_service_name:
use_session = False
# FIXME(usrleon): Here should be restrict for project id same as
# for os_username or os_password but for compatibility it is not.
if must_auth:
@ -615,11 +634,14 @@ class OpenStackComputeShell(object):
"or user id via --os-username, --os-user-id, "
"env[OS_USERNAME] or env[OS_USER_ID]"))
if not os_tenant_name and not os_tenant_id:
raise exc.CommandError(_("You must provide a tenant name "
"or tenant id via --os-tenant-name, "
"--os-tenant-id, env[OS_TENANT_NAME] "
"or env[OS_TENANT_ID]"))
if not any([args.os_tenant_name, args.os_tenant_id,
args.os_project_id, args.os_project_name]):
raise exc.CommandError(_("You must provide a project name or"
" project id via --os-project-name,"
" --os-project-id, env[OS_PROJECT_ID]"
" or env[OS_PROJECT_NAME]. You may"
" use os-project and os-tenant"
" interchangeably."))
if not os_auth_url:
if os_auth_system and os_auth_system != 'keystone':
@ -632,13 +654,39 @@ class OpenStackComputeShell(object):
"default url with --os-auth-system "
"or env[OS_AUTH_SYSTEM]"))
project_id = args.os_project_id or args.os_tenant_id
project_name = args.os_project_name or args.os_tenant_name
if use_session:
# Not using Nova auth plugin, so use keystone
start_time = time.time()
keystone_session = ksession.Session.load_from_cli_options(args)
keystone_auth = self._get_keystone_auth(
keystone_session,
args.os_auth_url,
username=args.os_username,
user_id=args.os_user_id,
user_domain_id=args.os_user_domain_id,
user_domain_name=args.os_user_domain_name,
password=args.os_password,
auth_token=args.os_auth_token,
project_id=project_id,
project_name=project_name,
project_domain_id=args.os_project_domain_id,
project_domain_name=args.os_project_domain_name)
end_time = time.time()
self.times.append(('%s %s' % ('auth_url', args.os_auth_url),
start_time, end_time))
if (options.os_compute_api_version and
options.os_compute_api_version != '1.0'):
if not os_tenant_name and not os_tenant_id:
raise exc.CommandError(_("You must provide a tenant name "
"or tenant id via --os-tenant-name, "
"--os-tenant-id, env[OS_TENANT_NAME] "
"or env[OS_TENANT_ID]"))
if not any([args.os_tenant_id, args.os_tenant_name,
args.os_project_id, args.os_project_name]):
raise exc.CommandError(_("You must provide a project name or"
" project id via --os-project-name,"
" --os-project-id, env[OS_PROJECT_ID]"
" or env[OS_PROJECT_NAME]. You may"
" use os-project and os-tenant"
" interchangeably."))
if not os_auth_url:
raise exc.CommandError(_("You must provide an auth url "
@ -658,6 +706,7 @@ class OpenStackComputeShell(object):
timings=args.timings, bypass_url=bypass_url,
os_cache=os_cache, http_log_debug=options.debug,
cacert=cacert, timeout=timeout,
session=keystone_session, auth=keystone_auth,
completion_cache=completion_cache)
# Now check for the password/token of which pieces of the
@ -691,7 +740,11 @@ class OpenStackComputeShell(object):
# This does a couple of bits which are useful even if we've
# got the token + service URL already. It exits fast in that case.
if not cliutils.isunauthenticated(args.func):
self.cs.authenticate()
if not use_session:
# Only call authenticate() if Nova auth plugin is used.
# If keystone is used, authentication is handled as part
# of session.
self.cs.authenticate()
except exc.Unauthorized:
raise exc.CommandError(_("Invalid OpenStack Nova credentials."))
except exc.AuthorizationFailure:
@ -720,12 +773,13 @@ class OpenStackComputeShell(object):
volume_service_name=volume_service_name,
timings=args.timings, bypass_url=bypass_url,
os_cache=os_cache, http_log_debug=options.debug,
session=keystone_session, auth=keystone_auth,
cacert=cacert, timeout=timeout)
args.func(self.cs, args)
if args.timings:
self._dump_timings(self.cs.get_timings())
self._dump_timings(self.times + self.cs.get_timings())
def _dump_timings(self, timings):
class Tyme(object):

View File

@ -16,8 +16,10 @@ import re
import sys
import fixtures
from keystoneclient import fixture
import mock
import prettytable
import requests_mock
import six
from testtools import matchers
@ -29,23 +31,33 @@ from novaclient.tests import utils
FAKE_ENV = {'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_NAME': 'tenant_name',
'OS_AUTH_URL': 'http://no.where'}
'OS_AUTH_URL': 'http://no.where/v2.0'}
FAKE_ENV2 = {'OS_USER_ID': 'user_id',
'OS_PASSWORD': 'password',
'OS_TENANT_ID': 'tenant_id',
'OS_AUTH_URL': 'http://no.where'}
'OS_AUTH_URL': 'http://no.where/v2.0'}
FAKE_ENV3 = {'OS_USER_ID': 'user_id',
'OS_PASSWORD': 'password',
'OS_TENANT_ID': 'tenant_id',
'OS_AUTH_URL': 'http://no.where',
'OS_AUTH_URL': 'http://no.where/v2.0',
'NOVA_ENDPOINT_TYPE': 'novaURL',
'OS_ENDPOINT_TYPE': 'osURL'}
def _create_ver_list(versions):
return {'versions': {'values': versions}}
class ShellTest(utils.TestCase):
_msg_no_tenant_project = ("You must provide a project name or project"
" id via --os-project-name, --os-project-id,"
" env[OS_PROJECT_ID] or env[OS_PROJECT_NAME]."
" You may use os-project and os-tenant"
" interchangeably.")
def make_env(self, exclude=None, fake_env=FAKE_ENV):
env = dict((k, v) for k, v in fake_env.items() if k != exclude)
self.useFixture(fixtures.MonkeyPatch('os.environ', env))
@ -79,6 +91,12 @@ class ShellTest(utils.TestCase):
sys.stderr = orig_stderr
return (stdout, stderr)
def register_keystone_discovery_fixture(self, mreq):
v2_url = "http://no.where/v2.0"
v2_version = fixture.V2Discovery(v2_url)
mreq.register_uri('GET', v2_url, json=_create_ver_list([v2_version]),
status_code=200)
def test_help_unknown_command(self):
self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo')
@ -163,9 +181,7 @@ class ShellTest(utils.TestCase):
self.fail('CommandError not raised')
def test_no_tenant_name(self):
required = ('You must provide a tenant name or tenant id'
' via --os-tenant-name, --os-tenant-id,'
' env[OS_TENANT_NAME] or env[OS_TENANT_ID]')
required = self._msg_no_tenant_project
self.make_env(exclude='OS_TENANT_NAME')
try:
self.shell('list')
@ -175,14 +191,12 @@ class ShellTest(utils.TestCase):
self.fail('CommandError not raised')
def test_no_tenant_id(self):
required = ('You must provide a tenant name or tenant id'
' via --os-tenant-name, --os-tenant-id,'
' env[OS_TENANT_NAME] or env[OS_TENANT_ID]',)
required = self._msg_no_tenant_project
self.make_env(exclude='OS_TENANT_ID', fake_env=FAKE_ENV2)
try:
self.shell('list')
except exceptions.CommandError as message:
self.assertEqual(required, message.args)
self.assertEqual(required, message.args[0])
else:
self.fail('CommandError not raised')
@ -200,15 +214,19 @@ class ShellTest(utils.TestCase):
self.fail('CommandError not raised')
@mock.patch('novaclient.client.Client')
def test_nova_endpoint_type(self, mock_client):
@requests_mock.Mocker()
def test_nova_endpoint_type(self, mock_client, m_requests):
self.make_env(fake_env=FAKE_ENV3)
self.register_keystone_discovery_fixture(m_requests)
self.shell('list')
client_kwargs = mock_client.call_args_list[0][1]
self.assertEqual(client_kwargs['endpoint_type'], 'novaURL')
@mock.patch('novaclient.client.Client')
def test_os_endpoint_type(self, mock_client):
@requests_mock.Mocker()
def test_os_endpoint_type(self, mock_client, m_requests):
self.make_env(exclude='NOVA_ENDPOINT_TYPE', fake_env=FAKE_ENV3)
self.register_keystone_discovery_fixture(m_requests)
self.shell('list')
client_kwargs = mock_client.call_args_list[0][1]
self.assertEqual(client_kwargs['endpoint_type'], 'osURL')
@ -222,7 +240,8 @@ class ShellTest(utils.TestCase):
@mock.patch('sys.stdin', side_effect=mock.MagicMock)
@mock.patch('getpass.getpass', return_value='password')
def test_password(self, mock_getpass, mock_stdin):
@requests_mock.Mocker()
def test_password(self, mock_getpass, mock_stdin, m_requests):
mock_stdin.encoding = "utf-8"
# default output of empty tables differs depending between prettytable
@ -240,6 +259,7 @@ class ShellTest(utils.TestCase):
''
])
self.make_env(exclude='OS_PASSWORD')
self.register_keystone_discovery_fixture(m_requests)
stdout, stderr = self.shell('list')
self.assertEqual((stdout + stderr), ex)
@ -310,3 +330,17 @@ class ShellTest(utils.TestCase):
novaclient.shell.main()
except SystemExit as ex:
self.assertEqual(ex.code, 130)
class ShellTestKeystoneV3(ShellTest):
def make_env(self, exclude=None, fake_env=FAKE_ENV):
if 'OS_AUTH_URL' in fake_env:
fake_env.update({'OS_AUTH_URL': 'http://no.where/v3'})
env = dict((k, v) for k, v in fake_env.items() if k != exclude)
self.useFixture(fixtures.MonkeyPatch('os.environ', env))
def register_keystone_discovery_fixture(self, mreq):
v3_url = "http://no.where/v3"
v3_version = fixture.V3Discovery(v3_url)
mreq.register_uri('GET', v3_url, json=_create_ver_list([v3_version]),
status_code=200)

View File

@ -56,6 +56,7 @@ class ShellTest(utils.TestCase):
'NOVA_PROJECT_ID': 'project_id',
'OS_COMPUTE_API_VERSION': '1.1',
'NOVA_URL': 'http://no.where',
'OS_AUTH_URL': 'http://no.where/v2.0',
}
def setUp(self):

View File

@ -51,6 +51,7 @@ class ShellTest(utils.TestCase):
'NOVA_PROJECT_ID': 'project_id',
'OS_COMPUTE_API_VERSION': '3',
'NOVA_URL': 'http://no.where',
'OS_AUTH_URL': 'http://no.where/v2.0',
}
def setUp(self):

View File

@ -217,11 +217,9 @@ class Client(object):
def set_management_url(self, url):
self.client.set_management_url(url)
@client._original_only
def get_timings(self):
return self.client.get_timings()
@client._original_only
def reset_timings(self):
self.client.reset_timings()

View File

@ -33,6 +33,7 @@ from oslo.utils import strutils
from oslo.utils import timeutils
import six
from novaclient import client
from novaclient import exceptions
from novaclient.i18n import _
from novaclient.openstack.common import cliutils
@ -2818,7 +2819,12 @@ def do_usage(cs, args):
if args.tenant:
usage = cs.usage.get(args.tenant, start, end)
else:
usage = cs.usage.get(cs.client.tenant_id, start, end)
if isinstance(cs.client, client.SessionClient):
auth = cs.client.auth
project_id = auth.get_auth_ref(cs.client.session).project_id
usage = cs.usage.get(project_id, start, end)
else:
usage = cs.usage.get(cs.client.tenant_id, start, end)
print(_("Usage from %(start)s to %(end)s:") %
{'start': start.strftime(dateformat),
@ -3315,23 +3321,32 @@ def ensure_service_catalog_present(cs):
def do_endpoints(cs, _args):
"""Discover endpoints that get returned from the authenticate services."""
ensure_service_catalog_present(cs)
if isinstance(cs.client, client.SessionClient):
auth = cs.client.auth
sc = auth.get_access(cs.client.session).service_catalog
for service in sc.get_data():
_print_endpoints(service, cs.client.region_name)
else:
ensure_service_catalog_present(cs)
catalog = cs.client.service_catalog.catalog
region = cs.client.region_name
catalog = cs.client.service_catalog.catalog
region = cs.client.region_name
for service in catalog['access']['serviceCatalog']:
_print_endpoints(service, region)
for service in catalog['access']['serviceCatalog']:
name, endpoints = service["name"], service["endpoints"]
try:
endpoint = _get_first_endpoint(endpoints, region)
utils.print_dict(endpoint, name)
except LookupError:
print(_("WARNING: %(service)s has no endpoint in %(region)s! "
"Available endpoints for this service:") %
{'service': name, 'region': region})
for other_endpoint in endpoints:
utils.print_dict(other_endpoint, name)
def _print_endpoints(service, region):
name, endpoints = service["name"], service["endpoints"]
try:
endpoint = _get_first_endpoint(endpoints, region)
utils.print_dict(endpoint, name)
except LookupError:
print(_("WARNING: %(service)s has no endpoint in %(region)s! "
"Available endpoints for this service:") %
{'service': name, 'region': region})
for other_endpoint in endpoints:
utils.print_dict(other_endpoint, name)
def _get_first_endpoint(endpoints, region):
@ -3357,11 +3372,19 @@ def _get_first_endpoint(endpoints, region):
help=_('wrap PKI tokens to a specified length, or 0 to disable'))
def do_credentials(cs, _args):
"""Show user credentials returned from auth."""
ensure_service_catalog_present(cs)
catalog = cs.client.service_catalog.catalog
utils.print_dict(catalog['access']['user'], "User Credentials",
wrap=int(_args.wrap))
utils.print_dict(catalog['access']['token'], "Token", wrap=int(_args.wrap))
if isinstance(cs.client, client.SessionClient):
auth = cs.client.auth
sc = auth.get_access(cs.client.session).service_catalog
utils.print_dict(sc.catalog['user'], 'User Credentials',
wrap=int(_args.wrap))
utils.print_dict(sc.get_token(), 'Token', wrap=int(_args.wrap))
else:
ensure_service_catalog_present(cs)
catalog = cs.client.service_catalog.catalog
utils.print_dict(catalog['access']['user'], "User Credentials",
wrap=int(_args.wrap))
utils.print_dict(catalog['access']['token'], "Token",
wrap=int(_args.wrap))
@utils.arg('server', metavar='<server>', help=_('Name or ID of server.'))

View File

@ -176,11 +176,9 @@ class Client(object):
def set_management_url(self, url):
self.client.set_management_url(url)
@client._original_only
def get_timings(self):
return self.client.get_timings()
@client._original_only
def reset_timings(self):
self.client.reset_timings()

View File

@ -31,6 +31,7 @@ from oslo.utils import strutils
from oslo.utils import timeutils
import six
from novaclient import client
from novaclient import exceptions
from novaclient.i18n import _
from novaclient.openstack.common import cliutils
@ -2140,7 +2141,12 @@ def do_usage(cs, args):
if args.tenant:
usage = cs.usage.get(args.tenant, start, end)
else:
usage = cs.usage.get(cs.client.tenant_id, start, end)
if isinstance(cs.client, client.SessionClient):
auth = cs.client.auth
project_id = auth.get_auth_ref(cs.client.session).project_id
usage = cs.usage.get(project_id, start, end)
else:
usage = cs.usage.get(cs.client.tenant_id, start, end)
print("Usage from %s to %s:" % (start.strftime(dateformat),
end.strftime(dateformat)))
@ -2692,23 +2698,33 @@ def ensure_service_catalog_present(cs):
def do_endpoints(cs, _args):
"""Discover endpoints that get returned from the authenticate services."""
ensure_service_catalog_present(cs)
if isinstance(cs.client, client.SessionClient):
auth = cs.client.auth
sc = auth.get_Access(cs.client.session).service_catalog
for service in sc.get_data():
_print_endpoints(service, cs.client.region_name)
else:
ensure_service_catalog_present(cs)
catalog = cs.client.service_catalog.catalog
region = cs.client.region_name
catalog = cs.client.service_catalog.catalog
region = cs.client.region_name
for service in catalog['access']['serviceCatalog']:
name, endpoints = service["name"], service["endpoints"]
for service in catalog['access']['serviceCatalog']:
_print_endpoints(service, region)
try:
endpoint = _get_first_endpoint(endpoints, region)
utils.print_dict(endpoint, name)
except LookupError:
print(_("WARNING: %(service)s has no endpoint in %(region)s! "
"Available endpoints for this service:") %
{'service': name, 'region': region})
for other_endpoint in endpoints:
utils.print_dict(other_endpoint, name)
def _print_endpoints(service, region):
name, endpoints = service["name"], service["endpoints"]
try:
endpoint = _get_first_endpoint(endpoints, region)
utils.print_dict(endpoint, name)
except LookupError:
print(_("WARNING: %(service)s has no endpoint in %(region)s! "
"Available endpoints for this service:") %
{'service': name, 'region': region})
for other_endpoint in endpoints:
utils.print_dict(other_endpoint, name)
def _get_first_endpoint(endpoints, region):
@ -2734,11 +2750,19 @@ def _get_first_endpoint(endpoints, region):
help='wrap PKI tokens to a specified length, or 0 to disable')
def do_credentials(cs, _args):
"""Show user credentials returned from auth."""
ensure_service_catalog_present(cs)
catalog = cs.client.service_catalog.catalog
utils.print_dict(catalog['access']['user'], "User Credentials",
wrap=int(_args.wrap))
utils.print_dict(catalog['access']['token'], "Token", wrap=int(_args.wrap))
if isinstance(cs.client, client.SessionClient):
auth = cs.client.auth
sc = auth.get_access(cs.client.session).service_catalog
utils.print_dict(sc.catalog['user'], 'User Credentials',
wrap=int(_args.wrap))
utils.print_dict(sc.get_token(), 'Token', wrap=int(_args.wrap))
else:
ensure_service_catalog_present(cs)
catalog = cs.client.service_catalog.catalog
utils.print_dict(catalog['access']['user'], "User Credentials",
wrap=int(_args.wrap))
utils.print_dict(catalog['access']['token'], "Token",
wrap=int(_args.wrap))
def do_extension_list(cs, _args):