diff --git a/requirements.txt b/requirements.txt index 7ec53a73..ba8a08f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,12 +4,14 @@ Babel>=1.3 # BSD pbr>=1.6 # Apache-2.0 +cliff!=1.16.0,>=1.15.0 # Apache-2.0 PrettyTable<0.8,>=0.7 # BSD openstacksdk # Apache-2.0 oslo.i18n>=2.1.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.4.0 # Apache-2.0 python-heatclient>=0.6.0 # Apache-2.0 +python-openstackclient>=2.0.0 # Apache-2.0 PyYAML>=3.1.0 # MIT requests!=2.9.0,>=2.8.1 # Apache-2.0 six>=1.9.0 # MIT diff --git a/senlinclient/osc/__init__.py b/senlinclient/osc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/senlinclient/osc/plugin.py b/senlinclient/osc/plugin.py new file mode 100644 index 00000000..bc92e30c --- /dev/null +++ b/senlinclient/osc/plugin.py @@ -0,0 +1,46 @@ +# 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 logging + +from openstack import connection +from openstackclient.common import utils + +LOG = logging.getLogger(__name__) + +DEFAULT_CLUSTERING_API_VERSION = '1' +API_VERSION_OPTION = 'os_clustering_api_version' +API_NAME = 'clustering' + + +def make_client(instance): + """Returns a clustering proxy""" + + conn = connection.Connection(authenticator=instance.session.auth) + LOG.debug('Connection: %s', conn) + LOG.debug('Clustering client initialized using OpenStackSDK: %s', + conn.cluster) + return conn.cluster + + +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-clustering-api-version', + metavar='', + default=utils.env( + 'OS_CLUSTERING_API_VERSION', + default=DEFAULT_CLUSTERING_API_VERSION), + help='Clustering API version, default=' + + DEFAULT_CLUSTERING_API_VERSION + + ' (Env: OS_CLUSTERING_API_VERSION)') + return parser diff --git a/senlinclient/osc/v1/__init__.py b/senlinclient/osc/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/senlinclient/osc/v1/profile.py b/senlinclient/osc/v1/profile.py new file mode 100644 index 00000000..1fb27b56 --- /dev/null +++ b/senlinclient/osc/v1/profile.py @@ -0,0 +1,72 @@ +# 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. + +"""Clustering v1 profile action implementations""" + +import logging + +from cliff import show +from openstack import exceptions as sdk_exc +from openstackclient.common import exceptions as exc +from openstackclient.common import utils + +from senlinclient.common import utils as senlin_utils + + +class ShowProfile(show.ShowOne): + """Show profile details.""" + + log = logging.getLogger(__name__ + ".ShowProfile") + + def get_parser(self, prog_name): + parser = super(ShowProfile, self).get_parser(prog_name) + parser.add_argument( + 'profile', + metavar='', + help='Name or ID of profile to show', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + senlin_client = self.app.client_manager.clustering + return _show_profile(senlin_client, profile_id=parsed_args.profile) + + +def _show_profile(senlin_client, profile_id): + try: + data = senlin_client.get_profile(profile_id) + except sdk_exc.ResourceNotFound: + raise exc.CommandError('Profile not found: %s' % profile_id) + else: + formatters = {} + formatters['metadata'] = senlin_utils.json_formatter + formatters['spec'] = senlin_utils.nested_dict_formatter( + ['type', 'version', 'properties'], + ['property', 'value']) + + columns = [ + 'created_at', + 'domain', + 'id', + 'metadata', + 'name', + 'permission', + 'project', + 'spec', + 'type', + 'updated_at', + 'user' + ] + return columns, utils.get_dict_properties(data.to_dict(), columns, + formatters=formatters) diff --git a/senlinclient/tests/unit/osc/__init__.py b/senlinclient/tests/unit/osc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/senlinclient/tests/unit/osc/fakes.py b/senlinclient/tests/unit/osc/fakes.py new file mode 100644 index 00000000..f6d47184 --- /dev/null +++ b/senlinclient/tests/unit/osc/fakes.py @@ -0,0 +1,160 @@ +# 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 requests +import six +import sys + + +AUTH_TOKEN = "foobar" +AUTH_URL = "http://0.0.0.0" +USERNAME = "itchy" +PASSWORD = "scratchy" + +TEST_RESPONSE_DICT_V3 = { + "token": { + "audit_ids": [ + "a" + ], + "catalog": [ + ], + "expires_at": "2034-09-29T18:27:15.978064Z", + "extras": {}, + "issued_at": "2014-09-29T17:27:15.978097Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "default", + "name": "Default" + }, + "id": "bbb", + "name": "project" + }, + "roles": [ + ], + "user": { + "domain": { + "id": "default", + "name": "Default" + }, + "id": "aaa", + "name": USERNAME + } + } +} +TEST_VERSIONS = { + "versions": { + "values": [ + { + "id": "v3.0", + "links": [ + { + "href": AUTH_URL, + "rel": "self" + } + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.identity-v3+json" + }, + { + "base": "application/xml", + "type": "application/vnd.openstack.identity-v3+xml" + } + ], + "status": "stable", + "updated": "2013-03-06T00:00:00Z" + } + ] + } +} + + +class FakeStdout(object): + def __init__(self): + self.content = [] + + def write(self, text): + self.content.append(text) + + def make_string(self): + result = '' + for line in self.content: + result = result + line + return result + + +class FakeApp(object): + def __init__(self, _stdout): + self.stdout = _stdout + self.client_manager = None + self.stdin = sys.stdin + self.stdout = _stdout or sys.stdout + self.stderr = sys.stderr + + +class FakeClient(object): + def __init__(self, **kwargs): + self.auth_url = kwargs['auth_url'] + self.token = kwargs['token'] + + +class FakeClientManager(object): + def __init__(self): + self.compute = None + self.identity = None + self.image = None + self.object_store = None + self.volume = None + self.network = None + self.session = None + self.auth_ref = None + + +class FakeModule(object): + def __init__(self, name, version): + self.name = name + self.__version__ = version + + +class FakeResource(object): + def __init__(self, manager, info, loaded=False): + self.manager = manager + self._info = info + self._add_details(info) + self._loaded = loaded + + def _add_details(self, info): + for (k, v) in six.iteritems(info): + setattr(self, k, v) + + def __repr__(self): + reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and + k != 'manager') + info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) + return "<%s %s>" % (self.__class__.__name__, info) + + +class FakeResponse(requests.Response): + def __init__(self, headers={}, status_code=200, data=None, encoding=None): + super(FakeResponse, self).__init__() + + self.status_code = status_code + + self.headers.update(headers) + self._content = json.dumps(data) + if not isinstance(self._content, six.binary_type): + self._content = self._content.encode() diff --git a/senlinclient/tests/unit/osc/v1/__init__.py b/senlinclient/tests/unit/osc/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/senlinclient/tests/unit/osc/v1/fakes.py b/senlinclient/tests/unit/osc/v1/fakes.py new file mode 100644 index 00000000..19a8363e --- /dev/null +++ b/senlinclient/tests/unit/osc/v1/fakes.py @@ -0,0 +1,33 @@ +# 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 openstackclient.tests import utils +from senlinclient.tests.unit.osc import fakes + + +class FakeClusteringv1Client(object): + def __init__(self, **kwargs): + self.http_client = mock.Mock() + self.http_client.auth_token = kwargs['token'] + self.profiles = fakes.FakeResource(None, {}) + + +class TestClusteringv1(utils.TestCommand): + def setUp(self): + super(TestClusteringv1, self).setUp() + + self.app.client_manager.clustering = FakeClusteringv1Client( + token=fakes.AUTH_TOKEN, + auth_url=fakes.AUTH_URL + ) diff --git a/senlinclient/tests/unit/osc/v1/test_profile.py b/senlinclient/tests/unit/osc/v1/test_profile.py new file mode 100644 index 00000000..4676b969 --- /dev/null +++ b/senlinclient/tests/unit/osc/v1/test_profile.py @@ -0,0 +1,109 @@ +# 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 openstack.cluster.v1 import profile as sdk_profile +from openstack import exceptions as sdk_exc +from openstackclient.common import exceptions as exc +from openstackclient.common import utils + +from senlinclient.osc.v1 import profile as osc_profile +from senlinclient.tests.unit.osc.v1 import fakes + + +class TestProfile(fakes.TestClusteringv1): + def setUp(self): + super(TestProfile, self).setUp() + self.mock_client = self.app.client_manager.clustering + + +class TestProfileShow(TestProfile): + get_response = {"profile": { + "created_at": "2015-03-01T14:28:25", + "domain": 'false', + "id": "7fa885cd-fa39-4531-a42d-780af95c84a4", + "metadata": {}, + "name": "test_prof1", + "project": "42d9e9663331431f97b75e25136307ff", + "spec": { + "disable_rollback": 'false', + "environment": { + "resource_registry": { + "os.heat.server": "OS::Heat::Server" + } + }, + "files": { + "file:///opt/stack/senlin/examples/profiles/test_script.sh": + "#!/bin/bash\n\necho \"this is a test script file\"\n" + }, + "parameters": {}, + "template": { + "heat_template_version": "2014-10-16", + "outputs": { + "result": { + "value": { + "get_attr": [ + "random", + "value" + ] + } + } + }, + "parameters": { + "file": { + "default": { + "get_file": "file:///opt/stack/senlin/" + "examples/profiles/test_script.sh" + }, + "type": "string" + } + }, + "resources": { + "random": { + "properties": { + "length": 64 + }, + "type": "OS::Heat::RandomString" + } + }, + "timeout": 60 + }, + "type": "os.heat.stack", + "version": "1.0" + }, + "type": "os.heat.stack-1.0", + "updated_at": 'null', + "user": "5e5bf8027826429c96af157f68dc9072" + }} + + def setUp(self): + super(TestProfileShow, self).setUp() + self.cmd = osc_profile.ShowProfile(self.app, None) + self.mock_client.get_profile = mock.Mock( + return_value=sdk_profile.Profile(None, self.get_response)) + utils.get_dict_properties = mock.Mock(return_value='') + + def test_profile_show(self): + arglist = ['my_profile'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.get_profile.assert_called_with('my_profile') + + def test_profile_show_not_found(self): + arglist = ['my_profile'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_client.get_profile.side_effect = sdk_exc.ResourceNotFound() + self.assertRaises( + exc.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/setup.cfg b/setup.cfg index c46da5bc..1431aae9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,12 @@ packages = console_scripts = senlin = senlinclient.shell:main +openstack.cli.extension = + clustering = senlinclient.osc.plugin + +openstack.clustering.v1 = + cluster_profile_show = senlinclient.osc.v1.profile:ShowProfile + [global] setup-hooks = pbr.hooks.setup_hook