From b9cf72710b4f520490f832cf7da013c486e21dae Mon Sep 17 00:00:00 2001 From: Seblu <sebastien.luttringer@smartjog.com> Date: Thu, 23 Dec 2010 15:48:40 +0100 Subject: [PATCH] Live free or die hard --- README | 92 +++++++++++++++++++++++++++++ bin/cc-cli | 19 +++--- cccli/__init__.py | 5 -- cccli/cli.py | 89 ++++++++++++++++++++++------ cccli/clierror.py | 3 + cccli/command.py | 147 ++++++++++++++++++++++++++++++++++++++-------- cccli/printer.py | 6 +- debian/control | 2 +- setup.py | 3 +- 9 files changed, 303 insertions(+), 63 deletions(-) diff --git a/README b/README index 245a349..e4587d9 100644 --- a/README +++ b/README @@ -1,3 +1,93 @@ +================== +Tag Query Language +================== +== by examples == +# list accounts +> list a + +# list hypervisor +> list hv + +# list vm +> list vm + +# list vm of hypervisor toto +> list hv=toto&vm + +# list vm chiche of hypervisor toto +> list hv=toto&vm=chiche + +#list vm with 2 cpu +> list vm&cpu=2 + +#list vm with 2 cpu and mem > 10g +> list vm&cpu=2&mem>10g + +#list hypervistor with 2cpu and vm with 2cpu +> list hv&cpu=2&vm&cpu=2 + +#list hypervistor at least 2cpu and show tags pop and alloc +> list hv&cpu>=2$pop$alloc + +== Basics == +- TQL build a list of objects from left to right +- Every tag can add or remove objects +- Separators create link between tag +- Operators apply only on one tag + +== separators of tags == +& and between tags +$ show a tag + +== operators on tags == += strict equality +!= not strict equlity +: globing matching +!:not globing matching +~ regex matching +!~ not regex matching +> superior strict +>= superior +< inferior +<= inferior strict + +== number facility == +10k = 1000 +10ki = 1024 +1m = 1000 ^ 2 +1mi = 1024 ^ 2 +1g = 1000 ^ 3 +1gi = 1024 ^ 3 + +== well known tags == +a: account name +hv: hypervisor name +vm: virtual machine name +id: a.hv.vm +h: hostname +role: (hypersivor/host/cli/vm) +hvtype: hypervistor type (xen/kvm) +libvirtver: Libvirt version +status: online/offline +vmstatus: Running/Paused/Stoped +pop: Point of Presence +cpu: cpu count +mem: memory size +usedmem: memory used +freemem: memory free +arch: (x86/x64) +uname: uname of host +uptime: uptime of hostname +load: load average +hvm: hardware virtualisation enabled +alloc: host is allowed to be selected to a migration +nvm: vm count on an hypervisor +version: account version +sto: total available storage +freesto: free storage +usedsto: used storage + + =========== New release =========== @@ -5,3 +95,5 @@ Update version in debian/control Update version in debian/changelog Update version in setup.py Update version in cccli/__init__.py + + diff --git a/bin/cc-cli b/bin/cc-cli index dbeb2aa..7840b22 100755 --- a/bin/cc-cli +++ b/bin/cc-cli @@ -17,17 +17,15 @@ import warnings import getpass settings = {} -alias = {} try: # parse rc file if "HOME" in os.environ and os.access("%s/.cc-cli.conf"%os.environ["HOME"], os.R_OK): fparser = ConfigParser.SafeConfigParser() fparser.read("%s/.cc-cli.conf"%os.environ["HOME"]) + settings["alias"] = "%s/.cc-cli.conf"%os.environ["HOME"] if fparser.has_section("cli"): - settings = dict(fparser.items("cli")) - if fparser.has_section("alias"): - alias = dict(fparser.items("alias")) + settings.update(fparser.items("cli")) # parse env if "CC_SERVER" in os.environ: @@ -36,6 +34,8 @@ try: settings["port"] = os.environ["CC_PORT"] if "CC_LOGIN" in os.environ: settings["login"] = os.environ["CC_LOGIN"] + if "CC_PASS" in os.environ: + settings["pass"] = os.environ["CC_PASS"] if "CC_DEBUG" in os.environ: settings["debug"] = "True" @@ -78,8 +78,8 @@ try: sys.exit(1) # remove pass to prevent stupid guy - if "pass" in settings: - del settings["pass"] + #if "pass" in settings: + # del settings["pass"] # debug stuff if "debug" in settings: @@ -88,7 +88,6 @@ try: warnings.filterwarnings("ignore") printer.debug("Debugging on") printer.debug("Settings: %s"%settings) - printer.debug("Alias: %s"%alias) # checking needs if settings["server"] == "": @@ -103,15 +102,17 @@ try: settings["login"] = raw_input("Login: ") # asking for password - settings["pass"] = getpass.getpass("Password: ") + if settings["pass"] == "": + settings["pass"] = getpass.getpass("Password: ") # start cli - cli = cli.Cli(settings, alias, args) + cli = cli.Cli(settings, args) cli.start() except KeyboardInterrupt: exit(1) except Exception, e: if cccli.debug: printer.fatal(str(e), exitcode=-1) + printer.warn("This is a not expected error, please report it!") raise printer.fatal(str(e)) diff --git a/cccli/__init__.py b/cccli/__init__.py index 22fbde2..243610e 100644 --- a/cccli/__init__.py +++ b/cccli/__init__.py @@ -7,8 +7,3 @@ CloudControl CLI version = "1~dev" debug = False - -import printer -import cli -import clierror -import command diff --git a/cccli/cli.py b/cccli/cli.py index b368dce..4cdd66a 100644 --- a/cccli/cli.py +++ b/cccli/cli.py @@ -10,24 +10,30 @@ import sys import socket import ssl import threading +import subprocess +import ConfigParser import cccli from cccli import printer, command from cccli.clierror import * +from cccli.command import Command from sjrpc.client import SimpleRpcClient from sjrpc.utils import RpcHandler, pure, threadless, ConnectionProxy +import sjrpc.core.exceptions import re import readline class Cli(object): - def __init__(self, settings, alias, args): + def __init__(self, settings, args): self._settings = settings - self._alias = alias + if "alias" in settings: + self.alias = Alias(settings["alias"]) + self.alias.load() self._interactive = sys.stderr.isatty() and sys.stdin.isatty() self._prompt = "> " self._commands = args - self._rpc = None + self.rpc = None def start(self): '''Start a CLI''' @@ -48,15 +54,18 @@ class Cli(object): printer.debug("Connecting...") rpcc = SimpleRpcClient.from_addr(self._settings["server"], self._settings["port"], - enable_ssl=True + enable_ssl=True, + on_disconnect=self._on_disconnect ) - # FIXME: wait sjrpc v5 with on_disconnect usable rpcc.start(daemonize=True) - self._rpc = ConnectionProxy(rpcc) + self.rpc = ConnectionProxy(rpcc) + + def _on_disconnect(self, rpc): + printer.fatal("Disconnected from server!") def _auth(self): printer.debug("Authenticating...") - if self._rpc.authentify(self._settings["login"], self._settings["pass"]): + if self.rpc.authentify(self._settings["login"], self._settings["pass"]): printer.debug("Authenticated.") else: printer.fatal("Autentification failed!") @@ -69,8 +78,6 @@ class Cli(object): try: line = raw_input(self._prompt) self._parse_line(line) - except BadCommand, e: - printer.error("No such command: %s"%e[0]) except EOFError: printer.out("") break @@ -78,12 +85,6 @@ class Cli(object): break except KeyboardInterrupt: printer.out("") - except Exception, e: - printer.error(e) - if cccli.debug: - raise - else: - printer.error("This is a not expected error, please report it!") try: pass #readline.write_history_file(self._settings["histfile"]) @@ -107,10 +108,62 @@ class Cli(object): p.wait() ret = p.returncode elif (cmd[0] == "?"): - command.Command("help").call() + Command("help").call() + else: + self._parse_command(cmd) + + def _parse_command(self, cmd): + try: + # lex command + argv = self._lex_argv(cmd) + # alias subs + if argv[0] in self.alias: + argv[0] = self.alias[argv[0]] + # execute command + Command(argv, self).call() + except BadArgument, e: + if str(e) != "": + printer.error("Bad argument: %s."%str(e)) else: - argv = self._lex_argv(cmd) - command.Command(argv, self._rpc).call() + printer.error("Bad argument.") + usage = Command.usage(argv[0]) + if usage != "": + printer.out("usage: %s."%usage) + except BadCommand: + printer.error("No command: %s."%argv[0]) + except sjrpc.core.exceptions.RpcError, e: + if cccli.debug: raise + printer.error("sjRPC: %s"%str(e)) + except Exception, e: + if cccli.debug: raise + printer.error("%s: %s."%(argv[0], str(e))) def _lex_argv(self, string): + '''Lex command argument''' return string.split(" ") + + +class Alias(dict): + ''' Alias wrapper''' + def __init__(self, filename): + self._filename = filename + + def load(self): + '''load alias from file''' + if os.access(self._filename, os.R_OK): + fparser = ConfigParser.SafeConfigParser() + fparser.read(self._filename) + if fparser.has_section("alias"): + self.clear() + self.update(fparser.items("alias")) + + def save(self): + '''save alias on file''' + if os.access(self._filename, os.R_OK or os.W_OK): + fparser = ConfigParser.SafeConfigParser() + fparser.read(self._filename) + fparser.remove_section("alias") + fparser.add_section("alias") + for n,v in self.items(): + fparser.set("alias", n, v) + fparser.write(open(self._filename, "w")) diff --git a/cccli/clierror.py b/cccli/clierror.py index 1ffd244..6fd1000 100644 --- a/cccli/clierror.py +++ b/cccli/clierror.py @@ -11,3 +11,6 @@ class cliError(Exception): class BadCommand(cliError): pass + +class BadArgument(cliError): + pass diff --git a/cccli/command.py b/cccli/command.py index fbf6656..81e88b5 100644 --- a/cccli/command.py +++ b/cccli/command.py @@ -1,15 +1,29 @@ +import os, os.path +import sys import re from sjrpc.client import SimpleRpcClient +import cccli +import pprint from cccli import printer from cccli.clierror import * class Command(object): - def __init__(self, argv, rpc): + def __init__(self, argv, cli): if len(argv) < 1: - raise Exception("Empty command") + raise BadCommand() self._argv = argv - self._rpc = rpc + self.cli = cli + + @classmethod + def usage(cls, cmdname): + '''Return usage of a command''' + fname = "cmd_%s"%cmdname + if not hasattr(cls, fname): + raise BadArgument(cmdname) + if hasattr(getattr(cls, fname), "usage"): + return getattr(getattr(cls, fname), "usage") + return "" def call(self): '''Run command''' @@ -20,35 +34,116 @@ class Command(object): return cmd(self._argv) raise BadCommand(self._argv[0]) - def usage(self, cmdname, printf=None): - '''Return usage of a command''' - fname = "cmd_%s"%cmdname - if hasattr(self, fname): - if hasattr(getattr(self, fname), "usage"): - s = getattr(getattr(self, fname), "usage") - if printf != None: - printf(s) - return s - else: - return "No usage" - else: - raise myError("No such command: %s"%cmdname) - def cmd_exit(self, argv): '''Quit application with respect''' raise SystemExit() + cmd_exit.usage = "exit" + cmd_exit.desc = "Quit application with respect" + + def cmd_quit(self, argv): + '''Quit application with respect''' + raise SystemExit() + cmd_quit.usage = "quit" + cmd_quit.desc = "Quit application with respect" + + def cmd_version(self, argv): + '''Print cli version''' + printer.out(cccli.version) + cmd_version.usage = "version" + cmd_version.desc = "Print cli version" + + def cmd_usage(self, argv): + '''Print usage of a command''' + if len(argv) != 2: + raise BadArgument() + usage = Command.usage(argv[1]) + if usage != "": + printer.out("usage: %s"%usage) + else: + printer.out("No usage.") + cmd_usage.usage = "usage [command]" + cmd_usage.desc = "Print usage of a command" + + def cmd_help(self, argv): + '''Print help''' + if len(argv) == 1: + # build command list + cmdlist = list() + doclist = list() + for x in dir(self): + m = re.match("^cmd_([a-zA-Z0-9]+)$", x) + if m: + cmdlist.append(m.group(1)) + if hasattr(getattr(self, x), "__doc__"): + doclist.append(getattr(getattr(self, x), "__doc__")) + # printing commands list + width = max(map(len, cmdlist)) + 3 + printer.out("%sCommands:%s"%(printer.color["lwhite"], printer.color["reset"])) + for c, d in zip(cmdlist, doclist): + line = "%s"%c + line = line.ljust(width,) + line += "- %s"%d + printer.out(line) + elif len(argv) == 2: + fname = "cmd_%s"%argv[1] + if hasattr(self, fname): + if hasattr(getattr(self, fname), "__doc__"): + printer.out("Description: %s"%getattr(getattr(self, fname), "__doc__")) + if hasattr(getattr(self, fname), "usage"): + printer.out("Usage: %s"%getattr(getattr(self, fname), "usage")) + if hasattr(getattr(self, fname), "details"): + Printer.out("Details: %s"%getattr(getattr(self, fname), "details")) + else: + raise BadArgument(argv[1]) + else: + raise BadArgument() + cmd_help.usage = "help [command]" + cmd_help.desc = "Print help about a command" + + def cmd_alias(self, argv): + '''Show or create alias''' + if len(argv) == 1: + for n, v in self.cli.alias.items(): + printer.out("%s=%s"%(n, v)) + elif len(argv) == 2: + if argv[1] not in self.cli.alias: + raise BadArgument(argv[1]) + printer.out("%s=%s"%(argv[1], self.cli.alias[argv[1]])) + elif len(argv) == 3: + self.cli.alias[argv[1]] = argv[2] + self.cli.alias.save() + else: + raise BadArgument() + cmd_alias.usage = "alias [name] [value]" + cmd_alias.desc = "Show or create aliases" + + def cmd_unalias(self, argv): + '''Remove an alias''' + if len(argv) != 2: + raise BadArgument() + if argv[1] not in self.cli.alias: + raise BadArgument("%s: No such alias"%argv[1]) + del self.cli.alias[argv[1]] + self.cli.alias.save() + cmd_alias.usage = "unalias [name]" + cmd_alias.desc = "Remove an aliases" + + def cmd_rcmd(self, argv): + '''Show remote commands''' + for cmds in self.cli.rpc.list_commands(): + printer.out("%s"%cmds["name"]) + cmd_rcmd.usage = "rcmd" + cmd_rcmd.desc = "Print remote command list" def cmd_list(self, argv): '''List something''' if len(argv) == 1: - argv.append("hv") - items = self._rpc.list(str.join("", argv[1:])) + argv.append("") + items = self.cli.rpc.list(str.join("", argv[1:])) for item in items: - for key, val in item.items(): - printer.out("%s: %s "%(key, val)) - printer.out("*"*80) + pprint.pprint(item) + #for key, val in item.items(): + # printer.out("%s: %s "%(key, val)) + cmd_list.usage = "list [tags]" + cmd_list.desc = "Print information about tags" - def cmd_export(self, argv): - '''List server exported methods''' - for cmds in self._rpc.list_commands(): - printer.out("%s"%cmds["name"]) diff --git a/cccli/printer.py b/cccli/printer.py index b35657b..67cc3a2 100644 --- a/cccli/printer.py +++ b/cccli/printer.py @@ -56,11 +56,11 @@ def fatal(message, exitcode=42, fd=sys.stderr, nl=os.linesep): color["reset"]) , fd, nl) if exitcode >= 0: - exit(exitcode) + os._exit(exitcode) ############################################################################### def error(message, fd=sys.stderr, nl=os.linesep): - out("%sError%s: %s.%s"%(color["lred"], + out("%sError%s: %s%s"%(color["lred"], color["red"], message, color["reset"]) @@ -68,7 +68,7 @@ def error(message, fd=sys.stderr, nl=os.linesep): ############################################################################### def warn(message, fd=sys.stderr, nl=os.linesep): - out("%sWarning%s: %s.%s"%(color["lyellow"], + out("%sWarning%s: %s%s"%(color["lyellow"], color["yellow"], message, color["reset"]) diff --git a/debian/control b/debian/control index 24c1a8f..71cbc3f 100644 --- a/debian/control +++ b/debian/control @@ -9,7 +9,7 @@ Standards-Version: 3.8.0 Package: cc-cli Architecture: all -Depends: ${misc:Depends}, ${python:Depends}, python (<< 3), python-sjrpc (>= 4) +Depends: ${misc:Depends}, ${python:Depends}, python (<< 3), python-sjrpc (>= 5) XB-Python-Version: ${python:Versions} Description: CloudControl CLI This package provides the Command Line Interface to CloudControl. diff --git a/setup.py b/setup.py index 4d20370..4352c36 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,12 @@ from setuptools import setup +import cccli import os ldesc = open(os.path.join(os.path.dirname(__file__), 'README')).read() setup( name='cc-cli', - version='1', + version=cccli.version, description='CloudControl CLI', long_description=ldesc, author='Sebastien Luttringer', -- GitLab