diff --git a/README.rst b/README.rst index 4f0a3a7..df9ec79 100644 --- a/README.rst +++ b/README.rst @@ -25,6 +25,62 @@ OpenStack. Command-line API ---------------- +To execute CLI commands to standalone searchlight set with keystone. + +* Clone repository for python-searchlightclient:: + + $ git clone https://github.com/openstack/python-searchlightclient.git + $ cd python-searchlightclient + +* Setup a virtualenv + +.. note:: + This is an optional step, but will allow Searchlightclient's dependencies + to be installed in a contained environment that can be easily deleted + if you choose to start over or uninstall Searchlightclient. + +:: + + $ tox -evenv --notest + +Activate the virtual environment whenever you want to work in it. +All further commands in this section should be run with the venv active: + +:: + + $ source .tox/venv/bin/activate + +.. note:: + When ALL steps are complete, deactivate the virtualenv: $ deactivate + +* Install Searchlightclient and its dependencies:: + + (venv) $ python setup.py develop + +* To execute CLI commands:: + + $ export OS_USERNAME= + $ export OS_PASSWORD= + $ export OS_TENANT_NAME= + $ export OS_AUTH_URL='http://localhost:5000/v2.0/' + +.. note:: + With devstack you just need to $ source openrc + +:: + + $ openstack + (openstack) search resource-type list + +--------------------------+--------------------------+ + | Name | Type | + +--------------------------+--------------------------+ + | OS::Designate::RecordSet | OS::Designate::RecordSet | + | OS::Designate::Zone | OS::Designate::Zone | + | OS::Glance::Image | OS::Glance::Image | + | OS::Glance::Metadef | OS::Glance::Metadef | + | OS::Nova::Server | OS::Nova::Server | + +--------------------------+--------------------------+ + Python API ---------- diff --git a/requirements.txt b/requirements.txt index cc38959..6527edc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,12 +4,14 @@ Babel>=1.3 # BSD pbr>=1.6 # Apache-2.0 +cliff>=1.15.0 # Apache-2.0 argparse # PSF PrettyTable<0.8,>=0.7 # BSD oslo.i18n>=1.5.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.4.0 # Apache-2.0 python-keystoneclient!=1.8.0,!=2.1.0,>=1.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/searchlightclient/osc/__init__.py b/searchlightclient/osc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/searchlightclient/osc/plugin.py b/searchlightclient/osc/plugin.py new file mode 100644 index 0000000..d6061c6 --- /dev/null +++ b/searchlightclient/osc/plugin.py @@ -0,0 +1,58 @@ +# 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 openstackclient.common import utils + +LOG = logging.getLogger(__name__) + +DEFAULT_SEARCH_API_VERSION = '1' +API_VERSION_OPTION = 'os_search_api_version' +API_NAME = 'search' +API_VERSIONS = { + '1': 'searchlightclient.v1.client.Client', +} + + +def make_client(instance): + """Returns a search service client""" + search_client = utils.get_client_class( + API_NAME, + instance._api_version[API_NAME], + API_VERSIONS) + + client = search_client( + endpoint=instance.get_endpoint_for_service_type('search'), + session=instance.session, + auth_url=instance._auth_url, + username=instance._username, + password=instance._password, + region_name=instance._region_name, + ) + + return client + + +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-search-api-version', + metavar='', + default=utils.env( + 'OS_SEARCH_API_VERSION', + default=DEFAULT_SEARCH_API_VERSION), + help='Search API version, default=' + + DEFAULT_SEARCH_API_VERSION + + ' (Env: OS_SEARCH_API_VERSION)') + return parser diff --git a/searchlightclient/osc/v1/__init__.py b/searchlightclient/osc/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/searchlightclient/osc/v1/resource_type.py b/searchlightclient/osc/v1/resource_type.py new file mode 100644 index 0000000..36894b6 --- /dev/null +++ b/searchlightclient/osc/v1/resource_type.py @@ -0,0 +1,40 @@ +# 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. +# + +"""Searchlight v1 Resource Type action implementations""" + +import logging + +from cliff import lister +from openstackclient.common import utils + + +class ListResourceType(lister.Lister): + """List Searchlight Resource Type (Plugin).""" + + log = logging.getLogger(__name__ + ".ListResourceType") + + @utils.log_method(log) + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + search_client = self.app.client_manager.search + columns = ( + "Name", + "Type" + ) + data = search_client.resource_types.list() + return (columns, + (utils.get_item_properties( + s, columns, + ) for s in data)) diff --git a/searchlightclient/tests/unit/osc/__init__.py b/searchlightclient/tests/unit/osc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/searchlightclient/tests/unit/osc/fakes.py b/searchlightclient/tests/unit/osc/fakes.py new file mode 100644 index 0000000..a5d4da8 --- /dev/null +++ b/searchlightclient/tests/unit/osc/fakes.py @@ -0,0 +1,68 @@ +# Copyright 2013 Nebula Inc. +# +# 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 six +import sys + + +AUTH_TOKEN = "foobar" +AUTH_URL = "http://0.0.0.0" + + +class FakeStdout: + 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 FakeClientManager(object): + def __init__(self): + self.session = None + self.auth_ref = None + + +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) diff --git a/searchlightclient/tests/unit/osc/utils.py b/searchlightclient/tests/unit/osc/utils.py new file mode 100644 index 0000000..0c669bf --- /dev/null +++ b/searchlightclient/tests/unit/osc/utils.py @@ -0,0 +1,93 @@ +# Copyright 2012-2013 OpenStack Foundation +# Copyright 2013 Nebula Inc. +# +# 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 os + +import fixtures +import sys +import testtools + +from searchlightclient.tests.unit.osc import fakes + + +class TestCase(testtools.TestCase): + def setUp(self): + testtools.TestCase.setUp(self) + + if (os.environ.get("OS_STDOUT_CAPTURE") == "True" or + os.environ.get("OS_STDOUT_CAPTURE") == "1"): + stdout = self.useFixture(fixtures.StringStream("stdout")).stream + self.useFixture(fixtures.MonkeyPatch("sys.stdout", stdout)) + + if (os.environ.get("OS_STDERR_CAPTURE") == "True" or + os.environ.get("OS_STDERR_CAPTURE") == "1"): + stderr = self.useFixture(fixtures.StringStream("stderr")).stream + self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr)) + + def assertNotCalled(self, m, msg=None): + """Assert a function was not called""" + + if m.called: + if not msg: + msg = 'method %s should not have been called' % m + self.fail(msg) + + # 2.6 doesn't have the assert dict equals so make sure that it exists + if tuple(sys.version_info)[0:2] < (2, 7): + + def assertIsInstance(self, obj, cls, msg=None): + """self.assertTrue(isinstance(obj, cls)), with a nicer message""" + + if not isinstance(obj, cls): + standardMsg = '%s is not an instance of %r' % (obj, cls) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertDictEqual(self, d1, d2, msg=None): + # Simple version taken from 2.7 + self.assertIsInstance(d1, dict, + 'First argument is not a dictionary') + self.assertIsInstance(d2, dict, + 'Second argument is not a dictionary') + if d1 != d2: + if msg: + self.fail(msg) + else: + standardMsg = '%r != %r' % (d1, d2) + self.fail(standardMsg) + + +class TestCommand(TestCase): + """Test cliff command classes""" + + def setUp(self): + super(TestCommand, self).setUp() + # Build up a fake app + self.fake_stdout = fakes.FakeStdout() + self.app = fakes.FakeApp(self.fake_stdout) + self.app.client_manager = fakes.FakeClientManager() + + def check_parser(self, cmd, args, verify_args): + cmd_parser = cmd.get_parser('check_parser') + try: + parsed_args = cmd_parser.parse_args(args) + except SystemExit: + raise Exception("Argument parse failed") + for av in verify_args: + attr, value = av + if attr: + self.assertIn(attr, parsed_args) + self.assertEqual(getattr(parsed_args, attr), value) + return parsed_args diff --git a/searchlightclient/tests/unit/osc/v1/__init__.py b/searchlightclient/tests/unit/osc/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/searchlightclient/tests/unit/osc/v1/fakes.py b/searchlightclient/tests/unit/osc/v1/fakes.py new file mode 100644 index 0000000..78f68c5 --- /dev/null +++ b/searchlightclient/tests/unit/osc/v1/fakes.py @@ -0,0 +1,45 @@ +# Copyright 2014 OpenStack Foundation +# +# 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 searchlightclient.tests.unit.osc import fakes +from searchlightclient.tests.unit.osc import utils + + +ResourceType = { + "index": "searchlight", + "type": "OS::Nova::Server", + "name": "OS::Nova::Server" +} + + +class FakeSearchv1Client(object): + def __init__(self, **kwargs): + self.http_client = mock.Mock() + self.http_client.auth_token = kwargs['token'] + self.http_client.management_url = kwargs['endpoint'] + self.resource_types = mock.Mock() + self.resource_types.list = mock.Mock(return_value=[]) + + +class TestSearchv1(utils.TestCommand): + def setUp(self): + super(TestSearchv1, self).setUp() + + self.app.client_manager.search = FakeSearchv1Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/searchlightclient/tests/unit/osc/v1/test_resource_type.py b/searchlightclient/tests/unit/osc/v1/test_resource_type.py new file mode 100644 index 0000000..32df2fc --- /dev/null +++ b/searchlightclient/tests/unit/osc/v1/test_resource_type.py @@ -0,0 +1,49 @@ +# 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 copy + +from searchlightclient.osc.v1 import resource_type +from searchlightclient.tests.unit.osc import fakes +from searchlightclient.tests.unit.osc.v1 import fakes as searchlight_fakes + + +class TestResourceType(searchlight_fakes.TestSearchv1): + def setUp(self): + super(TestResourceType, self).setUp() + self.rtype_client = self.app.client_manager.search.resource_types + + +class TestResourceTypeList(TestResourceType): + + def setUp(self): + super(TestResourceTypeList, self).setUp() + self.cmd = resource_type.ListResourceType(self.app, None) + self.rtype_client.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(searchlight_fakes.ResourceType), + loaded=True, + ), + ] + + def test_list(self): + parsed_args = self.check_parser(self.cmd, [], []) + columns, data = self.cmd.take_action(parsed_args) + self.rtype_client.list.assert_called_with() + + collist = ('Name', 'Type') + self.assertEqual(collist, columns) + + datalist = (('OS::Nova::Server', 'OS::Nova::Server'),) + self.assertEqual(datalist, tuple(data)) diff --git a/setup.cfg b/setup.cfg index 9623194..b95891e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,13 @@ classifier = packages = searchlightclient +[entry_points] +openstack.cli.extension = + search = searchlightclient.osc.plugin + +openstack.search.v1 = + search_resource-type_list = searchlightclient.osc.v1.resource_type:ListResourceType + [global] setup-hooks = pbr.hooks.setup_hook