From 2ba7dceaef84dec4cbeb4eece275c0baed96e938 Mon Sep 17 00:00:00 2001 From: Paul Montgomery Date: Thu, 30 Jan 2014 21:53:19 +0000 Subject: [PATCH] Solum M1 argparse-based Python CLI * Implements text parsing only, no REST communications * Implements Application create, delete and list * Implements Assembly create, delete and list * Includes some very basic unit tests (expand when REST is added) Partially implements: blueprint solum-minimal-cli Change-Id: I0b1431ffb84b0c6ee71640483669bf55401dc81b --- solumclient/common/cli_utils.py | 81 +++++++++++++++++++ solumclient/solum.py | 138 ++++++++++++++++++++++++++++++++ solumclient/tests/test_solum.py | 35 ++++++++ 3 files changed, 254 insertions(+) create mode 100644 solumclient/common/cli_utils.py create mode 100644 solumclient/solum.py create mode 100644 solumclient/tests/test_solum.py diff --git a/solumclient/common/cli_utils.py b/solumclient/common/cli_utils.py new file mode 100644 index 0000000..15ccc6c --- /dev/null +++ b/solumclient/common/cli_utils.py @@ -0,0 +1,81 @@ +# Copyright (c) 2014 Rackspace +# +# 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 CommandsBase(object): + """Base command parsing class.""" + parser = None + solum = None + + def __init__(self, parser): + self.parser = parser + self._get_global_flags() + self.parser.add_argument('action', + default='help', + help='Action to perform on resource') + action = None + + try: + parsed, _ = parser.parse_known_args() + action = parsed.action + except Exception: + # Parser has a habit of doing this when an arg is missing. + self.parser.print_help() + + if action in self._actions: + try: + self.parser.error = self.parser.the_error + self._actions[action]() + except Exception: + print(self._actions[action].__doc__) + self.parser.print_help() + + @property + def _actions(self): + """Action handler""" + return dict((attr, getattr(self, attr)) + for attr in dir(self) + if not attr.startswith('_') + and callable(getattr(self, attr))) + + def _get_global_flags(self): + """Get global flags.""" + # Good location to add_argument() global options like --verbose + pass + + def help(self): + """Print this help message.""" + print(self.__doc__) + show_help(self._actions, 'actions') + + +def show_help(resources, name='targets or nouns'): + """Help screen.""" + print("Full list of commands:") + print(" app create [--repo=repo_url] [--build=no] plan_name") + print(" app delete plan_name") + print(" app list") + print(" assembly create [--assembly=assembly_name] plan_name") + print(" assembly delete assembly_name") + print(" assembly list") + print("\n") + + print("Available %s:" % name) + for resource in sorted(resources): + commands = resources.get(resource) + docstring = "<%s %s>" % (name.capitalize(), resource) + if commands.__doc__: + docstring = commands.__doc__ + print("\t%-20s%s" % (resource, docstring)) diff --git a/solumclient/solum.py b/solumclient/solum.py new file mode 100644 index 0000000..05a1a37 --- /dev/null +++ b/solumclient/solum.py @@ -0,0 +1,138 @@ +# Copyright (c) 2014 Rackspace +# +# 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. + +""" +Initial M1 Solum CLI commands implemented (but not REST communications): +* app create --repo="repo_url" [--build=no] plan_name +* app delete plan_name +* app list +* assembly create [--assembly="assembly_name"] plan_name +* assembly delete assembly_name +* assembly list + +Notes: +* This code is expected to be replaced by the OpenStack Client (OSC) when + it has progressed a little bit farther as described at: + https://wiki.openstack.org/wiki/Solum/CLI +* Internationalization will not be added in M1 since this is a prototype +""" + +import argparse +import sys + +from solumclient.common import cli_utils + + +SOLUM_CLI_VER = "2014-01-30" + + +class AppCommands(cli_utils.CommandsBase): + """Application targets.""" + + def create(self): + """Create an application.""" + self.parser.add_argument('plan_name', + help="Tenant/project-wide unique plan name") + self.parser.add_argument('--repo', + help="Code repository URL") + self.parser.add_argument('--build', + default='yes', + help="Build flag") + args = self.parser.parse_args() + #TODO(noorul): Add REST communications + print("app create plan_name=%s repo=%s build=%s" % ( + args.plan_name, + args.repo, + args.build)) + + def delete(self): + """Delete an application.""" + self.parser.add_argument('plan_name', + help="Tenant/project-wide unique plan name") + args = self.parser.parse_args() + #TODO(noorul): Add REST communications + print("app delete plan_name=%s" % ( + args.plan_name)) + + def list(self): + """List all applications.""" + #TODO(noorul): Add REST communications + print("app list") + + +class AssemblyCommands(cli_utils.CommandsBase): + """Assembly targets.""" + + def create(self): + """Create an assembly.""" + self.parser.add_argument('plan_name', + help="Tenant/project-wide unique plan name") + self.parser.add_argument('--assembly', + help="Assembly name") + args = self.parser.parse_args() + #TODO(noorul): Add REST communications + print("assembly create plan_name=%s assembly=%s" % ( + args.plan_name, + args.assembly)) + + def delete(self): + """Delete an assembly.""" + self.parser.add_argument('assembly_name', + help="Assembly name") + args = self.parser.parse_args() + #TODO(noorul): Add REST communications + print("assembly delete assembly_name=%s" % ( + args.assembly_name)) + + def list(self): + """List all assemblies.""" + #TODO(noorul): Add REST communications + print("assembly list") + + +def main(): + """Basically the entry point.""" + print("Solum Python Command Line Client %s\n" % SOLUM_CLI_VER) + parser = argparse.ArgumentParser(conflict_handler='resolve') + parser.the_error = parser.error + parser.error = lambda m: None + + resources = { + 'app': AppCommands, + 'assembly': AssemblyCommands, + } + + choices = resources.keys() + + parser.add_argument('resource', choices=choices, + help="Target noun to act upon") + + resource = None + try: + parsed, _ = parser.parse_known_args() + resource = parsed.resource + except Exception as se_except: + parser.print_help() + return se_except + + if resource in resources: + resources[resource](parser) + else: + cli_utils.show_help(resources) + print("\n") + parser.print_help() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/solumclient/tests/test_solum.py b/solumclient/tests/test_solum.py new file mode 100644 index 0000000..10bc774 --- /dev/null +++ b/solumclient/tests/test_solum.py @@ -0,0 +1,35 @@ +# 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 argparse + +from solumclient import solum +from solumclient.tests import base + + +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)