You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by bh...@apache.org on 2012/10/31 18:50:13 UTC

[4/6] git commit: cli: cloudmonkey the command line interface

cli: cloudmonkey the command line interface

cloudmonkey
-----------
Apache CloudStack's very own monkey powered command line interface based on Marvin.
The neglected robot and monkey should rule the world!

Features:
- it's a shell and also a terminal tool
- scalable to find and run old and new APIs
- intuitive grammar and verbs
- autocompletion (functional hack)
- shell execution using ! or shell
- cfg support: user defined variables, like prompt, ruler, host, port etc.
- history
- colors
- dynamic API loading and rule generation
- leverages Marvin to get latest autogenerated APIs
- emacs like shortcuts on prompt
- uses apiKey and secretKey to interact with mgmt server
- logs all client commands
- PEP-8 compliant code

TODOs:
- Reverse search
- Fix input and output processing

Signed-off-by: Rohit Yadav <bh...@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/commit/2ceaa391
Tree: http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/tree/2ceaa391
Diff: http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/diff/2ceaa391

Branch: refs/heads/master
Commit: 2ceaa3911e792dbeb6c40dfb70961008a01f7e3c
Parents: ff9e609
Author: Rohit Yadav <bh...@apache.org>
Authored: Wed Oct 31 23:02:30 2012 +0530
Committer: Rohit Yadav <bh...@apache.org>
Committed: Wed Oct 31 23:19:14 2012 +0530

----------------------------------------------------------------------
 tools/cli/cloudmonkey/__init__.py    |   26 +++
 tools/cli/cloudmonkey/cloudmonkey.py |  323 +++++++++++++++++++++++++++++
 2 files changed, 349 insertions(+), 0 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/2ceaa391/tools/cli/cloudmonkey/__init__.py
----------------------------------------------------------------------
diff --git a/tools/cli/cloudmonkey/__init__.py b/tools/cli/cloudmonkey/__init__.py
new file mode 100644
index 0000000..e66b2b9
--- /dev/null
+++ b/tools/cli/cloudmonkey/__init__.py
@@ -0,0 +1,26 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+# Use following rules for versioning:
+# <cli major version>.<cloudstack minor version>.<cloudstack major version>
+# Example: For CloudStack 4.1.x, CLI version should be 0.1.4
+__version__ = "0.0.4"
+
+try:
+    from cloudmonkey import *
+except ImportError, e:
+    print e

