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