From 98a608647f757c12aa94d1d6c196ae2b6b5dab10 Mon Sep 17 00:00:00 2001
From: Seblu <sebastien.luttringer@smartjog.com>
Date: Mon, 23 May 2011 18:23:47 +0200
Subject: [PATCH] Improve commands loading

Commands loading is now done by crawling commands directory under cccli module.
All command which starts by Commmand_ and which is a son of Command class are candidate.
---
 cccli/cli.py                              |   2 +-
 cccli/{command => }/command.py            | 120 ++++++++++++++++++++++
 cccli/command/__init__.py                 |  90 ----------------
 cccli/commands.py                         | 108 -------------------
 cccli/commands/__init__.py                |   8 ++
 cccli/{command => commands}/account.py    |   2 +-
 cccli/{command => commands}/alias.py      |   2 +-
 cccli/{command => commands}/cancel.py     |   2 +-
 cccli/{command => commands}/execute.py    |   2 +-
 cccli/{command => commands}/expert.py     |   2 +-
 cccli/{command => commands}/jobs.py       |   2 +-
 cccli/{command => commands}/kill.py       |   2 +-
 cccli/{command => commands}/list.py       |   2 +-
 cccli/{command => commands}/migrate.py    |   2 +-
 cccli/{command => commands}/right.py      |   2 +-
 cccli/{command => commands}/server.py     |   2 +-
 cccli/{command => commands}/shell.py      |   2 +-
 cccli/{command => commands}/shutdown.py   |   2 +-
 cccli/{command => commands}/tag.py        |   2 +-
 cccli/{command => commands}/tagdisplay.py |   2 +-
 cccli/{command => commands}/vm.py         |   2 +-
 setup.py                                  |   4 +-
 22 files changed, 147 insertions(+), 217 deletions(-)
 rename cccli/{command => }/command.py (70%)
 delete mode 100644 cccli/command/__init__.py
 delete mode 100644 cccli/commands.py
 create mode 100644 cccli/commands/__init__.py
 rename cccli/{command => commands}/account.py (98%)
 rename cccli/{command => commands}/alias.py (97%)
 rename cccli/{command => commands}/cancel.py (95%)
 rename cccli/{command => commands}/execute.py (95%)
 rename cccli/{command => commands}/expert.py (95%)
 rename cccli/{command => commands}/jobs.py (96%)
 rename cccli/{command => commands}/kill.py (93%)
 rename cccli/{command => commands}/list.py (99%)
 rename cccli/{command => commands}/migrate.py (98%)
 rename cccli/{command => commands}/right.py (98%)
 rename cccli/{command => commands}/server.py (97%)
 rename cccli/{command => commands}/shell.py (98%)
 rename cccli/{command => commands}/shutdown.py (96%)
 rename cccli/{command => commands}/tag.py (97%)
 rename cccli/{command => commands}/tagdisplay.py (98%)
 rename cccli/{command => commands}/vm.py (99%)

diff --git a/cccli/cli.py b/cccli/cli.py
index b9cb2e3..e00cbcb 100644
--- a/cccli/cli.py
+++ b/cccli/cli.py
@@ -9,7 +9,7 @@ CloudControl CLI main module
 import cccli
 from cccli.exception import *
 from cccli.printer import Printer, color
-from cccli.commands import Commands, Aliases
+from cccli.command import Commands, Aliases
 from cccli.handler import CliHandler
 from cccli.tagdisplay import TagDisplay
 from sjrpc.core.exceptions import *
diff --git a/cccli/command/command.py b/cccli/command.py
similarity index 70%
rename from cccli/command/command.py
rename to cccli/command.py
index 4d098de..a8d0219 100644
--- a/cccli/command/command.py
+++ b/cccli/command.py
@@ -4,11 +4,126 @@
 '''
 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):
+        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'''
@@ -19,6 +134,11 @@ class Command(object):
         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):
