diff --git a/novaclient/client.py b/novaclient/client.py index 8d2d3f678..de4a64597 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -21,6 +21,7 @@ OpenStack Client interface. Handles the REST calls and responses. """ import errno +import functools import glob import hashlib import logging @@ -132,6 +133,86 @@ class CompletionCache(object): self._write_attribute(resource, attribute, value) +class SessionClient(object): + + def __init__(self, session, auth, interface, service_type, region_name): + self.session = session + self.auth = auth + + self.interface = interface + self.service_type = service_type + self.region_name = region_name + + def request(self, url, method, **kwargs): + kwargs.setdefault('user_agent', 'python-novaclient') + kwargs.setdefault('auth', self.auth) + kwargs.setdefault('authenticated', False) + + try: + kwargs['json'] = kwargs.pop('body') + except KeyError: + pass + + headers = kwargs.setdefault('headers', {}) + headers.setdefault('Accept', 'application/json') + + 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) + + body = None + if resp.text: + try: + body = resp.json() + except ValueError: + pass + + if resp.status_code >= 400: + raise exceptions.from_response(resp, body, url, method) + + return resp, body + + def _cs_request(self, url, method, **kwargs): + # this function is mostly redundant but makes compatibility easier + kwargs.setdefault('authenticated', True) + return self.request(url, method, **kwargs) + + def get(self, url, **kwargs): + return self._cs_request(url, 'GET', **kwargs) + + def post(self, url, **kwargs): + return self._cs_request(url, 'POST', **kwargs) + + def put(self, url, **kwargs): + return self._cs_request(url, 'PUT', **kwargs) + + def delete(self, url, **kwargs): + return self._cs_request(url, 'DELETE', **kwargs) + + +def _original_only(f): + """Indicates and enforces that this function can only be used if we are + using the original HTTPClient object. + + We use this to specify that if you use the newer Session HTTP client then + you are aware that the way you use your client has been updated and certain + functions are no longer allowed to be used. + """ + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if isinstance(self.client, SessionClient): + msg = ('This call is no longer available. The operation should ' + 'be performed on the session object instead.') + raise exceptions.InvalidUsage(msg) + + return f(self, *args, **kwargs) + + return wrapper + + class HTTPClient(object): USER_AGENT = 'python-novaclient' @@ -606,6 +687,53 @@ class HTTPClient(object): return self._extract_service_catalog(url, resp, respbody) +def _construct_http_client(username=None, password=None, project_id=None, + auth_url=None, insecure=False, timeout=None, + proxy_tenant_id=None, proxy_token=None, + region_name=None, endpoint_type='publicURL', + extensions=None, service_type='compute', + service_name=None, volume_service_name=None, + timings=False, bypass_url=None, os_cache=False, + no_cache=True, http_log_debug=False, + auth_system='keystone', auth_plugin=None, + auth_token=None, cacert=None, tenant_id=None, + user_id=None, connection_pool=False, session=None, + auth=None): + if session: + return SessionClient(session=session, + auth=auth, + interface=endpoint_type, + service_type=service_type, + region_name=region_name) + else: + # FIXME(jamielennox): username and password are now optional. Need + # to test that they were provided in this mode. + return HTTPClient(username, + password, + user_id=user_id, + projectid=project_id, + tenant_id=tenant_id, + auth_url=auth_url, + auth_token=auth_token, + insecure=insecure, + timeout=timeout, + auth_system=auth_system, + auth_plugin=auth_plugin, + proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + region_name=region_name, + endpoint_type=endpoint_type, + service_type=service_type, + service_name=service_name, + volume_service_name=volume_service_name, + timings=timings, + bypass_url=bypass_url, + os_cache=os_cache, + http_log_debug=http_log_debug, + cacert=cacert, + connection_pool=connection_pool) + + def get_client_class(version): version_map = { '1.1': 'novaclient.v1_1.client.Client', diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index c2a6bf6ed..73f04dcef 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -64,6 +64,16 @@ _code_map = dict( ) +class InvalidUsage(RuntimeError): + """This function call is invalid in the way you are using this client. + + Due to the transition to using keystoneclient some function calls are no + longer available. You should make a similar call to the session object + instead. + """ + pass + + def from_response(response, body, url, method=None): """ Return an instance of an HttpError or subclass diff --git a/novaclient/tests/fixture_data/client.py b/novaclient/tests/fixture_data/client.py index a221b9bd1..c4ca3ec38 100644 --- a/novaclient/tests/fixture_data/client.py +++ b/novaclient/tests/fixture_data/client.py @@ -12,6 +12,8 @@ import fixtures import httpretty +from keystoneclient.auth.identity import v2 +from keystoneclient import session from novaclient.openstack.common import jsonutils from novaclient.v1_1 import client as v1_1client @@ -107,3 +109,19 @@ class V3(V1): password='xx', project_id='xx', auth_url=self.identity_url) + + +class SessionV1(V1): + + def new_client(self): + self.session = session.Session() + self.session.auth = v2.Password(self.identity_url, 'xx', 'xx') + return v1_1client.Client(session=self.session) + + +class SessionV3(V1): + + def new_client(self): + self.session = session.Session() + self.session.auth = v2.Password(self.identity_url, 'xx', 'xx') + return v3client.Client(session=self.session) diff --git a/novaclient/tests/utils.py b/novaclient/tests/utils.py index e544d7510..be251998c 100644 --- a/novaclient/tests/utils.py +++ b/novaclient/tests/utils.py @@ -17,6 +17,7 @@ import fixtures import httpretty import requests import six +import testscenarios import testtools from novaclient.openstack.common import jsonutils @@ -43,7 +44,7 @@ class TestCase(testtools.TestCase): self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) -class FixturedTestCase(TestCase): +class FixturedTestCase(testscenarios.TestWithScenarios, TestCase): client_fixture_class = None data_fixture_class = None diff --git a/novaclient/tests/v1_1/test_agents.py b/novaclient/tests/v1_1/test_agents.py index 910400467..46efe839a 100644 --- a/novaclient/tests/v1_1/test_agents.py +++ b/novaclient/tests/v1_1/test_agents.py @@ -24,9 +24,11 @@ from novaclient.v1_1 import agents class AgentsTest(utils.FixturedTestCase): - client_fixture_class = client.V1 data_fixture_class = data.Fixture + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + def stub_hypervisors(self, hypervisor='kvm'): get_os_agents = {'agents': [ diff --git a/novaclient/tests/v1_1/test_aggregates.py b/novaclient/tests/v1_1/test_aggregates.py index 8d41de30d..6e7bd44e9 100644 --- a/novaclient/tests/v1_1/test_aggregates.py +++ b/novaclient/tests/v1_1/test_aggregates.py @@ -21,9 +21,11 @@ from novaclient.v1_1 import aggregates class AggregatesTest(utils.FixturedTestCase): - client_fixture_class = client.V1 data_fixture_class = data.Fixture + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + def test_list_aggregates(self): result = self.cs.aggregates.list() self.assert_called('GET', '/os-aggregates') diff --git a/novaclient/tests/v1_1/test_availability_zone.py b/novaclient/tests/v1_1/test_availability_zone.py index 6cd5fafa1..2144d8c19 100644 --- a/novaclient/tests/v1_1/test_availability_zone.py +++ b/novaclient/tests/v1_1/test_availability_zone.py @@ -27,9 +27,11 @@ class AvailabilityZoneTest(utils.FixturedTestCase): # this class can inherit off the v3 version of shell from novaclient.v1_1 import shell # noqa - client_fixture_class = client.V1 data_fixture_class = data.V1 + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + def setUp(self): super(AvailabilityZoneTest, self).setUp() self.availability_zone_type = self._get_availability_zone_type() diff --git a/novaclient/tests/v1_1/test_certs.py b/novaclient/tests/v1_1/test_certs.py index 0519254c0..0a568d585 100644 --- a/novaclient/tests/v1_1/test_certs.py +++ b/novaclient/tests/v1_1/test_certs.py @@ -19,10 +19,12 @@ from novaclient.v1_1 import certs class CertsTest(utils.FixturedTestCase): - client_fixture_class = client.V1 data_fixture_class = data.Fixture cert_type = certs.Certificate + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + def test_create_cert(self): cert = self.cs.certs.create() self.assert_called('POST', '/os-certificates') diff --git a/novaclient/tests/v1_1/test_cloudpipe.py b/novaclient/tests/v1_1/test_cloudpipe.py index d2183c47e..0a319085b 100644 --- a/novaclient/tests/v1_1/test_cloudpipe.py +++ b/novaclient/tests/v1_1/test_cloudpipe.py @@ -21,9 +21,11 @@ from novaclient.v1_1 import cloudpipe class CloudpipeTest(utils.FixturedTestCase): - client_fixture_class = client.V1 data_fixture_class = data.Fixture + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + def test_list_cloudpipes(self): cp = self.cs.cloudpipe.list() self.assert_called('GET', '/os-cloudpipe') diff --git a/novaclient/tests/v1_1/test_fixed_ips.py b/novaclient/tests/v1_1/test_fixed_ips.py index 38fa31c13..a081a8ecc 100644 --- a/novaclient/tests/v1_1/test_fixed_ips.py +++ b/novaclient/tests/v1_1/test_fixed_ips.py @@ -20,9 +20,11 @@ from novaclient.tests import utils class FixedIpsTest(utils.FixturedTestCase): - client_fixture_class = client.V1 data_fixture_class = data.Fixture + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + def test_get_fixed_ip(self): info = self.cs.fixed_ips.get(fixed_ip='192.168.1.1') self.assert_called('GET', '/os-fixed-ips/192.168.1.1') diff --git a/novaclient/tests/v3/test_agents.py b/novaclient/tests/v3/test_agents.py index 6aaee08c5..bdd101bdc 100644 --- a/novaclient/tests/v3/test_agents.py +++ b/novaclient/tests/v3/test_agents.py @@ -19,7 +19,8 @@ from novaclient.tests.v1_1 import test_agents class AgentsTest(test_agents.AgentsTest): - client_fixture_class = client.V3 + scenarios = [('original', {'client_fixture_class': client.V3}), + ('session', {'client_fixture_class': client.SessionV3})] def _build_example_update_body(self): return {"agent": { diff --git a/novaclient/tests/v3/test_aggregates.py b/novaclient/tests/v3/test_aggregates.py index 7f64fb68f..17e27d162 100644 --- a/novaclient/tests/v3/test_aggregates.py +++ b/novaclient/tests/v3/test_aggregates.py @@ -17,4 +17,6 @@ from novaclient.tests.v1_1 import test_aggregates class AggregatesTest(test_aggregates.AggregatesTest): - client_fixture = client.V3 + + scenarios = [('original', {'client_fixture_class': client.V3}), + ('session', {'client_fixture_class': client.SessionV3})] diff --git a/novaclient/tests/v3/test_availability_zone.py b/novaclient/tests/v3/test_availability_zone.py index ad7f3a715..67a4415bf 100644 --- a/novaclient/tests/v3/test_availability_zone.py +++ b/novaclient/tests/v3/test_availability_zone.py @@ -23,9 +23,11 @@ from novaclient.v3 import availability_zones class AvailabilityZoneTest(test_availability_zone.AvailabilityZoneTest): from novaclient.v3 import shell # noqa - client_fixture_class = client.V3 data_fixture_class = data.V3 + scenarios = [('original', {'client_fixture_class': client.V3}), + ('session', {'client_fixture_class': client.SessionV3})] + def _assertZone(self, zone, name, status): self.assertEqual(zone.zone_name, name) self.assertEqual(zone.zone_state, status) diff --git a/novaclient/tests/v3/test_certs.py b/novaclient/tests/v3/test_certs.py index cf09e58db..30597fc10 100644 --- a/novaclient/tests/v3/test_certs.py +++ b/novaclient/tests/v3/test_certs.py @@ -17,4 +17,5 @@ from novaclient.tests.v1_1 import test_certs class CertsTest(test_certs.CertsTest): - client_fixture_data = client.V3 + scenarios = [('original', {'client_fixture_class': client.V3}), + ('session', {'client_fixture_class': client.SessionV3})] diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index 9b7e01e41..46034bb95 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -78,18 +78,17 @@ class Client(object): ... AUTH_URL, connection_pool=True) """ - # FIXME(jesse): project_id isn't required to authenticate - def __init__(self, username, api_key, project_id, auth_url=None, - insecure=False, timeout=None, proxy_tenant_id=None, - proxy_token=None, region_name=None, - endpoint_type='publicURL', extensions=None, - service_type='compute', service_name=None, - volume_service_name=None, timings=False, - bypass_url=None, os_cache=False, no_cache=True, - http_log_debug=False, auth_system='keystone', - auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None, user_id=None, - connection_pool=False, completion_cache=None): + def __init__(self, username=None, api_key=None, project_id=None, + auth_url=None, insecure=False, timeout=None, + proxy_tenant_id=None, proxy_token=None, region_name=None, + endpoint_type='publicURL', extensions=None, + service_type='compute', service_name=None, + volume_service_name=None, timings=False, bypass_url=None, + os_cache=False, no_cache=True, http_log_debug=False, + auth_system='keystone', auth_plugin=None, auth_token=None, + cacert=None, tenant_id=None, user_id=None, + connection_pool=False, session=None, auth=None, + completion_cache=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument @@ -149,30 +148,33 @@ class Client(object): setattr(self, extension.name, extension.manager_class(self)) - self.client = client.HTTPClient(username, - password, - user_id=user_id, - projectid=project_id, - tenant_id=tenant_id, - auth_url=auth_url, - auth_token=auth_token, - insecure=insecure, - timeout=timeout, - auth_system=auth_system, - auth_plugin=auth_plugin, - proxy_token=proxy_token, - proxy_tenant_id=proxy_tenant_id, - region_name=region_name, - endpoint_type=endpoint_type, - service_type=service_type, - service_name=service_name, - volume_service_name=volume_service_name, - timings=timings, - bypass_url=bypass_url, - os_cache=self.os_cache, - http_log_debug=http_log_debug, - cacert=cacert, - connection_pool=connection_pool) + self.client = client._construct_http_client( + username=username, + password=password, + user_id=user_id, + project_id=project_id, + tenant_id=tenant_id, + auth_url=auth_url, + auth_token=auth_token, + insecure=insecure, + timeout=timeout, + auth_system=auth_system, + auth_plugin=auth_plugin, + proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + region_name=region_name, + endpoint_type=endpoint_type, + service_type=service_type, + service_name=service_name, + volume_service_name=volume_service_name, + timings=timings, + bypass_url=bypass_url, + os_cache=self.os_cache, + http_log_debug=http_log_debug, + cacert=cacert, + connection_pool=connection_pool, + session=session, + auth=auth) self.completion_cache = completion_cache @@ -184,22 +186,28 @@ class Client(object): if self.completion_cache: self.completion_cache.clear_class(obj_class) + @client._original_only def __enter__(self): self.client.open_session() return self + @client._original_only def __exit__(self, t, v, tb): self.client.close_session() + @client._original_only 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() + @client._original_only def authenticate(self): """ Authenticate against the server. diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py index 36a51902e..28ac908b7 100644 --- a/novaclient/v3/client.py +++ b/novaclient/v3/client.py @@ -63,18 +63,17 @@ class Client(object): ... AUTH_URL, connection_pool=True) """ - # FIXME(jesse): project_id isn't required to authenticate - def __init__(self, username, password, project_id, auth_url=None, - insecure=False, timeout=None, proxy_tenant_id=None, - proxy_token=None, region_name=None, - endpoint_type='publicURL', extensions=None, - service_type='computev3', service_name=None, - volume_service_name=None, timings=False, - bypass_url=None, os_cache=False, no_cache=True, - http_log_debug=False, auth_system='keystone', - auth_plugin=None, auth_token=None, - cacert=None, tenant_id=None, user_id=None, - connection_pool=False, completion_cache=None): + def __init__(self, username=None, password=None, project_id=None, + auth_url=None, insecure=False, timeout=None, + proxy_tenant_id=None, proxy_token=None, region_name=None, + endpoint_type='publicURL', extensions=None, + service_type='computev3', service_name=None, + volume_service_name=None, timings=False, bypass_url=None, + os_cache=False, no_cache=True, http_log_debug=False, + auth_system='keystone', auth_plugin=None, auth_token=None, + cacert=None, tenant_id=None, user_id=None, + connection_pool=False, session=None, auth=None, + completion_cache=None): # NOTE(cyeoh): In the novaclient context (unlike Nova) the # project_id is not the same as the tenant_id. Here project_id # is a name (what the Nova API often refers to as a project or @@ -111,30 +110,33 @@ class Client(object): setattr(self, extension.name, extension.manager_class(self)) - self.client = client.HTTPClient(username, - password, - user_id=user_id, - projectid=project_id, - tenant_id=tenant_id, - auth_url=auth_url, - insecure=insecure, - timeout=timeout, - auth_system=auth_system, - auth_plugin=auth_plugin, - auth_token=auth_token, - proxy_token=proxy_token, - proxy_tenant_id=proxy_tenant_id, - region_name=region_name, - endpoint_type=endpoint_type, - service_type=service_type, - service_name=service_name, - volume_service_name=volume_service_name, - timings=timings, - bypass_url=bypass_url, - os_cache=os_cache, - http_log_debug=http_log_debug, - cacert=cacert, - connection_pool=connection_pool) + self.client = client._construct_http_client( + username=username, + password=password, + user_id=user_id, + project_id=project_id, + tenant_id=tenant_id, + auth_url=auth_url, + auth_token=auth_token, + insecure=insecure, + timeout=timeout, + auth_system=auth_system, + auth_plugin=auth_plugin, + proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + region_name=region_name, + endpoint_type=endpoint_type, + service_type=service_type, + service_name=service_name, + volume_service_name=volume_service_name, + timings=timings, + bypass_url=bypass_url, + os_cache=self.os_cache, + http_log_debug=http_log_debug, + cacert=cacert, + connection_pool=connection_pool, + session=session, + auth=auth) self.completion_cache = completion_cache @@ -146,22 +148,28 @@ class Client(object): if self.completion_cache: self.completion_cache.clear_class(obj_class) + @client._original_only def __enter__(self): self.client.open_session() return self + @client._original_only def __exit__(self, t, v, tb): self.client.close_session() + @client._original_only 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() + @client._original_only def authenticate(self): """ Authenticate against the server. diff --git a/test-requirements.txt b/test-requirements.txt index 5f294a3c9..42d186390 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,6 +7,8 @@ httpretty>=0.8.0,!=0.8.1,!=0.8.2 keyring>=2.1 mock>=1.0 sphinx>=1.1.2,!=1.2.0,<1.3 +python-keystoneclient>=0.9.0 oslosphinx testrepository>=0.0.18 +testscenarios>=0.4 testtools>=0.9.34