mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
Added 1. display = [default|json|tabularize] has been added in the config to replace tabularize = [true|false] 2. tabularize is deprecated but we will still set it as "false" once the user removes it out of their config to avoid throwing an error. This will be removed in the next major version. 3. display = "default" is added to the [ui] section of the config if it is not present. 4. You can now output JSON formatted text by setting the config display = json 5. You can now filter text in JSON output mode. (i.e. list users account=grudzien filter=account,id,email). Filtered output returns a properly formatted JSON document. Removed 1. Removed the printing of attr keys in read_config(). Deprecated 1. tabularize = [true|false] is now messaged as deprecated. Signed-off-by: Justin Grudzien <grudzien@gmail.com>
543 lines
20 KiB
Python
543 lines
20 KiB
Python
#!/usr/bin/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 json
|
|
import logging
|
|
import os
|
|
import pdb
|
|
import shlex
|
|
import sys
|
|
import types
|
|
import copy
|
|
|
|
from cachemaker import loadcache, savecache, monkeycache, splitverbsubject
|
|
from config import __version__, __description__, __projecturl__
|
|
from config import read_config, write_config, config_file
|
|
from optparse import OptionParser
|
|
from prettytable import PrettyTable
|
|
from printer import monkeyprint
|
|
from requester import monkeyrequest
|
|
except ImportError, e:
|
|
print("Import error in %s : %s" % (__name__, e))
|
|
import sys
|
|
sys.exit()
|
|
|
|
try:
|
|
from precache import apicache
|
|
except ImportError:
|
|
apicache = {'count': 0, 'verbs': [], 'asyncapis': []}
|
|
|
|
try:
|
|
import readline
|
|
except ImportError, e:
|
|
print("Module readline not found, autocompletions will fail", e)
|
|
else:
|
|
import rlcompleter
|
|
if 'libedit' in readline.__doc__:
|
|
readline.parse_and_bind("bind ^I rl_complete")
|
|
else:
|
|
readline.parse_and_bind("tab: complete")
|
|
|
|
log_fmt = '%(asctime)s - %(filename)s:%(lineno)s - [%(levelname)s] %(message)s'
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CloudMonkeyShell(cmd.Cmd, object):
|
|
intro = ("☁ Apache CloudStack 🐵 cloudmonkey " + __version__ +
|
|
". Type help or ? to list commands.\n")
|
|
ruler = "="
|
|
config_options = []
|
|
verbs = []
|
|
|
|
def __init__(self, pname, cfile):
|
|
self.program_name = pname
|
|
self.config_file = cfile
|
|
self.config_options = read_config(self.get_attr, self.set_attr,
|
|
self.config_file)
|
|
self.loadcache()
|
|
self.prompt = self.prompt.strip() + " " # Cosmetic fix for prompt
|
|
|
|
logging.basicConfig(filename=self.log_file,
|
|
level=logging.DEBUG, format=log_fmt)
|
|
logger.debug("Loaded config fields:\n%s" % map(lambda x: "%s=%s" %
|
|
(x, getattr(self, x)),
|
|
self.config_options))
|
|
cmd.Cmd.__init__(self)
|
|
|
|
try:
|
|
if os.path.exists(self.history_file):
|
|
readline.read_history_file(self.history_file)
|
|
except IOError, e:
|
|
logger.debug("Error: Unable to read history. " + str(e))
|
|
atexit.register(readline.write_history_file, self.history_file)
|
|
|
|
def get_attr(self, field):
|
|
return getattr(self, field)
|
|
|
|
def set_attr(self, field, value):
|
|
return setattr(self, field, value)
|
|
|
|
def emptyline(self):
|
|
pass
|
|
|
|
def cmdloop(self, intro=None):
|
|
print(self.intro)
|
|
while True:
|
|
try:
|
|
super(CloudMonkeyShell, self).cmdloop(intro="")
|
|
self.postloop()
|
|
except KeyboardInterrupt:
|
|
print("^C")
|
|
|
|
def loadcache(self):
|
|
if os.path.exists(self.cache_file):
|
|
self.apicache = loadcache(self.cache_file)
|
|
else:
|
|
self.apicache = apicache
|
|
if 'verbs' in self.apicache:
|
|
self.verbs = self.apicache['verbs']
|
|
|
|
for verb in self.verbs:
|
|
def add_grammar(verb):
|
|
def grammar_closure(self, args):
|
|
if self.pipe_runner("%s %s" % (verb, args)):
|
|
return
|
|
if ' --help' in args or ' -h' in args:
|
|
self.do_help("%s %s" % (verb, args))
|
|
return
|
|
try:
|
|
args_partition = args.partition(" ")
|
|
cmd = self.apicache[verb][args_partition[0]]['name']
|
|
args = args_partition[2]
|
|
except KeyError, e:
|
|
self.monkeyprint("Error: invalid %s api arg" % verb, e)
|
|
return
|
|
self.default("%s %s" % (cmd, args))
|
|
return grammar_closure
|
|
|
|
grammar_handler = add_grammar(verb)
|
|
grammar_handler.__doc__ = "%ss resources" % verb.capitalize()
|
|
grammar_handler.__name__ = 'do_' + str(verb)
|
|
setattr(self.__class__, grammar_handler.__name__, grammar_handler)
|
|
|
|
def monkeyprint(self, *args):
|
|
output = ""
|
|
try:
|
|
for arg in args:
|
|
if isinstance(type(arg), types.NoneType):
|
|
continue
|
|
output += str(arg)
|
|
except Exception, e:
|
|
print(e)
|
|
|
|
if self.color == 'true':
|
|
monkeyprint(output)
|
|
else:
|
|
print(output)
|
|
|
|
def print_result(self, result, result_filter=None):
|
|
if result is None or len(result) == 0:
|
|
return
|
|
|
|
def printer_helper(printer, toprow):
|
|
if printer:
|
|
self.monkeyprint(printer)
|
|
return PrettyTable(toprow)
|
|
|
|
# method: print_result_json( result, result_filter )
|
|
# parameters: result - raw results from the API call
|
|
# result_filter - filterset
|
|
# description: prints result as a json object
|
|
def print_result_json(result, result_filter=None):
|
|
tfilter = {} # temp var to hold a dict of the filters
|
|
tresult = copy.deepcopy(result) # dupe the result to filter
|
|
if result_filter != None:
|
|
for res in result_filter:
|
|
tfilter[ res ] = 1
|
|
myresults = {}
|
|
for okey, oval in result.iteritems():
|
|
if isinstance( oval, dict ):
|
|
for tkey in x:
|
|
if tkey not in tfilter:
|
|
try:
|
|
del( tresult[okey][x][tkey] )
|
|
except:
|
|
pass
|
|
elif isinstance( oval, list ):
|
|
for x in range( len( oval ) ):
|
|
if isinstance( oval[x], dict ):
|
|
for tkey in oval[x]:
|
|
if tkey not in tfilter:
|
|
try:
|
|
del( tresult[okey][x][tkey] )
|
|
except:
|
|
pass
|
|
else:
|
|
try:
|
|
del( tresult[ okey ][ x ] )
|
|
except:
|
|
pass
|
|
print json.dumps(tresult,
|
|
sort_keys=True,
|
|
indent=2,
|
|
separators=(',', ': '))
|
|
|
|
def print_result_tabular(result, result_filter=None):
|
|
toprow = None
|
|
printer = None
|
|
for node in result:
|
|
if toprow != node.keys():
|
|
if result_filter is not None and len(result_filter) != 0:
|
|
commonkeys = filter(lambda x: x in node.keys(),
|
|
result_filter)
|
|
if commonkeys != toprow:
|
|
toprow = commonkeys
|
|
printer = printer_helper(printer, toprow)
|
|
else:
|
|
toprow = node.keys()
|
|
printer = printer_helper(printer, toprow)
|
|
row = map(lambda x: node[x], toprow)
|
|
if printer and row:
|
|
printer.add_row(row)
|
|
if printer:
|
|
self.monkeyprint(printer)
|
|
|
|
def print_result_as_dict(result, result_filter=None):
|
|
|
|
# tabularize overrides self.display
|
|
if self.display == "json" and not self.tabularize == "true":
|
|
print_result_json(result, result_filter)
|
|
return
|
|
|
|
for key in sorted(result.keys(), key=lambda x:
|
|
x not in ['id', 'count', 'name'] and x):
|
|
if not (isinstance(result[key], list) or
|
|
isinstance(result[key], dict)):
|
|
self.monkeyprint("%s = %s" % (key, result[key]))
|
|
else:
|
|
self.monkeyprint(key + ":")
|
|
self.print_result(result[key], result_filter)
|
|
|
|
def print_result_as_list(result, result_filter=None):
|
|
for node in result:
|
|
# Tabular print if it's a list of dict and tabularize is true
|
|
if isinstance(node, dict) and (self.display == 'tabularize' or self.tabularize == 'true'):
|
|
print_result_tabular(result, result_filter)
|
|
break
|
|
self.print_result(node)
|
|
if len(result) > 1:
|
|
self.monkeyprint(self.ruler * 80)
|
|
|
|
if isinstance(result, dict):
|
|
print_result_as_dict(result, result_filter)
|
|
elif isinstance(result, list):
|
|
print_result_as_list(result, result_filter)
|
|
elif isinstance(result, str):
|
|
print result
|
|
elif not (str(result) is None):
|
|
self.monkeyprint(result)
|
|
|
|
def make_request(self, command, args={}, isasync=False):
|
|
response, error = monkeyrequest(command, args, isasync,
|
|
self.asyncblock, logger,
|
|
self.host, self.port,
|
|
self.apikey, self.secretkey,
|
|
self.timeout, self.protocol, self.path)
|
|
if error is not None:
|
|
self.monkeyprint(error)
|
|
return response
|
|
|
|
def default(self, args):
|
|
if self.pipe_runner(args):
|
|
return
|
|
|
|
apiname = args.partition(' ')[0]
|
|
verb, subject = splitverbsubject(apiname)
|
|
|
|
lexp = shlex.shlex(args.strip())
|
|
lexp.whitespace = " "
|
|
lexp.whitespace_split = True
|
|
lexp.posix = True
|
|
args = []
|
|
while True:
|
|
next_val = lexp.next()
|
|
if next_val is None:
|
|
break
|
|
args.append(next_val.replace('\x00', ''))
|
|
|
|
args_dict = dict(map(lambda x: [x.partition("=")[0],
|
|
x.partition("=")[2]],
|
|
args[1:])[x] for x in range(len(args) - 1))
|
|
field_filter = None
|
|
if 'filter' in args_dict:
|
|
field_filter = filter(lambda x: x is not '',
|
|
map(lambda x: x.strip(),
|
|
args_dict.pop('filter').split(',')))
|
|
|
|
missing = []
|
|
if verb in self.apicache and subject in self.apicache[verb]:
|
|
missing = filter(lambda x: x not in args_dict.keys(),
|
|
self.apicache[verb][subject]['requiredparams'])
|
|
|
|
if len(missing) > 0:
|
|
self.monkeyprint("Missing arguments: ", ' '.join(missing))
|
|
return
|
|
|
|
isasync = False
|
|
if 'asyncapis' in self.apicache:
|
|
isasync = apiname in self.apicache['asyncapis']
|
|
|
|
result = self.make_request(apiname, args_dict, isasync)
|
|
|
|
if result is None:
|
|
return
|
|
try:
|
|
responsekeys = filter(lambda x: 'response' in x, result.keys())
|
|
for responsekey in responsekeys:
|
|
self.print_result(result[responsekey], field_filter)
|
|
print
|
|
except Exception as e:
|
|
self.monkeyprint("🙈 Error on parsing and printing", e)
|
|
|
|
def completedefault(self, text, line, begidx, endidx):
|
|
partitions = line.partition(" ")
|
|
verb = partitions[0].strip()
|
|
rline = partitions[2].lstrip().partition(" ")
|
|
subject = rline[0]
|
|
separator = rline[1]
|
|
params = rline[2].lstrip()
|
|
|
|
if verb not in self.verbs:
|
|
return []
|
|
|
|
autocompletions = []
|
|
search_string = ""
|
|
|
|
if separator != " ": # Complete verb subjects
|
|
autocompletions = self.apicache[verb].keys()
|
|
search_string = subject
|
|
else: # Complete subject params
|
|
autocompletions = map(lambda x: x + "=",
|
|
map(lambda x: x['name'],
|
|
self.apicache[verb][subject]['params']))
|
|
search_string = text
|
|
if self.paramcompletion == 'true':
|
|
param = line.split(" ")[-1]
|
|
idx = param.find("=")
|
|
value = param[idx + 1:]
|
|
param = param[:idx]
|
|
if len(value) < 36 and idx != -1:
|
|
params = self.apicache[verb][subject]['params']
|
|
related = filter(lambda x: x['name'] == param,
|
|
params)[0]['related']
|
|
api = min(filter(lambda x: 'list' in x, related), key=len)
|
|
response = self.make_request(api, args={'listall': 'true'})
|
|
responsekey = filter(lambda x: 'response' in x,
|
|
response.keys())[0]
|
|
result = response[responsekey]
|
|
uuids = []
|
|
for key in result.keys():
|
|
if isinstance(result[key], list):
|
|
for element in result[key]:
|
|
if 'id' in element.keys():
|
|
uuids.append(element['id'])
|
|
autocompletions = uuids
|
|
search_string = value
|
|
|
|
if (self.display == "tabularize" or self.display == "json" or self.tabularize == "true") and subject != "":
|
|
autocompletions.append("filter=")
|
|
return [s for s in autocompletions if s.startswith(search_string)]
|
|
|
|
def do_sync(self, args):
|
|
"""
|
|
Asks cloudmonkey to discovery and sync apis available on user specified
|
|
CloudStack host server which has the API discovery plugin, on failure
|
|
it rollbacks last datastore or api precached datastore.
|
|
"""
|
|
response = self.make_request("listApis")
|
|
if response is None:
|
|
monkeyprint("Failed to sync apis, please check your config?")
|
|
monkeyprint("Note: `sync` requires api discovery service enabled" +
|
|
" on the CloudStack management server")
|
|
return
|
|
self.apicache = monkeycache(response)
|
|
savecache(self.apicache, self.cache_file)
|
|
monkeyprint("%s APIs discovered and cached" % self.apicache["count"])
|
|
self.loadcache()
|
|
|
|
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.monkeyprint("Please use a valid syntax")
|
|
|
|
def do_set(self, args):
|
|
"""
|
|
Set config for cloudmonkey. For example, options can be:
|
|
host, port, apikey, secretkey, log_file, history_file
|
|
You may also edit your ~/.cloudmonkey_config instead of using set.
|
|
|
|
Example:
|
|
set host 192.168.56.2
|
|
set prompt 🐵 cloudmonkey>
|
|
set log_file /var/log/cloudmonkey.log
|
|
"""
|
|
args = args.strip().partition(" ")
|
|
key, value = (args[0], args[2])
|
|
setattr(self, key, value) # keys and attributes should have same names
|
|
self.prompt = self.prompt.strip() + " " # prompt fix
|
|
write_config(self.get_attr, self.config_file)
|
|
|
|
def complete_set(self, text, line, begidx, endidx):
|
|
mline = line.partition(" ")[2]
|
|
offs = len(mline) - len(text)
|
|
return [s[offs:] for s in self.config_options
|
|
if s.startswith(mline)]
|
|
|
|
def pipe_runner(self, args):
|
|
if args.find(' |') > -1:
|
|
pname = self.program_name
|
|
if '.py' in pname:
|
|
pname = "python " + pname
|
|
self.do_shell("%s %s" % (pname, args))
|
|
return True
|
|
return False
|
|
|
|
def do_shell(self, args):
|
|
"""
|
|
Execute shell commands using shell <command> or !<command>
|
|
|
|
Example:
|
|
!ls
|
|
shell ls
|
|
!for((i=0; i<10; i++)); do cloudmonkey create user account=admin \
|
|
email=test@test.tt firstname=user$i lastname=user$i \
|
|
password=password username=user$i; done
|
|
"""
|
|
os.system(args)
|
|
|
|
def do_help(self, args):
|
|
"""
|
|
Show help docs for various topics
|
|
|
|
Example:
|
|
help list
|
|
help list users
|
|
?list
|
|
?list users
|
|
"""
|
|
fields = args.partition(" ")
|
|
if fields[2] == "":
|
|
cmd.Cmd.do_help(self, args)
|
|
else:
|
|
verb = fields[0]
|
|
subject = fields[2].partition(" ")[0]
|
|
if subject in self.apicache[verb]:
|
|
api = self.apicache[verb][subject]
|
|
helpdoc = "(%s) %s" % (api['name'], api['description'])
|
|
if api['isasync']:
|
|
helpdoc += "\nThis API is asynchronous."
|
|
required = api['requiredparams']
|
|
if len(required) > 0:
|
|
helpdoc += "\nRequired params are %s" % ' '.join(required)
|
|
helpdoc += "\nParameters\n" + "=" * 10
|
|
for param in api['params']:
|
|
helpdoc += "\n%s = (%s) %s" % (param['name'],
|
|
param['type'], param['description'])
|
|
self.monkeyprint(helpdoc)
|
|
else:
|
|
self.monkeyprint("Error: no such api (%s) on %s" %
|
|
(subject, verb))
|
|
|
|
def complete_help(self, text, line, begidx, endidx):
|
|
fields = line.partition(" ")
|
|
subfields = fields[2].partition(" ")
|
|
|
|
if subfields[1] != " ":
|
|
return cmd.Cmd.complete_help(self, text, line, begidx, endidx)
|
|
else:
|
|
line = fields[2]
|
|
text = subfields[2]
|
|
return self.completedefault(text, line, begidx, endidx)
|
|
|
|
def do_EOF(self, args):
|
|
"""
|
|
Quit on Ctrl+d or EOF
|
|
"""
|
|
sys.exit()
|
|
|
|
def do_exit(self, args):
|
|
"""
|
|
Quit CloudMonkey CLI
|
|
"""
|
|
return self.do_quit(args)
|
|
|
|
def do_quit(self, args):
|
|
"""
|
|
Quit CloudMonkey CLI
|
|
"""
|
|
self.monkeyprint("Bye!")
|
|
return self.do_EOF(args)
|
|
|
|
class MonkeyParser(OptionParser):
|
|
def format_help(self, formatter=None):
|
|
if formatter is None:
|
|
formatter = self.formatter
|
|
result = []
|
|
if self.usage:
|
|
result.append("Usage: cloudmonkey [options] [cmds] [params]\n\n")
|
|
if self.description:
|
|
result.append(self.format_description(formatter) + "\n")
|
|
result.append(self.format_option_help(formatter))
|
|
result.append("\nTry cloudmonkey [help|?]\n")
|
|
return "".join(result)
|
|
|
|
def main():
|
|
parser = MonkeyParser()
|
|
parser.add_option("-c", "--config-file",
|
|
dest="cfile", default=config_file,
|
|
help="config file for cloudmonkey", metavar="FILE")
|
|
parser.add_option("-v", "--version",
|
|
action="store_true", dest="version", default=False,
|
|
help="prints cloudmonkey version information")
|
|
|
|
(options, args) = parser.parse_args()
|
|
if options.version:
|
|
print "cloudmonkey", __version__
|
|
print __description__, "(%s)" % __projecturl__
|
|
sys.exit(0)
|
|
|
|
shell = CloudMonkeyShell(sys.argv[0], options.cfile)
|
|
if len(args) > 0:
|
|
shell.onecmd(' '.join(args))
|
|
else:
|
|
shell.cmdloop()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|