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