diff --git a/requirements.txt b/requirements.txt index 4cab695..058c282 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,6 @@ pbr>=0.5.21,<1.0 Babel>=1.3 oslo.config>=1.2.0 iso8601>=0.1.8 +requests>=1.1 +python-keystoneclient>=0.4.2 +stevedore>=0.12 diff --git a/solumclient/client.py b/solumclient/client.py new file mode 100644 index 0000000..1e45481 --- /dev/null +++ b/solumclient/client.py @@ -0,0 +1,34 @@ +# Copyright 2013 - Noorul Islam K M +# +# 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. + +from solumclient.common import auth +from solumclient.openstack.common.apiclient import client + +API_NAME = 'solum' +VERSION_MAP = { + '1': 'solumclient.v1.client.Client', +} + + +def Client(version, **kwargs): + client_class = client.BaseClient.get_class(API_NAME, version, VERSION_MAP) + keystone_auth = auth.KeystoneAuthPlugin( + username=kwargs.get('username'), + password=kwargs.get('password'), + tenant_name=kwargs.get('tenant_name'), + token=kwargs.get('token'), + auth_url=kwargs.get('auth_url'), + solum_url=kwargs.get('solum_url')) + http_client = client.HTTPClient(keystone_auth) + return client_class(http_client) diff --git a/solumclient/common/__init__.py b/solumclient/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solumclient/common/auth.py b/solumclient/common/auth.py new file mode 100644 index 0000000..b032e6d --- /dev/null +++ b/solumclient/common/auth.py @@ -0,0 +1,74 @@ +# Copyright 2013 - Noorul Islam K M +# +# 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. + +from keystoneclient.v2_0 import client as ksclient + +from solumclient.openstack.common.apiclient import auth +from solumclient.openstack.common.apiclient import exceptions + + +class KeystoneAuthPlugin(auth.BaseAuthPlugin): + + opt_names = [ + "username", + "password", + "tenant_name", + "token", + "auth_url", + "solum_url" + ] + + def _do_authenticate(self, http_client): + if self.opts.get('token') is None: + ks_kwargs = { + 'username': self.opts.get('username'), + 'password': self.opts.get('password'), + 'tenant_name': self.opts.get('tenant_name'), + 'auth_url': self.opts.get('auth_url'), + } + + self._ksclient = ksclient.Client(**ks_kwargs) + + def token_and_endpoint(self, endpoint_type, service_type): + token = endpoint = None + + if self.opts.get('token') and self.opts.get('solum_url'): + token = self.opts.get('token') + endpoint = self.opts.get('solum_url') + + elif hasattr(self, '_ksclient'): + token = self._ksclient.auth_token + endpoint = (self.opts.get('solum_url') or + self._ksclient.service_catalog.url_for( + service_type=service_type, + endpoint_type=endpoint_type)) + + return (token, endpoint) + + def sufficient_options(self): + """Check if all required options are present. + + :raises: AuthPluginOptionsMissing + """ + + if self.opts.get('token'): + lookup_table = ["token", "solum_url"] + else: + lookup_table = ["username", "password", "tenant_name", "auth_url"] + + missing = [opt + for opt in lookup_table + if not self.opts.get(opt)] + if missing: + raise exceptions.AuthPluginOptionsMissing(missing) diff --git a/solumclient/common/base.py b/solumclient/common/base.py new file mode 100644 index 0000000..50556e9 --- /dev/null +++ b/solumclient/common/base.py @@ -0,0 +1,33 @@ +# Copyright 2013 - Noorul Islam K M +# +# 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. + +from solumclient.openstack.common.apiclient import base + + +class BaseManager(base.BaseManager): + def _get(self, url, response_key=None): + """Get an object from collection. + + :param url: a partial URL, e.g., '/servers' + :param response_key: the key to be looked up in response dictionary, + e.g., 'server' + """ + body = self.client.get(url).json() + + if response_key is None: + data = body + else: + data = body[response_key] + + return self.resource_class(self, data, loaded=True) diff --git a/solumclient/tests/common/__init__.py b/solumclient/tests/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solumclient/tests/common/test_auth.py b/solumclient/tests/common/test_auth.py new file mode 100644 index 0000000..488893c --- /dev/null +++ b/solumclient/tests/common/test_auth.py @@ -0,0 +1,69 @@ +# Copyright 2013 - Noorul Islam K M +# +# 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. + +from keystoneclient.v2_0 import client as ksclient +import mock + +from solumclient.common import auth +from solumclient.openstack.common.apiclient import client +from solumclient.tests import base + + +@mock.patch.object(ksclient, 'Client') +class KeystoneAuthPluginTest(base.TestCase): + def setUp(self): + super(KeystoneAuthPluginTest, self).setUp() + plugin = auth.KeystoneAuthPlugin( + username="fake-username", + password="fake-password", + tenant_name="fake-tenant-name", + auth_url="http://auth", + solum_url="http://solum") + self.cs = client.HTTPClient(auth_plugin=plugin) + + def test_authenticate(self, mock_ksclient): + self.cs.authenticate() + mock_ksclient.assert_called_with( + username="fake-username", + password="fake-password", + tenant_name="fake-tenant-name", + auth_url="http://auth") + + def test_token_and_endpoint(self, mock_ksclient): + self.cs.authenticate() + (token, endpoint) = self.cs.auth_plugin.token_and_endpoint( + "fake-endpoint-type", "fake-service-type") + self.assertIsInstance(token, mock.MagicMock) + self.assertEqual(endpoint, "http://solum") + + def test_token_and_endpoint_before_auth(self, mock_ksclient): + (token, endpoint) = self.cs.auth_plugin.token_and_endpoint( + "fake-endpoint-type", "fake-service-type") + self.assertIsNone(token, None) + self.assertIsNone(endpoint, None) + + +@mock.patch.object(ksclient, 'Client') +class KeystoneAuthPluginTokenTest(base.TestCase): + def test_token_and_endpoint(self, mock_ksclient): + plugin = auth.KeystoneAuthPlugin( + token="fake-token", + solum_url="http://solum") + cs = client.HTTPClient(auth_plugin=plugin) + + cs.authenticate() + (token, endpoint) = cs.auth_plugin.token_and_endpoint( + "fake-endpoint-type", "fake-service-type") + self.assertEqual(token, 'fake-token') + self.assertEqual(endpoint, 'http://solum') diff --git a/solumclient/tests/common/test_base.py b/solumclient/tests/common/test_base.py new file mode 100644 index 0000000..1423bc2 --- /dev/null +++ b/solumclient/tests/common/test_base.py @@ -0,0 +1,76 @@ +# Copyright 2013 - Noorul Islam K M +# +# 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. + +from solumclient.common import base as solum_base +from solumclient.openstack.common.apiclient import base as apiclient_base +from solumclient.openstack.common.apiclient import client +from solumclient.openstack.common.apiclient import fake_client +from solumclient.tests import base as test_base + + +fixture1 = { + '/foo_resource': { + 'GET': ( + {}, + {'id': 1, 'name': 'foo'} + ), + } +} + +fixture2 = { + '/foo_resource': { + 'GET': ( + {}, + {'foo_resource': {'id': 1, 'name': 'foo'}} + ), + } +} + + +class FooResource(apiclient_base.Resource): + pass + + +class FooResourceManager(solum_base.BaseManager): + resource_class = FooResource + + def get(self): + return self._get("/foo_resource") + + def get_with_response_key(self): + return self._get("/foo_resource", "foo_resource") + + +class TestClient(client.BaseClient): + + service_type = "test" + + def __init__(self, http_client, extensions=None): + super(TestClient, self).__init__( + http_client, extensions=extensions) + + self.foo_resource = FooResourceManager(self) + + +class BaseManagerTest(test_base.TestCase): + + def test_get(self): + http_client = fake_client.FakeHTTPClient(fixtures=fixture1) + tc = TestClient(http_client) + tc.foo_resource.get() + + def test_get_with_response_key(self): + http_client = fake_client.FakeHTTPClient(fixtures=fixture2) + tc = TestClient(http_client) + tc.foo_resource.get_with_response_key() diff --git a/solumclient/tests/test_client.py b/solumclient/tests/test_client.py new file mode 100644 index 0000000..4cb6f7c --- /dev/null +++ b/solumclient/tests/test_client.py @@ -0,0 +1,31 @@ +# Copyright 2013 - Noorul Islam K M +# +# 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 mock + +from solumclient import client +from solumclient.common import auth +from solumclient.openstack.common.apiclient import exceptions +from solumclient.tests import base + + +class ClientTest(base.TestCase): + + def test_client_unsupported_version(self): + self.assertRaises(exceptions.UnsupportedVersion, + client.Client, '111.11', **{}) + + def test_client(self): + with mock.patch.object(auth, 'KeystoneAuthPlugin'): + client.Client('1', **{}) diff --git a/solumclient/tests/v1/__init__.py b/solumclient/tests/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solumclient/tests/v1/test_platform.py b/solumclient/tests/v1/test_platform.py new file mode 100644 index 0000000..9bbefde --- /dev/null +++ b/solumclient/tests/v1/test_platform.py @@ -0,0 +1,62 @@ +# Copyright 2013 - Noorul Islam K M +# +# 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. + +from solumclient.openstack.common.apiclient import fake_client +from solumclient.tests import base +from solumclient.v1 import client as solumclient +from solumclient.v1 import platform + + +fixtures = { + '/v1': { + 'GET': ( + {}, + { + 'uri': 'http://example.com/v1', + 'name': 'solum', + 'type': 'platform', + 'tags': ['solid'], + 'project_id': '1dae5a09ef2b4d8cbf3594b0eb4f6b94', + 'user_id': '55f41cf46df74320b9486a35f5d28a11', + 'description': 'solum native implementation', + 'implementation_version': '2014.1.1', + 'assemblies_uri': 'http://example.com:9777/v1/assemblies', + 'services_uri': 'http://example.com:9777/v1/services', + 'components_uri': 'http://example.com:9777/v1/components', + 'extenstions_uri': 'http://example.com:9777/v1/extenstions' + } + ), + } +} + + +class PlatformManagerTest(base.TestCase): + + def setUp(self): + super(PlatformManagerTest, self).setUp() + fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + api_client = solumclient.Client(fake_http_client) + self.mgr = platform.PlatformManager(api_client) + + def test_get(self): + platform = self.mgr.get() + self.assertIn('Platform', repr(platform)) + self.assertEqual(platform.uri, + 'http://example.com/v1') + self.assertEqual(platform.type, + 'platform') + self.assertEqual(platform.project_id, + '1dae5a09ef2b4d8cbf3594b0eb4f6b94') + self.assertEqual(platform.user_id, + '55f41cf46df74320b9486a35f5d28a11') diff --git a/solumclient/v1/__init__.py b/solumclient/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solumclient/v1/client.py b/solumclient/v1/client.py new file mode 100644 index 0000000..9b8421a --- /dev/null +++ b/solumclient/v1/client.py @@ -0,0 +1,25 @@ +# Copyright 2013 - Noorul Islam K M +# +# 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. + +from solumclient.openstack.common.apiclient import client +from solumclient.v1 import platform + + +class Client(client.BaseClient): + """Client for the Solum v1 API.""" + + def __init__(self, http_client, extensions=None): + """Initialize a new client for the Solum v1 API.""" + super(Client, self).__init__(http_client, extensions) + self.platform = platform.PlatformManager(self) diff --git a/solumclient/v1/platform.py b/solumclient/v1/platform.py new file mode 100644 index 0000000..f3513cf --- /dev/null +++ b/solumclient/v1/platform.py @@ -0,0 +1,28 @@ +# Copyright 2013 - Noorul Islam K M +# +# 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. + +from solumclient.common import base as solum_base +from solumclient.openstack.common.apiclient import base as apiclient_base + + +class Platform(apiclient_base.Resource): + def __repr__(self): + return "" % self._info + + +class PlatformManager(solum_base.BaseManager): + resource_class = Platform + + def get(self, **kwargs): + return self._get('/v1') diff --git a/test-requirements.txt b/test-requirements.txt index 26392a6..8c64343 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,3 +9,4 @@ oslo.sphinx testrepository>=0.0.17 testscenarios>=0.4 testtools>=0.9.32 +mock>=1.0