diff --git a/cccli/command/__init__.py b/cccli/command/__init__.py
deleted file mode 100644
index d4d3fc5..0000000
--- a/cccli/command/__init__.py
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/env python
-#coding=utf8
-
-'''
-CloudControl CLI Commands Package
-'''
-
-__all__ = [
-    "Command",
-    "OptionCommand",
-    "RemoteCommand",
-    "TqlCommand",
-    "Command_addaccount",
-    "Command_addright",
-    "Command_addtag",
-    "Command_alias",
-    "Command_cancel",
-    "Command_clear",
-    "Command_clone",
-    "Command_close",
-    "Command_declose",
-    "Command_delaccount",
-    "Command_delright",
-    "Command_deltag",
-    "Command_destroy",
-    "Command_execute",
-    "Command_expert",
-    "Command_help",
-    "Command_history",
-    "Command_jobs",
-    "Command_kill",
-    "Command_list",
-    "Command_migrate",
-    "Command_passwd",
-    "Command_pause",
-    "Command_quit",
-    "Command_resume",
-    "Command_rights",
-    "Command_server",
-    "Command_shutdown",
-    "Command_start",
-    "Command_stop",
-    "Command_tagdisplay",
-    "Command_tags",
-    "Command_unalias",
-    "Command_undefine",
-    "Command_usage",
-    "Command_version",
-    "Command_whoami",
-]
-
-# importing
-from cccli.command.command import Command, OptionCommand, RemoteCommand, TqlCommand
-from cccli.command.account import Command_addaccount
-from cccli.command.account import Command_close
-from cccli.command.account import Command_declose
-from cccli.command.account import Command_delaccount
-from cccli.command.account import Command_passwd
-from cccli.command.alias import Command_alias
-from cccli.command.alias import Command_unalias
-from cccli.command.cancel import Command_cancel
-from cccli.command.execute import Command_execute
-from cccli.command.expert import Command_expert
-from cccli.command.jobs import Command_jobs
-from cccli.command.kill import Command_kill
-from cccli.command.list import Command_list
-from cccli.command.migrate import Command_migrate
-from cccli.command.right import Command_addright
-from cccli.command.right import Command_delright
-from cccli.command.right import Command_rights
-from cccli.command.server import Command_server
-from cccli.command.shell import Command_clear
-from cccli.command.shell import Command_help
-from cccli.command.shell import Command_history
-from cccli.command.shell import Command_quit
-from cccli.command.shell import Command_usage
-from cccli.command.shell import Command_version
-from cccli.command.shell import Command_whoami
-from cccli.command.shutdown import Command_shutdown
-from cccli.command.tag import Command_addtag
-from cccli.command.tag import Command_deltag
-from cccli.command.tag import Command_tags
-from cccli.command.tagdisplay import Command_tagdisplay
-from cccli.command.vm import Command_clone
-from cccli.command.vm import Command_destroy
-from cccli.command.vm import Command_pause
-from cccli.command.vm import Command_resume
-from cccli.command.vm import Command_start
-from cccli.command.vm import Command_stop
-from cccli.command.vm import Command_undefine
diff --git a/cccli/commands.py b/cccli/commands.py
deleted file mode 100644
index 21a463f..0000000
--- a/cccli/commands.py
+++ /dev/null
@@ -1,108 +0,0 @@
-#!/usr/bin/env python
-#coding=utf8
-
-'''
-CloudControl CLI commands module
-'''
-import re
-import ConfigParser
-import os
-import shlex
-
-from cccli.exception import *
-from cccli.command import *
-
-class Commands(object):
-    '''Command manager'''
-
-    def __init__(self, cli):
-        # save cli context
-        self.cli = cli
-        # build command list
-        self.cmds = dict()
-        for x in [ x for x in globals() if x.startswith("Command_") ]:
-            self.cmds[x[8:]] = globals()[x]
-        # 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 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 ""
-
-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):
-        if argv[0] in self:
-            oldargv = argv[1:]
-            argv = shlex.split(self[argv[0]])
-            argv.extend(oldargv)
-        return argv
diff --git a/cccli/commands/__init__.py b/cccli/commands/__init__.py
new file mode 100644
index 0000000..5796bf0
--- /dev/null
+++ b/cccli/commands/__init__.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+#coding=utf8
+
+'''
+CloudControl CLI Commands Package
+'''
+
+__all__ = []
diff --git a/cccli/command/account.py b/cccli/commands/account.py
similarity index 98%
rename from cccli/command/account.py
rename to cccli/commands/account.py
index 350b846..280cf6a 100644
--- a/cccli/command/account.py
+++ b/cccli/commands/account.py
@@ -8,7 +8,7 @@ CloudControl accounts related commands
 from cccli.exception import *
 from sjrpc.core.exceptions import *
 from cccli.printer import Printer, color
-from cccli.command.command import TqlCommand
+from cccli.command import TqlCommand
 
 class Command_addaccount(TqlCommand):
     '''Create an account'''
