From 2ceaa3911e792dbeb6c40dfb70961008a01f7e3c Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Wed, 31 Oct 2012 23:02:30 +0530 Subject: [PATCH] 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 --- tools/cli/cloudmonkey/__init__.py | 26 +++ tools/cli/cloudmonkey/cloudmonkey.py | 323 +++++++++++++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 tools/cli/cloudmonkey/__init__.py create mode 100644 tools/cli/cloudmonkey/cloudmonkey.py diff --git a/tools/cli/cloudmonkey/__init__.py b/tools/cli/cloudmonkey/__init__.py new file mode 100644 index 00000000000..e66b2b9e6b0 --- /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: +# .. +# 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 diff --git a/tools/cli/cloudmonkey/cloudmonkey.py b/tools/cli/cloudmonkey/cloudmonkey.py new file mode 100644 index 00000000000..9147d053ac1 --- /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 or ! + 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 =. 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()