Connect CLI and python client

As a first step implement assembly list command.

Part of blueprint solum-minimal-cli

Change-Id: I9ad43598542053ae8d1eb30b18105d9acc2b955c
This commit is contained in:
Noorul Islam K M
2014-02-07 20:46:04 +05:30
committed by Gerrit Code Review
parent 9e06ecebd1
commit 14fef5bf22
6 changed files with 244 additions and 17 deletions

View File

@@ -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)

View File

@@ -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', '')

26
solumclient/common/exc.py Normal file
View File

@@ -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."""

View File

@@ -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")

View File

@@ -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))

View File

@@ -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)