#!/usr/bin/env python #coding=utf8 ''' CloudControl CLI command module ''' import re import ConfigParser import os import shlex import imp from cccli.exception import * from sjrpc.core.exceptions import * from cccli.printer import Printer, color from optparse import OptionParser import cccli.commands class Commands(object): '''Command manager''' def __init__(self, cli): # save cli context self.cli = cli # build command list self.cmds = self.load_commands(cccli.commands.__path__[0], Command) # build remote function list try: self.functions = set([ c["name"] for c in self.cli.rpc.call("functions") ]) except RpcError as e: raise cliError("RPCError: Unable to retrieve remote commands: %s"%str(e)) # remove not available remote commands for cname in tuple(self.cmds): cobj = self.cmds[cname](self.cli, cname) if isinstance(cobj, RemoteCommand): try: if len(cobj.remote_functions()) == 0: raise NotImplementedError("No remote function") if not cobj.remote_functions().issubset(self.functions): del self.cmds[cname] self.cli.printer.debug("Command %s not available"%cname) except NotImplementedError as e: self.cli.printer.debug("Command %s lack of remote_functions"%cname) del self.cmds[cname] def __len__(self): return len(self.cmds) def __contains__(self, item): return item in self.cmds.keys() def __iter__(self): return iter(self.cmds.keys()) def __repr__(self): return repr(self.cmds.keys()) def __call__(self, argv): # check argv if len(argv) == 0: raise cmdBadName() # find right commands to call if argv[0] not in self: matchlist = [ x for x in self if re.match("%s.+"%re.escape(argv[0]), x) ] if len(matchlist) == 1: argv[0] = matchlist[0] else: raise cmdBadName() # create class and call it cmd = self.cmds[argv[0]](self.cli, argv[0]) return cmd(argv) def usage(self, argv0): '''Return usage of a command''' u = self.cmds[argv0](self.cli, argv0).usage() return u if u is not None else "" def help(self, argv0): '''Return of a command''' h = self.cmds[argv0](self.cli, argv0).help() return h if h is not None else "" def load_commands(self, path, cls): '''Load sublasss of cls from package name''' cmds=dict() for name in os.listdir(path): if name.endswith(".py") and not name.startswith("__"): fpath = os.path.join(path, name) module = imp.load_source(os.path.splitext(name)[0], fpath) for key, entry in module.__dict__.items(): try: if issubclass(entry, cls) and entry.__name__.startswith("Command_"): cmds[entry.__name__[8:]] = entry except TypeError: continue return cmds class Aliases(dict): ''' Aliases manager''' def load(self, filename): '''load alias from file''' if filename is not None: fparser = ConfigParser.RawConfigParser() fparser.read(filename) if fparser.has_section("alias"): self.clear() self.update(fparser.items("alias")) def save(self, filename): '''save alias on file''' if filename is not None: fparser = ConfigParser.RawConfigParser() fparser.read(filename) fparser.remove_section("alias") fparser.add_section("alias") for n,v in self.items(): fparser.set("alias", n, v) fparser.write(open(filename, "w")) def substitute(self, argv): # trip is the number of subtitution of an alias # maximum number of trip is set to 10 for trip in range(10): if argv[0] in self: oldargv = argv[1:] argv = shlex.split(self[argv[0]]) argv.extend(oldargv) return argv class Command(object): '''Base of all command class''' def __init__(self, cli, argv0): self.cli = cli self.printer = self.cli.printer self.name = argv0 def __call__(self, argv): '''Code called when command is invoked''' raise NotImplementedError def name(self): '''Name of the command''' raise NotImplementedError def usage(self): return "Usage: %s"%self.name def help(self): return self.__doc__ class OptionCommand(Command): '''Commands with options handling''' class OptionCommandParser(OptionParser): '''Parser of Option for OptionCommand''' def error(self, e): raise cmdBadArgument(e) def exit(self): raise cmdExit() def __init__(self, cli, argv0): Command.__init__(self, cli, argv0) self.optionparser = OptionCommand.OptionCommandParser(prog=argv0) self.set_usage("%prog [options]") self.options = None self.args = list() def usage(self): '''Return usage string''' return self.optionparser.format_help().strip() def parse_args(self, argv): '''Wrapper to parse_args''' (self.options, self.args) = self.optionparser.parse_args(argv[1:]) def add_option(self, *args, **kwargs): '''Proxy to OptionParser''' self.optionparser.add_option(*args, **kwargs) def remove_option(self, *args, **kwargs): '''Proxy to OptionParser''' self.optionparser.remove_option(*args, **kwargs) def set_usage(self, *args, **kwargs): '''Proxy to OptionParser''' self.optionparser.set_usage(*args, **kwargs) class RemoteCommand(OptionCommand): '''Command which needs connection to server''' def __init__(self, cli, argv0): OptionCommand.__init__(self, cli, argv0) # ignore tag option self.add_option("-I", "--ignore-tag", action="append", dest="ignore", default=[], help="Don't display a tag in objects") # no print count at end option self.add_option("--no-count", action="store_true", dest="nocount", default=False, help="Don't print count at end of objects") # index printing self.add_option("-i", "--index", action="store_true", dest="index", default=False, help="Print object lines indexed") self.rpc = cli.rpc def remote_functions(self): '''Return a set of needed remote functions''' raise NotImplementedError def print_objects(self, objectlist): '''Trivial objectlist printing of tag''' if objectlist is None: return # get default index from local options _order = objectlist.get("order", None) for (i, o) in enumerate(objectlist["objects"]): if self.options.index: self.printer.out("[%s] "%i, nl="") self.print_tags(o, order=_order) if not self.options.nocount: length = len(objectlist["objects"]) if length > 1: self.printer.out("Objects count: %s" % length) def print_tags(self, taglist, order=None): '''Display a tag with tagdisplay settings''' order = () if order is None else order # copy dict to show tl = taglist.copy() # remove ignore tags for tn in self.options.ignore: tl.pop(tn, None) # list to print pls = [] # print firstly order tags for tn in order: tv = tl.pop(tn, None) if tv is not None: pls.append("%s%s:%s%s"%(self.tdtc(tn), tn, self.tdc(tn), self.tdr(tn, tv))) # print tags without order, alpha ordered for tn in sorted(tl.keys()): pls.append("%s%s:%s%s"%(self.tdtc(tn), tn, self.tdc(tn), self.tdr(tn, tl[tn]))) self.printer.out("%s%s"%(" ".join(pls), color["reset"])) class TqlCommand(RemoteCommand): '''Command which handle TQL stuff''' def __init__(self, cli, argv0): RemoteCommand.__init__(self, cli, argv0) self.set_usage("%prog [options] <tql>") # set tql filter stuff self.tql_filter = "" self.add_option("-r", "--raw", action="callback", dest="raw", callback=self._cb_raw, help="Don't append security filter to TQL") # set tql check stuff self.add_option("-d", "--direct", action="store_true", dest="direct", default=False, help="Directly send TQL to server") # set tql status stuff self.add_option("-q", "--quiet", action="store_false", dest="status", default=True, help="Dont status of call request") # tql printer option self.add_option("--print-tql", action="store_true", dest="tql_print", default=False, help="Print TQL before sending to server") # set tagdisplay stuff self.tdr = self.cli.tagdisplay.resolve self.tdc = self.cli.tagdisplay.color self.tdtc = self.cli.tagdisplay.titlecolor self.add_option("--no-tagdisplay", action="callback", callback=self._cb_notagdisplay, help="No tagdisplay custom display") self.add_option("--no-color", action="callback", callback=self._cb_nocolor, help="No output coloration") def _cb_notagdisplay(self, option, opt, value, parser): '''Callback for option --no-tagdisplay''' self.tdr = lambda tagname, tagvalue: tagvalue self.tdc = self.cli.tagdisplay.default_color self.tdtc = self.cli.tagdisplay.default_titlecolor def _cb_nocolor(self, option, opt, value, parser): '''Callback for option --no-color''' self.tdr = lambda tagname, tagvalue: tagvalue self.tdc = lambda tagname: "" self.tdtc = lambda tagname: "" def _cb_raw(self, option, opt, value, parser): '''Callback for option --raw''' self.tql_filter = "" def rpccall(self, *args, **kwargs): ''' Call a RPC method an show tql return _callback: call function _callback after each rpccall _status: display call status _tql_index: is index in args where filter should be appended (def: 1) _tql_print: print tql with filter _exception: catch or not RPCError exception _direct: call directly, no listing before _arg_list: list of arg which are added to args one by one (=> multiple call) ''' # set on line modified options _saved_options = {} for _o in kwargs.copy(): if _o.startswith("_"): o = _o[1:] _saved_options[o] = getattr(self.options, o) if hasattr(self.options, o) else None setattr(self.options, o, kwargs[_o]) kwargs.pop(_o) # set tql_index to 1 if any if not hasattr(self.options, "tql_index"): self.options.tql_index = 1 # check tql index if self.options.tql_index < 0 or self.options.tql_index >= len(args): raise cmdError("No indexed TQL") # append filter (empty if raw mode) if self.tql_filter != "": l = list(args) l[self.options.tql_index] += self.tql_filter args = tuple(l) # Tql printer if self.options.tql_print: self.printer.out("TQL: %s"%args[self.options.tql_index]) # Tql check if self.options.direct: out= self._unsecure_rpccall(args, **kwargs) else: out = self._secure_rpccall(args, **kwargs) # restore saved options for (k, v) in _saved_options.items(): if v is None: delattr(self.options, k) else: setattr(self.options, k, v) # return output return out def _unsecure_rpccall(self, args, **kwargs): '''Just call an RPC without checking before''' try: if not hasattr(self.options, "arg_list"): d = self.rpc.call(*args, **kwargs) if hasattr(self.options, "callback"): self.options.callback(d) if hasattr(self.options, "status") and self.options.status: self.options.ignore += [ "output" ] self.print_objects(d) else: # arg_list mode call command for each argument # return is returned as a list d = [] for arg in self.options.arg_list: args2 = args + [ arg ] r = self.rpc.call(*args2, **kwargs) d.append(r) if hasattr(self.options, "callback"): self.options.callback(r) if hasattr(self.options, "status") and self.options.status: self.options.ignore += [ "output" ] self.print_objects(r) return d except RpcError as e: if hasattr(self.options, "exception"): raise raise cmdError("RPCError: %s"%str(e)) def _secure_rpccall(self, args, **kwargs): '''Call RPC after listing, confirmation and with id''' # get objects id try: objs = self.cli.rpc.call("list", args[self.options.tql_index]) except RpcError as e: raise cmdError("RPCError: %s"%str(e)) # no result, goodbye if len(objs["objects"]) == 0: raise cmdError("No selected object by TQL.") self.printer.out("Objects:") self.print_objects(objs) # be sure boby want do that if self.printer.ask("%sProceed?%s (yes): "%(color["lred"], color["reset"])) != "yes": raise cmdWarning("User aborted") # bobby doing many things, he needs to be really sure! if len(objs["objects"]) > 5: self.printer.out("%sYou will act on more than 5 objets!%s"%(color["uyellow"], color["reset"])) if self.printer.ask("%sAre you really sure?%s (Yes Mistress): " %(color["lred"], color["reset"])) != "Yes Mistress": raise cmdWarning("User aborted") # per validated id execution (this is a kind of atomic implementation) for obj in objs["objects"]: # make a list from tupe for editing args2 = list(args) # ovewrite tql object by id from object args2[self.options.tql_index] = "id=%s"%obj["id"] # now we can make call self._unsecure_rpccall(args2, **kwargs)