From 14fef5bf22ddd73e110eadcd64acd3c831508cf9 Mon Sep 17 00:00:00 2001 From: Noorul Islam K M Date: Fri, 7 Feb 2014 20:46:04 +0530 Subject: [PATCH] Connect CLI and python client As a first step implement assembly list command. Part of blueprint solum-minimal-cli Change-Id: I9ad43598542053ae8d1eb30b18105d9acc2b955c --- solumclient/client.py | 26 ++++++++ solumclient/common/cli_utils.py | 81 +++++++++++++++++++++++ solumclient/common/exc.py | 26 ++++++++ solumclient/solum.py | 16 +++-- solumclient/tests/test_solum.py | 110 ++++++++++++++++++++++++++++---- solumclient/v1/client.py | 2 + 6 files changed, 244 insertions(+), 17 deletions(-) create mode 100644 solumclient/common/exc.py diff --git a/solumclient/client.py b/solumclient/client.py index 1e45481..d12f3f0 100644 --- a/solumclient/client.py +++ b/solumclient/client.py @@ -32,3 +32,29 @@ def Client(version, **kwargs): solum_url=kwargs.get('solum_url')) http_client = client.HTTPClient(keystone_auth) return client_class(http_client) + + +def get_client(api_version, **kwargs): + """Get an authtenticated client, based on the credentials + in the keyword args. + + :param api_version: the API version to use + :param kwargs: keyword args containing credentials, either: + * os_auth_token: pre-existing token to re-use + * solum_url: solum API endpoint + or: + * os_username: name of user + * os_password: user's password + * os_auth_url: endpoint to authenticate against + * os_tenant_name: name of tenant + """ + cli_kwargs = { + 'username': kwargs.get('os_username'), + 'password': kwargs.get('os_password'), + 'tenant_name': kwargs.get('os_tenant_name'), + 'token': kwargs.get('os_auth_token'), + 'auth_url': kwargs.get('os_auth_url'), + 'solum_url': kwargs.get('solum_url') + } + + return Client(api_version, **cli_kwargs) diff --git a/solumclient/common/cli_utils.py b/solumclient/common/cli_utils.py index 15ccc6c..103e81b 100644 --- a/solumclient/common/cli_utils.py +++ b/solumclient/common/cli_utils.py @@ -13,6 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + +from solumclient import client as solum_client +from solumclient.common import exc + class CommandsBase(object): """Base command parsing class.""" @@ -25,6 +30,37 @@ class CommandsBase(object): self.parser.add_argument('action', default='help', help='Action to perform on resource') + + self.parser.add_argument('--os-username', + default=env('OS_USERNAME'), + help='Defaults to env[OS_USERNAME]') + + self.parser.add_argument('--os-password', + default=env('OS_PASSWORD'), + help='Defaults to env[OS_PASSWORD]') + + self.parser.add_argument('--os-tenant-name', + default=env('OS_TENANT_NAME'), + help='Defaults to env[OS_TENANT_NAME]') + + self.parser.add_argument('--os-auth-url', + default=env('OS_AUTH_URL'), + help='Defaults to env[OS_AUTH_URL]') + + self.parser.add_argument('--os-auth-token', + default=env('OS_AUTH_TOKEN'), + help='Defaults to env[OS_AUTH_TOKEN]') + + self.parser.add_argument('--solum-url', + default=env('SOLUM_URL'), + help='Defaults to env[SOLUM_URL]') + + self.parser.add_argument('--solum-api-version', + default=env( + 'SOLUM_API_VERSION', default='1'), + help='Defaults to env[SOLUM_API_VERSION] ' + 'or 1') + action = None try: @@ -34,6 +70,37 @@ class CommandsBase(object): # Parser has a habit of doing this when an arg is missing. self.parser.print_help() + client_args = parsed.__dict__ + + if not (parsed.os_auth_token and parsed.solum_url): + # Remove arguments that are not to be passed to the client in this + # case. + del client_args['os_auth_token'] + del client_args['solum_url'] + + if not parsed.os_username: + raise exc.CommandError("You must provide a username via " + "either --os-username or via " + "env[OS_USERNAME]") + + if not parsed.os_password: + raise exc.CommandError("You must provide a password via " + "either --os-password or via " + "env[OS_PASSWORD]") + + if not parsed.os_tenant_name: + raise exc.CommandError("You must provide a tenant_name via " + "either --os-tenant-name or via " + "env[OS_TENANT_NAME]") + + if not parsed.os_auth_url: + raise exc.CommandError("You must provide an auth url via " + "either --os-auth-url or via " + "env[OS_AUTH_URL]") + + self.client = solum_client.get_client(parsed.solum_api_version, + **client_args) + if action in self._actions: try: self.parser.error = self.parser.the_error @@ -41,6 +108,7 @@ class CommandsBase(object): except Exception: print(self._actions[action].__doc__) self.parser.print_help() + raise @property def _actions(self): @@ -79,3 +147,16 @@ def show_help(resources, name='targets or nouns'): if commands.__doc__: docstring = commands.__doc__ print("\t%-20s%s" % (resource, docstring)) + + +def env(*vars, **kwargs): + """Search for the first defined of possibly many env vars + + Returns the first environment variable defined in vars, or + returns the default defined in kwargs. + """ + for v in vars: + value = os.environ.get(v, None) + if value: + return value + return kwargs.get('default', '') diff --git a/solumclient/common/exc.py b/solumclient/common/exc.py new file mode 100644 index 0000000..e507eb6 --- /dev/null +++ b/solumclient/common/exc.py @@ -0,0 +1,26 @@ +# 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. + + +class BaseException(Exception): + """An error occurred.""" + def __init__(self, message=None): + self.message = message + + def __str__(self): + return self.message or self.__class__.__doc__ + + +class CommandError(BaseException): + """Invalid usage of CLI.""" diff --git a/solumclient/solum.py b/solumclient/solum.py index 05a1a37..f0bc82f 100644 --- a/solumclient/solum.py +++ b/solumclient/solum.py @@ -29,11 +29,15 @@ Notes: * Internationalization will not be added in M1 since this is a prototype """ +from __future__ import print_function + import argparse import sys -from solumclient.common import cli_utils +import six +from solumclient.common import cli_utils +from solumclient.openstack.common import strutils SOLUM_CLI_VER = "2014-01-30" @@ -98,8 +102,7 @@ class AssemblyCommands(cli_utils.CommandsBase): def list(self): """List all assemblies.""" - #TODO(noorul): Add REST communications - print("assembly list") + print(self.client.assemblies.list()) def main(): @@ -128,7 +131,12 @@ def main(): return se_except if resource in resources: - resources[resource](parser) + try: + resources[resource](parser) + except Exception as e: + print(strutils.safe_encode(six.text_type(e)), file=sys.stderr) + sys.exit(1) + else: cli_utils.show_help(resources) print("\n") diff --git a/solumclient/tests/test_solum.py b/solumclient/tests/test_solum.py index 10bc774..9f7a4a1 100644 --- a/solumclient/tests/test_solum.py +++ b/solumclient/tests/test_solum.py @@ -12,24 +12,108 @@ # License for the specific language governing permissions and limitations # under the License. -import argparse +import re +import sys +import fixtures +import mock +import six +from stevedore import extension +from testtools import matchers + +from solumclient.openstack.common.apiclient import auth from solumclient import solum from solumclient.tests import base +from solumclient.v1 import assembly + +FAKE_ENV = {'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_TENANT_NAME': 'tenant_name', + 'OS_AUTH_URL': 'http://no.where'} + + +class MockEntrypoint(object): + def __init__(self, name, plugin): + self.name = name + self.plugin = plugin + + +class BaseFakePlugin(auth.BaseAuthPlugin): + def _do_authenticate(self, http_client): + pass + + def token_and_endpoint(self, endpoint_type, service_type): + pass class TestSolum(base.TestCase): """Test the Solum CLI.""" - def test_application(self): - """Test the application code.""" - parser = argparse.ArgumentParser() - app_obj = solum.AppCommands(parser) - self.assertRaises(SystemExit, app_obj.create) - self.assertRaises(SystemExit, app_obj.delete) - def test_assembly(self): - """Test the assembly code.""" - parser = argparse.ArgumentParser() - assembly_obj = solum.AssemblyCommands(parser) - self.assertRaises(SystemExit, assembly_obj.create) - self.assertRaises(SystemExit, assembly_obj.delete) + re_options = re.DOTALL | re.MULTILINE + + # Patch os.environ to avoid required auth info. + def make_env(self, exclude=None): + env = dict((k, v) for k, v in FAKE_ENV.items() if k != exclude) + self.useFixture(fixtures.MonkeyPatch('os.environ', env)) + + @mock.patch.object(extension.ExtensionManager, "map") + def shell(self, argstr, mock_mgr_map): + class FakePlugin(BaseFakePlugin): + def authenticate(self, cls): + cls.request( + "POST", "http://auth/tokens", + json={"fake": "me"}, allow_redirects=True) + + mock_mgr_map.side_effect = ( + lambda func: func(MockEntrypoint("fake", FakePlugin))) + + orig = sys.stdout + try: + sys.stdout = six.StringIO() + argv = [__file__, ] + argv.extend(argstr.split()) + self.useFixture( + fixtures.MonkeyPatch('sys.argv', argv)) + solum.main() + except SystemExit: + exc_type, exc_value, exc_traceback = sys.exc_info() + self.assertEqual(exc_value.code, 0) + finally: + out = sys.stdout.getvalue() + sys.stdout.close() + sys.stdout = orig + + return out + + def test_help(self): + required = [ + '.*?^Solum Python Command Line Client', + '.*?^usage:' + '.*?^positional arguments' + '.*?^optional arguments' + + ] + for argstr in ['--help', 'help']: + help_text = self.shell(argstr) + for r in required: + self.assertThat(help_text, + matchers.MatchesRegex(r, + self.re_options)) + + @mock.patch.object(assembly.AssemblyManager, "list") + def test_assembly_list(self, mock_assembly_list): + self.make_env() + required = [ + '.*?^Solum Python Command Line Client', + '.*?\[\]' + ] + + mock_assembly_list.side_effect = ( + lambda: [] + ) + + out = self.shell("assembly list") + for r in required: + self.assertThat(out, + matchers.MatchesRegex(r, + self.re_options)) diff --git a/solumclient/v1/client.py b/solumclient/v1/client.py index a185344..24a8974 100644 --- a/solumclient/v1/client.py +++ b/solumclient/v1/client.py @@ -21,6 +21,8 @@ from solumclient.v1 import platform class Client(client.BaseClient): """Client for the Solum v1 API.""" + service_type = "application_deployment" + def __init__(self, http_client, extensions=None): """Initialize a new client for the Solum v1 API.""" super(Client, self).__init__(http_client, extensions)