diff --git a/cccli/command/alias.py b/cccli/commands/alias.py
similarity index 97%
rename from cccli/command/alias.py
rename to cccli/commands/alias.py
index a41da61..e499511 100644
--- a/cccli/command/alias.py
+++ b/cccli/commands/alias.py
@@ -6,7 +6,7 @@ CloudControl alias related command
 '''
 
 from cccli.exception import *
-from cccli.command.command import OptionCommand
+from cccli.command import OptionCommand
 import re
 
 class Command_alias(OptionCommand):
diff --git a/cccli/command/cancel.py b/cccli/commands/cancel.py
similarity index 95%
rename from cccli/command/cancel.py
rename to cccli/commands/cancel.py
index 08e1d45..0eac3ea 100644
--- a/cccli/command/cancel.py
+++ b/cccli/commands/cancel.py
@@ -8,7 +8,7 @@ CloudControl cancel command
 from cccli.exception import *
 from sjrpc.core.exceptions import *
 from cccli.printer import Printer, color
-from cccli.command.command import RemoteCommand
+from cccli.command import RemoteCommand
 
 class Command_cancel(RemoteCommand):
     '''Cancel a job'''
diff --git a/cccli/command/execute.py b/cccli/commands/execute.py
similarity index 95%
rename from cccli/command/execute.py
rename to cccli/commands/execute.py
index 41dc467..49ea3aa 100644
--- a/cccli/command/execute.py
+++ b/cccli/commands/execute.py
@@ -6,7 +6,7 @@ CloudControl execute command
 from cccli.exception import *
 from sjrpc.core.exceptions import *
 from cccli.printer import Printer, color
-from cccli.command.command import TqlCommand
+from cccli.command import TqlCommand
 
 class Command_execute(TqlCommand):
     '''Execute a command on the remote host'''
diff --git a/cccli/command/expert.py b/cccli/commands/expert.py
similarity index 95%
rename from cccli/command/expert.py
rename to cccli/commands/expert.py
index 9ab46be..3b31fee 100644
--- a/cccli/command/expert.py
+++ b/cccli/commands/expert.py
@@ -6,7 +6,7 @@ CloudControl expert command
 '''
 from cccli.exception import *
 from cccli.printer import Printer, color
-from cccli.command.command import Command
+from cccli.command import Command
 from sjrpc.utils import ConnectionProxy
 import code
 
diff --git a/cccli/command/jobs.py b/cccli/commands/jobs.py
similarity index 96%
rename from cccli/command/jobs.py
rename to cccli/commands/jobs.py
index 570807e..a6eb370 100644
--- a/cccli/command/jobs.py
+++ b/cccli/commands/jobs.py
@@ -8,7 +8,7 @@ CloudControl jobs command
 from cccli.exception import *
 from sjrpc.core.exceptions import *
 from cccli.printer import Printer, color
-from cccli.command.command import TqlCommand
+from cccli.command import TqlCommand
 
 class Command_jobs(TqlCommand):
     '''List jobs'''
diff --git a/cccli/command/kill.py b/cccli/commands/kill.py
similarity index 93%
rename from cccli/command/kill.py
rename to cccli/commands/kill.py
index 5ce3018..d89e108 100644
--- a/cccli/command/kill.py
+++ b/cccli/commands/kill.py
@@ -8,7 +8,7 @@ CloudControl Connection related commands
 from cccli.exception import *
 from sjrpc.core.exceptions import *
 from cccli.printer import Printer, color
-from cccli.command.command import TqlCommand
+from cccli.command import TqlCommand
 
 class Command_kill(TqlCommand):
     '''Kill a server connection'''
diff --git a/cccli/command/list.py b/cccli/commands/list.py
similarity index 99%
rename from cccli/command/list.py
rename to cccli/commands/list.py
index e4c1334..63c0ff4 100644
--- a/cccli/command/list.py
+++ b/cccli/commands/list.py
@@ -8,7 +8,7 @@ CloudControl list command
 from cccli.exception import *
 from sjrpc.core.exceptions import *
 from cccli.printer import Printer, color
-from cccli.command.command import TqlCommand
+from cccli.command import TqlCommand
 import math
 import os
 
diff --git a/cccli/command/migrate.py b/cccli/commands/migrate.py
similarity index 98%
rename from cccli/command/migrate.py
rename to cccli/commands/migrate.py
index 967ccf2..3fc6bf2 100644
--- a/cccli/command/migrate.py
+++ b/cccli/commands/migrate.py
@@ -8,7 +8,7 @@ CloudControl migrate command
 from cccli.exception import *
 from sjrpc.core.exceptions import *
 from cccli.printer import Printer, color
-from cccli.command.command import TqlCommand
+from cccli.command import TqlCommand
 
 class Command_migrate(TqlCommand):
     '''Migrate vm'''
diff --git a/cccli/command/right.py b/cccli/commands/right.py
similarity index 98%
rename from cccli/command/right.py
rename to cccli/commands/right.py
index c4fe42d..2e5250b 100644
--- a/cccli/command/right.py
+++ b/cccli/commands/right.py
@@ -8,7 +8,7 @@ CloudControl right releated commands
 from cccli.exception import *
 from sjrpc.core.exceptions import *
 from cccli.printer import Printer, color
