Add keystone v3 CLI support
This change allows user domains and tenant/project domains to be specified through CLI. This is necessary because keystone v2 API is deprecatedas of Icehouse. Change-Id: I50187a12b73777118ee77590202746b272ecc030
This commit is contained in:
parent
2c84d91456
commit
e3d356476b
|
@ -22,6 +22,7 @@ import os
|
|||
import socket
|
||||
import ssl
|
||||
|
||||
from keystoneclient import adapter
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
|
@ -280,6 +281,62 @@ class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection):
|
|||
return None
|
||||
|
||||
|
||||
class SessionClient(adapter.LegacyJsonAdapter):
|
||||
"""HTTP client based on Keystone client session."""
|
||||
|
||||
def _http_request(self, url, method, **kwargs):
|
||||
kwargs.setdefault('user_agent', USER_AGENT)
|
||||
kwargs.setdefault('auth', self.auth)
|
||||
|
||||
endpoint_filter = kwargs.setdefault('endpoint_filter', {})
|
||||
endpoint_filter.setdefault('interface', self.interface)
|
||||
endpoint_filter.setdefault('service_type', self.service_type)
|
||||
endpoint_filter.setdefault('region_name', self.region_name)
|
||||
|
||||
resp = self.session.request(url, method,
|
||||
raise_exc=False, **kwargs)
|
||||
|
||||
if 400 <= resp.status_code < 600:
|
||||
raise exc.from_response(resp)
|
||||
elif resp.status_code in (301, 302, 305):
|
||||
# Redirected. Reissue the request to the new location.
|
||||
location = resp.headers.get('location')
|
||||
resp = self._http_request(location, method, **kwargs)
|
||||
elif resp.status_code == 300:
|
||||
raise exc.from_response(resp, method=method, url=url)
|
||||
return resp
|
||||
|
||||
def json_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
||||
kwargs['headers'].setdefault('Accept', 'application/json')
|
||||
|
||||
if 'body' in kwargs:
|
||||
kwargs['data'] = json.dumps(kwargs.pop('body'))
|
||||
|
||||
resp = self._http_request(url, method, **kwargs)
|
||||
body = resp.content
|
||||
content_type = resp.headers.get('content-type', None)
|
||||
status = resp.status_code
|
||||
if status == 204 or status == 205 or content_type is None:
|
||||
return resp, list()
|
||||
if 'application/json' in content_type:
|
||||
try:
|
||||
body = resp.json()
|
||||
except ValueError:
|
||||
LOG.error('Could not decode response body as JSON')
|
||||
else:
|
||||
body = None
|
||||
|
||||
return resp, body
|
||||
|
||||
def raw_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type',
|
||||
'application/octet-stream')
|
||||
return self._http_request(url, method, **kwargs)
|
||||
|
||||
|
||||
class ResponseBodyIterator(object):
|
||||
"""A class that acts as an iterator over an HTTP response."""
|
||||
|
||||
|
@ -296,3 +353,22 @@ class ResponseBodyIterator(object):
|
|||
return chunk
|
||||
else:
|
||||
raise StopIteration()
|
||||
|
||||
|
||||
def _construct_http_client(*args, **kwargs):
|
||||
session = kwargs.pop('session', None)
|
||||
auth = kwargs.pop('auth', None)
|
||||
|
||||
if session:
|
||||
service_type = kwargs.pop('service_type', 'baremetal')
|
||||
interface = kwargs.pop('endpoint_type', None)
|
||||
region_name = kwargs.pop('region_name', None)
|
||||
return SessionClient(session=session,
|
||||
auth=auth,
|
||||
interface=interface,
|
||||
service_type=service_type,
|
||||
region_name=region_name,
|
||||
service_name=None,
|
||||
user_agent='python-ironicclient')
|
||||
else:
|
||||
return HTTPClient(*args, **kwargs)
|
||||
|
|
|
@ -23,6 +23,13 @@ import logging
|
|||
import sys
|
||||
|
||||
import httplib2
|
||||
from keystoneclient.auth.identity import v2 as v2_auth
|
||||
from keystoneclient.auth.identity import v3 as v3_auth
|
||||
from keystoneclient import discover
|
||||
from keystoneclient.openstack.common.apiclient import exceptions as ks_exc
|
||||
from keystoneclient import session as kssession
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
|
||||
import ironicclient
|
||||
from ironicclient import client as iroclient
|
||||
|
@ -36,6 +43,46 @@ gettextutils.install('ironicclient')
|
|||
|
||||
class IronicShell(object):
|
||||
|
||||
def _append_global_identity_args(self, parser):
|
||||
# FIXME(dhu): these are global identity (Keystone) arguments which
|
||||
# should be consistent and shared by all service clients. Therefore,
|
||||
# they should be provided by python-keystoneclient. We will need to
|
||||
# refactor this code once this functionality is avaible in
|
||||
# python-keystoneclient.
|
||||
|
||||
# Register arguments needed for a Session
|
||||
kssession.Session.register_cli_options(parser)
|
||||
|
||||
parser.add_argument('--os-user-domain-id',
|
||||
default=cliutils.env('OS_USER_DOMAIN_ID'),
|
||||
help='Defaults to env[OS_USER_DOMAIN_ID].')
|
||||
|
||||
parser.add_argument('--os-user-domain-name',
|
||||
default=cliutils.env('OS_USER_DOMAIN_NAME'),
|
||||
help='Defaults to env[OS_USER_DOMAIN_NAME].')
|
||||
|
||||
parser.add_argument('--os-project-id',
|
||||
default=cliutils.env('OS_PROJECT_ID'),
|
||||
help='Another way to specify tenant ID. '
|
||||
'This option is mutually exclusive with '
|
||||
' --os-tenant-id. '
|
||||
'Defaults to env[OS_PROJECT_ID].')
|
||||
|
||||
parser.add_argument('--os-project-name',
|
||||
default=cliutils.env('OS_PROJECT_NAME'),
|
||||
help='Another way to specify tenant name. '
|
||||
'This option is mutually exclusive with '
|
||||
' --os-tenant-name. '
|
||||
'Defaults to env[OS_PROJECT_NAME].')
|
||||
|
||||
parser.add_argument('--os-project-domain-id',
|
||||
default=cliutils.env('OS_PROJECT_DOMAIN_ID'),
|
||||
help='Defaults to env[OS_PROJECT_DOMAIN_ID].')
|
||||
|
||||
parser.add_argument('--os-project-domain-name',
|
||||
default=cliutils.env('OS_PROJECT_DOMAIN_NAME'),
|
||||
help='Defaults to env[OS_PROJECT_DOMAIN_NAME].')
|
||||
|
||||
def get_base_parser(self):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='ironic',
|
||||
|
@ -65,36 +112,20 @@ class IronicShell(object):
|
|||
default=False, action="store_true",
|
||||
help="Print more verbose output")
|
||||
|
||||
parser.add_argument('-k', '--insecure',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="Explicitly allow ironicclient to "
|
||||
"perform \"insecure\" SSL (https) requests. "
|
||||
"The server's certificate will "
|
||||
"not be verified against any certificate "
|
||||
"authorities. This option should be used with "
|
||||
"caution")
|
||||
|
||||
# for backward compatibility only
|
||||
parser.add_argument('--cert-file',
|
||||
help='Path of certificate file to use in SSL '
|
||||
'connection. This file can optionally be prepended'
|
||||
' with the private key')
|
||||
dest='os_cert',
|
||||
help='DEPRECATED! Use --os-cert.')
|
||||
|
||||
# for backward compatibility only
|
||||
parser.add_argument('--key-file',
|
||||
help='Path of client key to use in SSL connection.'
|
||||
' This option is not necessary if your key is '
|
||||
'prepended to your cert file')
|
||||
dest='os_key',
|
||||
help='DEPRECATED! Use --os-key.')
|
||||
|
||||
# for backward compatibility only
|
||||
parser.add_argument('--ca-file',
|
||||
help='Path of CA SSL certificate(s) used to verify'
|
||||
' the remote server certificate. Without this '
|
||||
'option ironic looks for the default system '
|
||||
'CA certificates')
|
||||
|
||||
parser.add_argument('--timeout',
|
||||
default=600,
|
||||
help='Number of seconds to wait for a response; '
|
||||
'defaults to 600')
|
||||
dest='os_cacert',
|
||||
help='DEPRECATED! Use --os-cacert.')
|
||||
|
||||
parser.add_argument('--os-username',
|
||||
default=cliutils.env('OS_USERNAME'),
|
||||
|
@ -177,6 +208,12 @@ class IronicShell(object):
|
|||
parser.add_argument('--os_endpoint_type',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
# FIXME(gyee): this method should come from python-keystoneclient.
|
||||
# Will refactor this code once it is available.
|
||||
# https://bugs.launchpad.net/python-keystoneclient/+bug/1332337
|
||||
|
||||
self._append_global_identity_args(parser)
|
||||
|
||||
return parser
|
||||
|
||||
def get_subcommand_parser(self, version):
|
||||
|
@ -213,6 +250,94 @@ class IronicShell(object):
|
|||
commands.remove('bash-completion')
|
||||
print(' '.join(commands | options))
|
||||
|
||||
def _discover_auth_versions(self, session, auth_url):
|
||||
# discover the API versions the server is supporting base on the
|
||||
# given URL
|
||||
v2_auth_url = None
|
||||
v3_auth_url = None
|
||||
try:
|
||||
ks_discover = discover.Discover(session=session, auth_url=auth_url)
|
||||
v2_auth_url = ks_discover.url_for('2.0')
|
||||
v3_auth_url = ks_discover.url_for('3.0')
|
||||
except ks_exc.ClientException:
|
||||
# Identity service may not support discover API version.
|
||||
# Let's try to figure out the API version from the original URL.
|
||||
url_parts = urlparse.urlparse(auth_url)
|
||||
(scheme, netloc, path, params, query, fragment) = url_parts
|
||||
path = path.lower()
|
||||
if path.startswith('/v3'):
|
||||
v3_auth_url = auth_url
|
||||
elif path.startswith('/v2'):
|
||||
v2_auth_url = auth_url
|
||||
else:
|
||||
# not enough information to determine the auth version
|
||||
msg = _('Unable to determine the Keystone version '
|
||||
'to authenticate with using the given '
|
||||
'auth_url. Identity service may not support API '
|
||||
'version discovery. Please provide a versioned '
|
||||
'auth_url instead. %s') % auth_url
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
return (v2_auth_url, v3_auth_url)
|
||||
|
||||
def _get_keystone_v3_auth(self, v3_auth_url, **kwargs):
|
||||
auth_token = kwargs.pop('auth_token', None)
|
||||
if auth_token:
|
||||
return v3_auth.Token(v3_auth_url, auth_token)
|
||||
else:
|
||||
return v3_auth.Password(v3_auth_url, **kwargs)
|
||||
|
||||
def _get_keystone_v2_auth(self, v2_auth_url, **kwargs):
|
||||
auth_token = kwargs.pop('auth_token', None)
|
||||
if auth_token:
|
||||
return v2_auth.Token(v2_auth_url, auth_token,
|
||||
tenant_id=kwargs.pop('project_id', None),
|
||||
tenant_name=kwargs.pop('project_name', None))
|
||||
else:
|
||||
return v2_auth.Password(v2_auth_url,
|
||||
username=kwargs.pop('username', None),
|
||||
password=kwargs.pop('password', None),
|
||||
tenant_id=kwargs.pop('project_id', None),
|
||||
tenant_name=kwargs.pop('project_name', None))
|
||||
|
||||
def _get_keystone_auth(self, session, auth_url, **kwargs):
|
||||
# FIXME(dhu): this code should come from keystoneclient
|
||||
|
||||
# discover the supported keystone versions using the given url
|
||||
(v2_auth_url, v3_auth_url) = self._discover_auth_versions(
|
||||
session=session,
|
||||
auth_url=auth_url)
|
||||
|
||||
# Determine which authentication plugin to use. First inspect the
|
||||
# auth_url to see the supported version. If both v3 and v2 are
|
||||
# supported, then use the highest version if possible.
|
||||
auth = None
|
||||
if v3_auth_url and v2_auth_url:
|
||||
user_domain_name = kwargs.get('user_domain_name', None)
|
||||
user_domain_id = kwargs.get('user_domain_id', None)
|
||||
project_domain_name = kwargs.get('project_domain_name', None)
|
||||
project_domain_id = kwargs.get('project_domain_id', None)
|
||||
|
||||
# support both v2 and v3 auth. Use v3 if domain information is
|
||||
# provided.
|
||||
if (user_domain_name or user_domain_id or project_domain_name or
|
||||
project_domain_id):
|
||||
auth = self._get_keystone_v3_auth(v3_auth_url, **kwargs)
|
||||
else:
|
||||
auth = self._get_keystone_v2_auth(v2_auth_url, **kwargs)
|
||||
elif v3_auth_url:
|
||||
# support only v3
|
||||
auth = self._get_keystone_v3_auth(v3_auth_url, **kwargs)
|
||||
elif v2_auth_url:
|
||||
# support only v2
|
||||
auth = self._get_keystone_v2_auth(v2_auth_url, **kwargs)
|
||||
else:
|
||||
raise exc.CommandError('Unable to determine the Keystone version '
|
||||
'to authenticate with using the given '
|
||||
'auth_url.')
|
||||
|
||||
return auth
|
||||
|
||||
def main(self, argv):
|
||||
# Parse args once to find version
|
||||
parser = self.get_base_parser()
|
||||
|
@ -252,17 +377,73 @@ class IronicShell(object):
|
|||
"either --os-password or via "
|
||||
"env[OS_PASSWORD]"))
|
||||
|
||||
if not (args.os_tenant_id or args.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 (args.os_tenant_id or args.os_tenant_name or
|
||||
args.os_project_id or 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 args.os_auth_url:
|
||||
raise exc.CommandError(_("You must provide an auth url via "
|
||||
"either --os-auth-url or via "
|
||||
"env[OS_AUTH_URL]"))
|
||||
|
||||
client = iroclient.get_client(api_version, **(args.__dict__))
|
||||
endpoint = args.ironic_url
|
||||
service_type = args.os_service_type or 'baremetal'
|
||||
project_id = args.os_project_id or args.os_tenant_id
|
||||
project_name = args.os_project_name or args.os_tenant_name
|
||||
|
||||
if args.os_auth_token and args.ironic_url:
|
||||
kwargs = {
|
||||
'token': args.os_auth_token,
|
||||
'insecure': args.insecure,
|
||||
'timeout': args.timeout,
|
||||
'ca_file': args.ca_file,
|
||||
'cert_file': args.cert_file,
|
||||
'key_file': args.key_file,
|
||||
'auth_ref': None,
|
||||
}
|
||||
elif (args.os_username and
|
||||
args.os_password and
|
||||
args.os_auth_url and
|
||||
(project_id or project_name)):
|
||||
|
||||
keystone_session = kssession.Session.load_from_cli_options(args)
|
||||
|
||||
kwargs = {
|
||||
'username': args.os_username,
|
||||
'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,
|
||||
}
|
||||
keystone_auth = self._get_keystone_auth(keystone_session,
|
||||
args.os_auth_url,
|
||||
**kwargs)
|
||||
if not endpoint:
|
||||
svc_type = args.os_service_type
|
||||
region_name = args.os_region_name
|
||||
endpoint = keystone_auth.get_endpoint(keystone_session,
|
||||
service_type=svc_type,
|
||||
region_name=region_name)
|
||||
|
||||
endpoint_type = args.os_endpoint_type or 'publicURL'
|
||||
kwargs = {
|
||||
'auth_url': args.os_auth_url,
|
||||
'session': keystone_session,
|
||||
'auth': keystone_auth,
|
||||
'service_type': service_type,
|
||||
'endpoint_type': endpoint_type,
|
||||
'region_name': args.os_region_name,
|
||||
'username': args.os_username,
|
||||
'password': args.os_password,
|
||||
}
|
||||
client = iroclient.Client(api_version, endpoint, **kwargs)
|
||||
|
||||
try:
|
||||
args.func(client, args)
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from keystoneclient.fixture import v2 as ks_v2_fixture
|
||||
from keystoneclient.fixture import v3 as ks_v3_fixture
|
||||
|
||||
# these are copied from python-keystoneclient tests
|
||||
BASE_HOST = 'http://keystone.example.com'
|
||||
BASE_URL = "%s:5000/" % BASE_HOST
|
||||
UPDATED = '2013-03-06T00:00:00Z'
|
||||
|
||||
V2_URL = "%sv2.0" % BASE_URL
|
||||
V2_DESCRIBED_BY_HTML = {'href': 'http://docs.openstack.org/api/'
|
||||
'openstack-identity-service/2.0/content/',
|
||||
'rel': 'describedby',
|
||||
'type': 'text/html'}
|
||||
V2_DESCRIBED_BY_PDF = {'href': 'http://docs.openstack.org/api/openstack-ident'
|
||||
'ity-service/2.0/identity-dev-guide-2.0.pdf',
|
||||
'rel': 'describedby',
|
||||
'type': 'application/pdf'}
|
||||
|
||||
V2_VERSION = {'id': 'v2.0',
|
||||
'links': [{'href': V2_URL, 'rel': 'self'},
|
||||
V2_DESCRIBED_BY_HTML, V2_DESCRIBED_BY_PDF],
|
||||
'status': 'stable',
|
||||
'updated': UPDATED}
|
||||
|
||||
V3_URL = "%sv3" % BASE_URL
|
||||
V3_MEDIA_TYPES = [{'base': 'application/json',
|
||||
'type': 'application/vnd.openstack.identity-v3+json'},
|
||||
{'base': 'application/xml',
|
||||
'type': 'application/vnd.openstack.identity-v3+xml'}]
|
||||
|
||||
V3_VERSION = {'id': 'v3.0',
|
||||
'links': [{'href': V3_URL, 'rel': 'self'}],
|
||||
'media-types': V3_MEDIA_TYPES,
|
||||
'status': 'stable',
|
||||
'updated': UPDATED}
|
||||
|
||||
TOKENID = uuid.uuid4().hex
|
||||
|
||||
|
||||
def _create_version_list(versions):
|
||||
return json.dumps({'versions': {'values': versions}})
|
||||
|
||||
|
||||
def _create_single_version(version):
|
||||
return json.dumps({'version': version})
|
||||
|
||||
|
||||
V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION])
|
||||
V2_VERSION_LIST = _create_version_list([V2_VERSION])
|
||||
|
||||
V3_VERSION_ENTRY = _create_single_version(V3_VERSION)
|
||||
V2_VERSION_ENTRY = _create_single_version(V2_VERSION)
|
||||
|
||||
|
||||
def keystone_request_callback(request, uri, headers):
|
||||
response_headers = {"content-type": "application/json"}
|
||||
token_id = TOKENID
|
||||
if uri == BASE_URL:
|
||||
return (200, headers, V3_VERSION_LIST)
|
||||
elif uri == BASE_URL + "/v2.0":
|
||||
v2_token = ks_v2_fixture.Token(token_id)
|
||||
return (200, response_headers, json.dumps(v2_token))
|
||||
elif uri == BASE_URL + "/v3":
|
||||
v3_token = ks_v3_fixture.Token()
|
||||
response_headers["X-Subject-Token"] = token_id
|
||||
return (201, response_headers, json.dumps(v3_token))
|
|
@ -12,16 +12,22 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
import fixtures
|
||||
import httplib2
|
||||
import httpretty
|
||||
from keystoneclient.fixture import v2 as ks_v2_fixture
|
||||
from keystoneclient.fixture import v3 as ks_v3_fixture
|
||||
import six
|
||||
import testtools
|
||||
from testtools import matchers
|
||||
|
||||
from ironicclient import exc
|
||||
from ironicclient import shell as ironic_shell
|
||||
from ironicclient.tests import keystone_client_fixtures
|
||||
from ironicclient.tests import utils
|
||||
|
||||
FAKE_ENV = {'OS_USERNAME': 'username',
|
||||
|
@ -29,6 +35,22 @@ FAKE_ENV = {'OS_USERNAME': 'username',
|
|||
'OS_TENANT_NAME': 'tenant_name',
|
||||
'OS_AUTH_URL': 'http://no.where'}
|
||||
|
||||
FAKE_ENV_KEYSTONE_V2 = {
|
||||
'OS_USERNAME': 'username',
|
||||
'OS_PASSWORD': 'password',
|
||||
'OS_TENANT_NAME': 'tenant_name',
|
||||
'OS_AUTH_URL': keystone_client_fixtures.BASE_URL,
|
||||
}
|
||||
|
||||
FAKE_ENV_KEYSTONE_V3 = {
|
||||
'OS_USERNAME': 'username',
|
||||
'OS_PASSWORD': 'password',
|
||||
'OS_TENANT_NAME': 'tenant_name',
|
||||
'OS_AUTH_URL': keystone_client_fixtures.BASE_URL,
|
||||
'OS_USER_DOMAIN_ID': 'default',
|
||||
'OS_PROJECT_DOMAIN_ID': 'default',
|
||||
}
|
||||
|
||||
|
||||
class ShellTest(utils.BaseTestCase):
|
||||
re_options = re.DOTALL | re.MULTILINE
|
||||
|
@ -109,3 +131,128 @@ class ShellTest(utils.BaseTestCase):
|
|||
for r in required:
|
||||
self.assertThat(stdout,
|
||||
matchers.MatchesRegex(r, self.re_options))
|
||||
|
||||
|
||||
class TestCase(testtools.TestCase):
|
||||
|
||||
tokenid = keystone_client_fixtures.TOKENID
|
||||
|
||||
def set_fake_env(self, fake_env):
|
||||
client_env = ('OS_USERNAME', 'OS_PASSWORD', 'OS_TENANT_ID',
|
||||
'OS_TENANT_NAME', 'OS_AUTH_URL', 'OS_REGION_NAME',
|
||||
'OS_AUTH_TOKEN', 'OS_NO_CLIENT_AUTH', 'OS_SERVICE_TYPE',
|
||||
'OS_ENDPOINT_TYPE')
|
||||
|
||||
for key in client_env:
|
||||
self.useFixture(
|
||||
fixtures.EnvironmentVariable(key, fake_env.get(key)))
|
||||
|
||||
# required for testing with Python 2.6
|
||||
def assertRegexpMatches(self, text, expected_regexp, msg=None):
|
||||
"""Fail the test unless the text matches the regular expression."""
|
||||
if isinstance(expected_regexp, six.string_types):
|
||||
expected_regexp = re.compile(expected_regexp)
|
||||
if not expected_regexp.search(text):
|
||||
msg = msg or "Regexp didn't match"
|
||||
msg = '%s: %r not found in %r' % (
|
||||
msg, expected_regexp.pattern, text)
|
||||
raise self.failureException(msg)
|
||||
|
||||
def register_keystone_v2_token_fixture(self):
|
||||
v2_token = ks_v2_fixture.Token(token_id=self.tokenid)
|
||||
service = v2_token.add_service('baremetal')
|
||||
service.add_endpoint('http://ironic.example.com', region='RegionOne')
|
||||
httpretty.register_uri(
|
||||
httpretty.POST,
|
||||
'%s/tokens' % (keystone_client_fixtures.V2_URL),
|
||||
body=json.dumps(v2_token))
|
||||
|
||||
def register_keystone_v3_token_fixture(self):
|
||||
v3_token = ks_v3_fixture.Token()
|
||||
service = v3_token.add_service('baremetal')
|
||||
service.add_standard_endpoints(public='http://ironic.example.com')
|
||||
httpretty.register_uri(
|
||||
httpretty.POST,
|
||||
'%s/auth/tokens' % (keystone_client_fixtures.V3_URL),
|
||||
body=json.dumps(v3_token),
|
||||
adding_headers={'X-Subject-Token': self.tokenid})
|
||||
|
||||
def register_keystone_auth_fixture(self):
|
||||
self.register_keystone_v2_token_fixture()
|
||||
self.register_keystone_v3_token_fixture()
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
keystone_client_fixtures.BASE_URL,
|
||||
body=keystone_client_fixtures.keystone_request_callback)
|
||||
|
||||
|
||||
class ShellTestNoMox(TestCase):
|
||||
def setUp(self):
|
||||
super(ShellTestNoMox, self).setUp()
|
||||
self.set_fake_env(FAKE_ENV_KEYSTONE_V2)
|
||||
|
||||
def shell(self, argstr):
|
||||
orig = sys.stdout
|
||||
try:
|
||||
sys.stdout = six.StringIO()
|
||||
_shell = ironic_shell.IronicShell()
|
||||
_shell.main(argstr.split())
|
||||
self.subcommands = _shell.subcommands.keys()
|
||||
except SystemExit:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
self.assertEqual(0, exc_value.code)
|
||||
finally:
|
||||
out = sys.stdout.getvalue()
|
||||
sys.stdout.close()
|
||||
sys.stdout = orig
|
||||
|
||||
return out
|
||||
|
||||
@httpretty.activate
|
||||
def test_node_list(self):
|
||||
self.register_keystone_auth_fixture()
|
||||
resp_dict = {"nodes": [
|
||||
{"instance_uuid": "null",
|
||||
"uuid": "351a82d6-9f04-4c36-b79a-a38b9e98ff71",
|
||||
"links": [{"href": "http://ironic.example.com:6385/"
|
||||
"v1/nodes/foo",
|
||||
"rel": "self"},
|
||||
{"href": "http://ironic.example.com:6385/"
|
||||
"nodes/foo",
|
||||
"rel": "bookmark"}],
|
||||
"maintenance": "false",
|
||||
"provision_state": "null",
|
||||
"power_state": "power off"},
|
||||
{"instance_uuid": "null",
|
||||
"uuid": "66fbba13-29e8-4b8a-9e80-c655096a40d3",
|
||||
"links": [{"href": "http://ironic.example.com:6385/"
|
||||
"v1/nodes/foo2",
|
||||
"rel": "self"},
|
||||
{"href": "http://ironic.example.com:6385/"
|
||||
"nodes/foo2",
|
||||
"rel": "bookmark"}],
|
||||
"maintenance": "false",
|
||||
"provision_state": "null",
|
||||
"power_state": "power off"}]}
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
'http://ironic.example.com/v1/nodes',
|
||||
status=200,
|
||||
content_type='application/json; charset=UTF-8',
|
||||
body=json.dumps(resp_dict))
|
||||
|
||||
event_list_text = self.shell('node-list')
|
||||
|
||||
required = [
|
||||
'351a82d6-9f04-4c36-b79a-a38b9e98ff71',
|
||||
'66fbba13-29e8-4b8a-9e80-c655096a40d3',
|
||||
]
|
||||
|
||||
for r in required:
|
||||
self.assertRegexpMatches(event_list_text, r)
|
||||
|
||||
|
||||
class ShellTestNoMoxV3(ShellTestNoMox):
|
||||
|
||||
def _set_fake_env(self):
|
||||
self.set_fake_env(FAKE_ENV_KEYSTONE_V3)
|
||||
|
|
|
@ -34,7 +34,7 @@ class Client(object):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize a new client for the Ironic v1 API."""
|
||||
self.http_client = http.HTTPClient(*args, **kwargs)
|
||||
self.http_client = http._construct_http_client(*args, **kwargs)
|
||||
self.chassis = chassis.ChassisManager(self.http_client)
|
||||
self.node = node.NodeManager(self.http_client)
|
||||
self.port = port.PortManager(self.http_client)
|
||||
|
|
|
@ -6,6 +6,7 @@ coverage>=3.6
|
|||
discover
|
||||
fixtures>=0.3.14
|
||||
hacking>=0.9.2,<0.10
|
||||
httpretty>=0.8.0,!=0.8.1,!=0.8.2,!=0.8.3
|
||||
mock>=1.0
|
||||
oslosphinx>=2.2.0 # Apache-2.0
|
||||
python-subunit>=0.0.18
|
||||
|
|
Loading…
Reference in New Issue