http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/2ceaa391/tools/cli/cloudmonkey/cloudmonkey.py
----------------------------------------------------------------------
diff --git a/tools/cli/cloudmonkey/cloudmonkey.py b/tools/cli/cloudmonkey/cloudmonkey.py
new file mode 100644
index 0000000..9147d05
--- /dev/null
+++ b/tools/cli/cloudmonkey/cloudmonkey.py
@@ -0,0 +1,323 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+try:
+    import atexit
+    import cmd
+    import clint
+    import codecs
+    import logging
+    import os
+    import pdb
+    import readline
+    import rlcompleter
+    import sys
+    import types
+
+    from clint.textui import colored
+    from ConfigParser import ConfigParser, SafeConfigParser
+
+    from marvin.cloudstackConnection import cloudConnection
+    from marvin.cloudstackException import cloudstackAPIException
+    from marvin.cloudstackAPI import *
+    from marvin import cloudstackAPI
+except ImportError, e:
+    print "Import error in %s : %s" % (__name__, e)
+    import sys
+    sys.exit()
+
+log_fmt = '%(asctime)s - %(filename)s:%(lineno)s - [%(levelname)s] %(message)s'
+logger = logging.getLogger(__name__)
+completions = cloudstackAPI.__all__
+
+
+class CloudStackShell(cmd.Cmd):
+    intro = "☁ Apache CloudStack CLI. Type help or ? to list commands.\n"
+    ruler = "-"
+    config_file = os.path.expanduser('~/.cloudmonkey_config')
+
+    def __init__(self):
+        self.config_fields = {'host': 'localhost', 'port': '8080',
+                              'apiKey': '', 'secretKey': '',
+                              'prompt': '🙉 cloudmonkey> ', 'color': 'true',
+                              'log_file':
+                              os.path.expanduser('~/.cloudmonkey_log'),
+                              'history_file':
+                              os.path.expanduser('~/.cloudmonkey_history')}
+        if os.path.exists(self.config_file):
+            config = self.read_config()
+        else:
+            for key in self.config_fields.keys():
+                setattr(self, key, self.config_fields[key])
+            config = self.write_config()
+            print "Set your api and secret keys using the set command!"
+
+        for key in self.config_fields.keys():
+            setattr(self, key, config.get('CLI', key))
+
+        self.prompt += " "  # Cosmetic fix for prompt
+        logging.basicConfig(filename=self.log_file,
+                            level=logging.DEBUG, format=log_fmt)
+        self.logger = logging.getLogger(self.__class__.__name__)
+
+        cmd.Cmd.__init__(self)
+        # Update config if config_file does not exist
+        if not os.path.exists(self.config_file):
+            config = self.write_config()
+
+        # Fix autocompletion issue
+        if sys.platform == "darwin":
+            readline.parse_and_bind("bind ^I rl_complete")
+        else:
+            readline.parse_and_bind("tab: complete")
+
+        # Enable history support
+        try:
+            if os.path.exists(self.history_file):
+                readline.read_history_file(self.history_file)
+            atexit.register(readline.write_history_file, self.history_file)
+        except IOError:
+            print("Error: history support")
+
+    def read_config(self):
+        config = ConfigParser()
+        try:
+            with open(self.config_file, 'r') as cfg:
+                config.readfp(cfg)
+            for section in config.sections():
+                for option in config.options(section):
+                    logger.debug("[%s] %s=%s" % (section, option,
+                                                 config.get(section, option)))
+        except IOError, e:
+            self.print_shell("Error: config_file not found", e)
+        return config
+
+    def write_config(self):
+        config = ConfigParser()
+        config.add_section('CLI')
+        for key in self.config_fields.keys():
+            config.set('CLI', key, getattr(self, key))
+        with open(self.config_file, 'w') as cfg:
+            config.write(cfg)
+        return config
+
+    def emptyline(self):
+        pass
+
+    def print_shell(self, *args):
+        try:
+            for arg in args:
+                if isinstance(type(args), types.NoneType):
+                    continue
+                if self.color == 'true':
+                    if str(arg).count(self.ruler) == len(str(arg)):
+                        print colored.green(arg),
+                    elif 'type' in arg:
+                        print colored.green(arg),
+                    elif 'state' in arg:
+                        print colored.yellow(arg),
+                    elif 'id =' in arg:
+                        print colored.cyan(arg),
+                    elif 'name =' in arg:
+                        print colored.magenta(arg),
+                    elif ':' in arg:
+                        print colored.blue(arg),
+                    elif 'Error' in arg:
+                        print colored.red(arg),
+                    else:
+                        print arg,
+                else:
+                    print arg,
+            print
+        except Exception, e:
+            print colored.red("Error: "), e
+
+    # FIXME: Fix result processing and printing
+    def print_result(self, result, response, api_mod):
+        def print_result_as_list():
+            if result is None:
+                return
+            for node in result:
+                print_result_as_instance(node)
+
+        def print_result_as_instance(node):
+            for attribute in dir(response):
+                if "__" not in attribute:
+                    attribute_value = getattr(node, attribute)
+                    if isinstance(attribute_value, list):
+                        self.print_shell("\n%s:" % attribute)
+                        try:
+                            self.print_result(attribute_value,
+                                              getattr(api_mod, attribute)(),
+                                              api_mod)
+                        except AttributeError, e:
+                            pass
+                    elif attribute_value is not None:
+                        self.print_shell("%s = %s" %
+                                         (attribute, attribute_value))
+            self.print_shell(self.ruler * 80)
+
+        if result is None:
+            return
+
+        if type(result) is types.InstanceType:
+            print_result_as_instance(result)
+        elif isinstance(result, list):
+            print_result_as_list()
+        elif isinstance(result, str):
+            print result
+        elif isinstance(type(result), types.NoneType):
+            print_result_as_instance(result)
+        elif not (str(result) is None):
+            self.print_shell(result)
+
+    def do_quit(self, s):
+        """
+        Quit Apache CloudStack CLI
+        """
+        self.print_shell("Bye!")
+        return True
+
+    def do_shell(self, args):
+        """
+        Execute shell commands using shell <command> or !<command>
+        Example: !ls or shell ls
+        """
+        os.system(args)
+
+    def make_request(self, command, requests={}):
+        conn = cloudConnection(self.host, port=int(self.port),
+                               apiKey=self.apiKey, securityKey=self.secretKey,
+                               logging=logging.getLogger("cloudConnection"))
+        try:
+            response = conn.make_request(command, requests)
+        except cloudstackAPIException, e:
+            self.print_shell("API Error", e)
+            return None
+        return response
+
+    def default(self, args):
+        args = args.split(" ")
+        api_name = args[0]
+
+        try:
+            api_cmd_str = "%sCmd" % api_name
+            api_rsp_str = "%sResponse" % api_name
+            api_mod = __import__("marvin.cloudstackAPI.%s" % api_name,
+                                 globals(), locals(), [api_cmd_str], -1)
+            api_cmd = getattr(api_mod, api_cmd_str)
+            api_rsp = getattr(api_mod, api_rsp_str)
+        except ImportError, e:
+            self.print_shell("Error: API %s not found!" % e)
+            return
+
+        command = api_cmd()
+        response = api_rsp()
+        #FIXME: Parsing logic
+        args_dict = dict(map(lambda x: x.split("="),
+                             args[1:])[x] for x in range(len(args) - 1))
+
+        for attribute in dir(command):
+            if attribute in args_dict:
+                setattr(command, attribute, args_dict[attribute])
+
+        result = self.make_request(command, response)
+        try:
+            self.print_result(result, response, api_mod)
+        except Exception as e:
+            self.print_shell("🙈  Error on parsing and printing", e)
+
+    def completedefault(self, text, line, begidx, endidx):
+        mline = line.partition(" ")[2]
+        offs = len(mline) - len(text)
+        return [s[offs:] for s in completions if s.startswith(mline)]
+
+    def do_api(self, args):
+        """
+        Make raw api calls. Syntax: api <apiName> <args>=<values>. Example:
+        api listAccount listall=true
+        """
+        if len(args) > 0:
+            return self.default(args)
+        else:
+            self.print_shell("Please use a valid syntax")
+
+    def complete_api(self, text, line, begidx, endidx):
+        return self.completedefault(text, line, begidx, endidx)
+
+    def do_set(self, args):
+        """
+        Set config for CloudStack CLI. Available options are:
+        host, port, apiKey, secretKey, log_file, history_file
+        """
+        args = args.split(' ')
+        if len(args) == 2:
+            key, value = args
+            # Note: keys and fields should have same names
+            setattr(self, key, value)
+            self.write_config()
+        else:
+            self.print_shell("Please use the syntax: set valid-key value")
+
+    def complete_set(self, text, line, begidx, endidx):
+        mline = line.partition(" ")[2]
+        offs = len(mline) - len(text)
+        return [s[offs:] for s in
+               ['host', 'port', 'apiKey', 'secretKey', 'prompt', 'color',
+                'log_file', 'history_file'] if s.startswith(mline)]
+
+
+def main():
+    grammar = ['list',  'create', 'delete', 'update', 'disable', 'enable',
+               'add', 'remove']
+    self = CloudStackShell
+    for rule in grammar:
+        setattr(self, 'completions_' + rule, map(lambda x: x.replace(rule, ''),
+                                                 filter(lambda x: rule in x,
+                                                        completions)))
+
+        def add_grammar(rule):
+            def grammar_closure(self, args):
+                self.default(rule + args)
+            return grammar_closure
+
+        grammar_handler = add_grammar(rule)
+        grammar_handler.__doc__ = "%ss resources" % rule.capitalize()
+        grammar_handler.__name__ = 'do_' + rule
+        setattr(self, grammar_handler.__name__, grammar_handler)
+
+        def add_completer(rule):
+            def completer_closure(self, text, line, begidx, endidx):
+                mline = line.partition(" ")[2]
+                offs = len(mline) - len(text)
+                return [s[offs:] for s in getattr(self, 'completions_' + rule)
+                        if s.startswith(mline)]
+            return completer_closure
+
+        completion_handler = add_completer(rule)
+        completion_handler.__name__ = 'complete_' + rule
+        setattr(self, completion_handler.__name__, completion_handler)
+
+    if len(sys.argv) > 1:
+        CloudStackShell().onecmd(' '.join(sys.argv[1:]))
+    else:
+        CloudStackShell().cmdloop()
+
+if __name__ == "__main__":
+    main()