-from cccli.command.command import Command, TqlCommand
+from cccli.command import Command, TqlCommand
 
 class Command_rights(TqlCommand):
     '''List account rights'''
diff --git a/cccli/command/server.py b/cccli/commands/server.py
similarity index 97%
rename from cccli/command/server.py
rename to cccli/commands/server.py
index 2252b66..2d4cc3c 100644
--- a/cccli/command/server.py
+++ b/cccli/commands/server.py
@@ -7,7 +7,7 @@ CloudControl server command
 from cccli.exception import *
 from sjrpc.core.exceptions import *
 from cccli.printer import Printer, color
-from cccli.command.command import RemoteCommand
+from cccli.command import RemoteCommand
 
 class Command_server(RemoteCommand):
     '''Server manipulation command'''
diff --git a/cccli/command/shell.py b/cccli/commands/shell.py
similarity index 98%
rename from cccli/command/shell.py
rename to cccli/commands/shell.py
index 886ff1f..6e88a91 100644
--- a/cccli/command/shell.py
+++ b/cccli/commands/shell.py
@@ -7,7 +7,7 @@ CloudControl shells related commands
 
 from cccli.exception import *
 from cccli.printer import Printer, color
-from cccli.command.command import Command
+from cccli.command import Command
 
 class Command_quit(Command):
     '''Quit application with respect'''
diff --git a/cccli/command/shutdown.py b/cccli/commands/shutdown.py
similarity index 96%
rename from cccli/command/shutdown.py
rename to cccli/commands/shutdown.py
index c80c6de..fd61661 100644
--- a/cccli/command/shutdown.py
+++ b/cccli/commands/shutdown.py
@@ -6,7 +6,7 @@ CloudControl physical host related commands
 from cccli.exception import *
 from sjrpc.core.exceptions import *
 from cccli.printer import Printer, color
-from cccli.command.command import TqlCommand
+from cccli.command import TqlCommand
 
 class Command_shutdown(TqlCommand):
     '''Shutdown a physical host'''
diff --git a/cccli/command/tag.py b/cccli/commands/tag.py
similarity index 97%
rename from cccli/command/tag.py
rename to cccli/commands/tag.py
index ba10e1e..d9b34f8 100644
--- a/cccli/command/tag.py
+++ b/cccli/commands/tag.py
@@ -8,7 +8,7 @@ CloudControl tag releated commands
 from cccli.exception import *
 from sjrpc.core.exceptions import *
 from cccli.printer import Printer, color
-from cccli.command.command import TqlCommand
+from cccli.command import TqlCommand
 
 from optparse import OptionParser
 
diff --git a/cccli/command/tagdisplay.py b/cccli/commands/tagdisplay.py
similarity index 98%
rename from cccli/command/tagdisplay.py
rename to cccli/commands/tagdisplay.py
index 884554d..ea8dcaa 100644
--- a/cccli/command/tagdisplay.py
+++ b/cccli/commands/tagdisplay.py
@@ -7,7 +7,7 @@ CloudControl tagdisplay command
 from cccli.exception import *
 from sjrpc.core.exceptions import *
 from cccli.printer import Printer, color
-from cccli.command.command import OptionCommand
+from cccli.command import OptionCommand
 
 class Command_tagdisplay(OptionCommand):
     '''Tagdisplay tool'''
diff --git a/cccli/command/vm.py b/cccli/commands/vm.py
similarity index 99%
rename from cccli/command/vm.py
rename to cccli/commands/vm.py
index 6892051..814013e 100644
--- a/cccli/command/vm.py
+++ b/cccli/commands/vm.py
@@ -6,7 +6,7 @@ CloudControl VM related commands
 from cccli.exception import *
 from sjrpc.core.exceptions import *
 from cccli.printer import Printer, color
-from cccli.command.command import TqlCommand
+from cccli.command import TqlCommand
 
 class Command_start(TqlCommand):
     '''Start a stopped vm'''
diff --git a/setup.py b/setup.py
index 0e03c42..c9473e1 100644
--- a/setup.py
+++ b/setup.py
@@ -11,8 +11,8 @@ setup(
     long_description=ldesc,
     author='Sebastien Luttringer',
     author_email='sebastien.luttringer@smartjog.com',
-    license='GPL2', 
-    packages=['cccli', 'cccli.command'],
+    license='GPL2',
+    packages=['cccli', 'cccli.commands'],
     scripts=['bin/cc-cli'],
     classifiers=[
         'Operating System :: Unix',
-- 
GitLab