From 4dc3e186da8b1b2b7577acb8f960f323f2bc710a Mon Sep 17 00:00:00 2001 From: Brad Morgan Date: Mon, 7 Nov 2011 18:23:29 -0800 Subject: [PATCH 1/2] Added new openstack authentication scheme - 'keystone'. (docs here - http://docs.openstack.org/api/openstack-identity-service/2.0/content/Identity-Service-Concepts-e1362.html) Fixed small bug with morph_action_hook as well as affected tests. --- libcloud/common/openstack.py | 64 ++++++++++++++++++++++++++++------------ test/compute/test_openstack.py | 26 ++++++++-------- 2 files changed, 58 insertions(+), 32 deletions(-) diff --git libcloud/common/openstack.py libcloud/common/openstack.py index 794a439..9a3be7d 100644 --- libcloud/common/openstack.py +++ libcloud/common/openstack.py @@ -96,6 +96,8 @@ class OpenStackAuthConnection(ConnectionUserAndKey): return self.authenticate_1_0() elif self.auth_version == "1.1": return self.authenticate_1_1() + elif self.auth_version == "2.0": + return self.authenticate_2_0() else: raise LibcloudError('Unsupported Auth Version requested') @@ -151,6 +153,30 @@ class OpenStackAuthConnection(ConnectionUserAndKey): except KeyError, e: raise MalformedResponseError('Auth JSON response is missing required elements', e) + # 'keystone' - http://docs.openstack.org/api/openstack-identity-service/2.0/content/Identity-Service-Concepts-e1362.html + def authenticate_2_0(self): + reqbody = json.dumps({'auth':{'passwordCredentials':{'username':self.user_id, 'password':self.key}}}) + resp = self.request('tokens/', + data=reqbody, + headers={'Content-Type':'application/json'}, + method='POST') + if resp.status == httplib.UNAUTHORIZED: + raise InvalidCredsError() + elif resp.status not in [httplib.OK, httplib.NON_AUTHORITATIVE_INFORMATION]: + raise MalformedResponseError('Malformed response', + body='code: %s body: %s' % (resp.status, resp.body), + driver=self.driver) + else: + try: + body = json.loads(resp.body) + except Exception as e: + raise MalformedResponseError('Failed to parse JSON', e) + try: + self.auth_token = body['access']['token']['id'] + self.urls = body['access']['serviceCatalog'] + except KeyError as e: + raise MalformedResponseError('Auth JSON response is missing required elements', e) + class OpenStackBaseConnection(ConnectionUserAndKey): auth_url = None @@ -173,21 +199,18 @@ class OpenStackBaseConnection(ConnectionUserAndKey): self._auth_version = '1.1' super(OpenStackBaseConnection, self).__init__( - user_id, key) + user_id, key, secure=secure) def add_default_headers(self, headers): headers['X-Auth-Token'] = self.auth_token headers['Accept'] = self.accept_format return headers - def morph_action(self, action): - key = self._url_key - - value = getattr(self, key, None) + def morph_action_hook(self, action): + value = getattr(self, self._url_key, None) if not value: self._populate_hosts_and_request_paths() - - request_path = getattr(self, '__request_path_%s' % (key), '') + request_path = getattr(self, '__request_path_%s' % (self._url_key), '') action = request_path + action return action @@ -206,10 +229,6 @@ class OpenStackBaseConnection(ConnectionUserAndKey): def _get_default_region(self, arr): if len(arr): - for i in arr: - if i.get('v1Default', False): - return i['publicURL'] - # uber lame return arr[0]['publicURL'] return None @@ -240,13 +259,20 @@ class OpenStackBaseConnection(ConnectionUserAndKey): self.auth_token = osa.auth_token # TODO: Multi-region support - self.server_url = self._get_default_region(osa.urls.get('cloudServers', [])) - self.cdn_management_url = self._get_default_region(osa.urls.get('cloudFilesCDN', [])) - self.storage_url = self._get_default_region(osa.urls.get('cloudFiles', [])) - # TODO: this is even more broken, the service catalog does NOT show load - # balanacers :( You must hard code in the Rackspace Load balancer URLs... - self.lb_url = self.server_url.replace("servers", "ord.loadbalancers") - self.dns_url = self.server_url.replace("servers", "dns") + if self._auth_version == '2.0': + for service in osa.urls: + if service.get('type') == 'compute': + self.server_url = self._get_default_region(service.get('endpoints', [])) + elif self._auth_version in ['1.1', '1.0']: + self.server_url = self._get_default_region(osa.urls.get('cloudServers', [])) + self.cdn_management_url = self._get_default_region(osa.urls.get('cloudFilesCDN', [])) + self.storage_url = self._get_default_region(osa.urls.get('cloudFiles', [])) + # TODO: this is even more broken, the service catalog does NOT show load + # balanacers :( You must hard code in the Rackspace Load balancer URLs... + self.lb_url = self.server_url.replace("servers", "ord.loadbalancers") + self.dns_url = self.server_url.replace("servers", "dns") + else: + raise LibcloudError('auth version "%s" not supported' % (self._auth_version)) for key in ['server_url', 'storage_url', 'cdn_management_url', 'lb_url', 'dns_url']: @@ -259,7 +285,7 @@ class OpenStackBaseConnection(ConnectionUserAndKey): scheme, server, request_path, param, query, fragment = ( urlparse.urlparse(base_url)) # Set host to where we want to make further requests to - setattr(self, '__%s' % (key), server) + setattr(self, '__%s' % (key), server+request_path) setattr(self, '__request_path_%s' % (key), request_path) (self.host, self.port, self.secure, self.request_path) = self._tuple_from_url(self.base_url) diff --git test/compute/test_openstack.py test/compute/test_openstack.py index a3e6fb8..1cb530f 100644 --- test/compute/test_openstack.py +++ test/compute/test_openstack.py @@ -683,41 +683,41 @@ class OpenStack_1_1_MockHttp(MockHttpTestCase): } return (httplib.NO_CONTENT, "", headers, httplib.responses[httplib.NO_CONTENT]) - def _servers_detail(self, method, url, body, headers): + def _v1_1_slug_servers_detail(self, method, url, body, headers): body = self.fixtures.load('_servers_detail.json') return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) - def _flavors_detail(self, method, url, body, headers): + def _v1_1_slug_flavors_detail(self, method, url, body, headers): body = self.fixtures.load('_flavors_detail.json') return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) - def _images_detail(self, method, url, body, headers): + def _v1_1_slug_images_detail(self, method, url, body, headers): body = self.fixtures.load('_images_detail.json') return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) - def _servers(self, method, url, body, headers): + def _v1_1_slug_servers(self, method, url, body, headers): body = self.fixtures.load('_servers.json') return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) - def _servers_12065_action(self, method, url, body, headers): + def _v1_1_slug_servers_12065_action(self, method, url, body, headers): if method != "POST": self.fail('HTTP method other than POST to action URL') return (httplib.ACCEPTED, "", {}, httplib.responses[httplib.ACCEPTED]) - def _servers_12064_action(self, method, url, body, headers): + def _v1_1_slug_servers_12064_action(self, method, url, body, headers): if method != "POST": self.fail('HTTP method other than POST to action URL') return (httplib.ACCEPTED, "", {}, httplib.responses[httplib.ACCEPTED]) - def _servers_12065(self, method, url, body, headers): + def _v1_1_slug_servers_12065(self, method, url, body, headers): if method == "DELETE": return (httplib.ACCEPTED, "", {}, httplib.responses[httplib.ACCEPTED]) else: raise NotImplementedError() - def _servers_12064(self, method, url, body, headers): + def _v1_1_slug_servers_12064(self, method, url, body, headers): if method == "GET": body = self.fixtures.load('_servers_12064.json') return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) @@ -729,12 +729,12 @@ class OpenStack_1_1_MockHttp(MockHttpTestCase): else: raise NotImplementedError() - def _servers_12062(self, method, url, body, headers): + def _v1_1_slug_servers_12062(self, method, url, body, headers): if method == "GET": body = self.fixtures.load('_servers_12064.json') return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) - def _servers_12063_metadata(self, method, url, body, headers): + def _v1_1_slug_servers_12063_metadata(self, method, url, body, headers): if method == "GET": body = self.fixtures.load('_servers_12063_metadata_two_keys.json') return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) @@ -742,21 +742,21 @@ class OpenStack_1_1_MockHttp(MockHttpTestCase): body = self.fixtures.load('_servers_12063_metadata_two_keys.json') return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) - def _flavors_7(self, method, url, body, headers): + def _v1_1_slug_flavors_7(self, method, url, body, headers): if method == "GET": body = self.fixtures.load('_flavors_7.json') return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) else: raise NotImplementedError() - def _images_13(self, method, url, body, headers): + def _v1_1_slug_images_13(self, method, url, body, headers): if method == "GET": body = self.fixtures.load('_images_13.json') return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) else: raise NotImplementedError() - def _images_DELETEUUID(self, method, url, body, headers): + def _v1_1_slug_images_DELETEUUID(self, method, url, body, headers): if method == "DELETE": return (httplib.ACCEPTED, "", {}, httplib.responses[httplib.ACCEPTED]) else: -- 1.7.4.4 From c5cb9d4b8e2f756a30e2b47b1f6fab21b5a98a5c Mon Sep 17 00:00:00 2001 From: Brad Morgan Date: Mon, 7 Nov 2011 18:50:34 -0800 Subject: [PATCH 2/2] add a separate test class that uses auth 2.0, overly redundant because it uses the same node driver, but whatevs --- test/compute/fixtures/openstack/_v2_0__auth.json | 1 + test/compute/test_openstack.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 0 deletions(-) create mode 100644 test/compute/fixtures/openstack/_v2_0__auth.json diff --git test/compute/fixtures/openstack/_v2_0__auth.json test/compute/fixtures/openstack/_v2_0__auth.json new file mode 100644 index 0000000..c343695 --- /dev/null +++ test/compute/fixtures/openstack/_v2_0__auth.json @@ -0,0 +1 @@ +{"access": {"token": {"expires": "2011-11-08T15:57:43.653263", "id": "aaaaaaaaaaaa-bbb-cccccccccccccc", "tenant": {"id": "45", "name": "testproj-project"}}, "serviceCatalog": [{"endpoints": [{"adminURL": "http://my.fake.hostname:8774/v1.1/slug", "region": "ORD", "internalURL": ".", "publicURL": "http://my.fake.hostname:8774/v1.1/slug"}], "type": "compute", "name": "nova"}], "user": {"id": "45", "roles": [{"tenantId": "45", "id": "2", "name": "Member"}], "name": "testproj"}}} diff --git test/compute/test_openstack.py test/compute/test_openstack.py index 1cb530f..ae9a124 100644 --- test/compute/test_openstack.py +++ test/compute/test_openstack.py @@ -676,6 +676,10 @@ class OpenStack_1_1_MockHttp(MockHttpTestCase): auth_fixtures = OpenStackFixtures() json_content_headers = {'content-type': 'application/json; charset=UTF-8'} + def _v2_0_tokens(self, method, url, body, headers): + body = self.auth_fixtures.load('_v2_0__auth.json') + return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) + def _v1_0_(self, method, url, body, headers): headers = { 'x-auth-token': 'FE011C19-CF86-4F87-BE5D-9229145D7A06', @@ -762,6 +766,16 @@ class OpenStack_1_1_MockHttp(MockHttpTestCase): else: raise NotImplementedError() +class OpenStack_1_1_Auth_2_0_Tests(OpenStack_1_1_Tests): + driver_kwargs = {'ex_force_auth_version': '2.0'} + + def setUp(self): + self.driver_klass.connectionCls.conn_classes = (OpenStack_1_1_MockHttp, OpenStack_1_1_MockHttp) + self.driver_klass.connectionCls.auth_url = "https://auth.api.example.com/v2.0/" + OpenStack_1_1_MockHttp.type = None + self.driver = self.create_driver() + clear_pricing_data() + self.node = self.driver.list_nodes()[1] if __name__ == '__main__': sys.exit(unittest.main()) -- 1.7.4.4