Browse Source

Add basic CLI

Ben Firshman 12 years ago
parent
commit
3b654ad349

+ 0 - 0
plum/cli/__init__.py


+ 29 - 0
plum/cli/command.py

@@ -0,0 +1,29 @@
+from docker import Client
+import logging
+import os
+import yaml
+
+from ..service_collection import ServiceCollection
+from .docopt_command import DocoptCommand
+from .formatter import Formatter
+from .utils import cached_property, mkdir
+
+log = logging.getLogger(__name__)
+
+class Command(DocoptCommand):
+    @cached_property
+    def client(self):
+        if os.environ.get('DOCKER_URL'):
+            return Client(os.environ['DOCKER_URL'])
+        else:
+            return Client()
+
+    @cached_property
+    def service_collection(self):
+        config = yaml.load(open('plum.yml'))
+        return ServiceCollection.from_config(self.client, config)
+
+    @cached_property
+    def formatter(self):
+        return Formatter()
+

+ 46 - 0
plum/cli/docopt_command.py

@@ -0,0 +1,46 @@
+import sys
+
+from inspect import getdoc
+from docopt import docopt, DocoptExit
+
+
+def docopt_full_help(docstring, *args, **kwargs):
+    try:
+        return docopt(docstring, *args, **kwargs)
+    except DocoptExit:
+        raise SystemExit(docstring)
+
+
+class DocoptCommand(object):
+    def sys_dispatch(self):
+        self.dispatch(sys.argv[1:], None)
+
+    def dispatch(self, argv, global_options):
+        self.perform_command(*self.parse(argv, global_options))
+
+    def perform_command(self, options, command, handler, command_options):
+        handler(command_options)
+
+    def parse(self, argv, global_options):
+        options = docopt_full_help(getdoc(self), argv, options_first=True)
+        command = options['COMMAND']
+
+        if not hasattr(self, command):
+            raise NoSuchCommand(command, self)
+
+        handler = getattr(self, command)
+        docstring = getdoc(handler)
+
+        if docstring is None:
+            raise NoSuchCommand(command, self)
+
+        command_options = docopt_full_help(docstring, options['ARGS'], options_first=True)
+        return (options, command, handler, command_options)
+
+
+class NoSuchCommand(Exception):
+    def __init__(self, command, supercommand):
+        super(NoSuchCommand, self).__init__("No such command: %s" % command)
+
+        self.command = command
+        self.supercommand = supercommand

+ 6 - 0
plum/cli/errors.py

@@ -0,0 +1,6 @@
+from textwrap import dedent
+
+
+class UserError(Exception):
+    def __init__(self, msg):
+        self.msg = dedent(msg).strip()

+ 15 - 0
plum/cli/formatter.py

@@ -0,0 +1,15 @@
+import texttable
+import os
+
+
+class Formatter(object):
+    def table(self, headers, rows):
+        height, width = os.popen('stty size', 'r').read().split()
+
+        table = texttable.Texttable(max_width=width)
+        table.set_cols_dtype(['t' for h in headers])
+        table.add_rows([headers] + rows)
+        table.set_deco(table.HEADER)
+        table.set_chars(['-', '|', '+', '-'])
+
+        return table.draw()

+ 83 - 0
plum/cli/main.py

@@ -0,0 +1,83 @@
+import datetime
+import logging
+import sys
+import os
+import re
+
+from docopt import docopt
+from inspect import getdoc
+
+from .. import __version__
+from ..service_collection import ServiceCollection
+from .command import Command
+
+from .errors import UserError
+from .docopt_command import NoSuchCommand
+
+log = logging.getLogger(__name__)
+
+def main():
+    try:
+        command = TopLevelCommand()
+        command.sys_dispatch()
+    except KeyboardInterrupt:
+        log.error("\nAborting.")
+        exit(1)
+    except UserError, e:
+        log.error(e.msg)
+        exit(1)
+    except NoSuchCommand, e:
+        log.error("No such command: %s", e.command)
+        log.error("")
+        log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand))))
+        exit(1)
+
+
+# stolen from docopt master
+def parse_doc_section(name, source):
+    pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)',
+                         re.IGNORECASE | re.MULTILINE)
+    return [s.strip() for s in pattern.findall(source)]
+
+
+class TopLevelCommand(Command):
+    """.
+
+    Usage:
+      plum [options] [COMMAND] [ARGS...]
+      plum -h|--help
+
+    Options:
+      --verbose            Show more output
+      --version            Print version and exit
+
+    Commands:
+      ps        List services and containers
+
+    """
+    def ps(self, options):
+        """
+        List services and containers.
+
+        Usage: ps
+        """
+        for service in self.service_collection:
+            for container in service.containers:
+                print container['Names'][0]
+
+    def start(self, options):
+        """
+        Start all services
+
+        Usage: start
+        """
+        self.service_collection.start()
+
+    def stop(self, options):
+        """
+        Stop all services
+
+        Usage: stop
+        """
+        self.service_collection.stop()
+

+ 76 - 0
plum/cli/utils.py

@@ -0,0 +1,76 @@
+import datetime
+import os
+
+
+def cached_property(f):
+    """
+    returns a cached property that is calculated by function f
+    http://code.activestate.com/recipes/576563-cached-property/
+    """
+    def get(self):
+        try:
+            return self._property_cache[f]
+        except AttributeError:
+            self._property_cache = {}
+            x = self._property_cache[f] = f(self)
+            return x
+        except KeyError:
+            x = self._property_cache[f] = f(self)
+            return x
+
+    return property(get)
+
+
+def yesno(prompt, default=None):
+    """
+    Prompt the user for a yes or no.
+
+    Can optionally specify a default value, which will only be
+    used if they enter a blank line.
+
+    Unrecognised input (anything other than "y", "n", "yes",
+    "no" or "") will return None.
+    """
+    answer = raw_input(prompt).strip().lower()
+
+    if answer == "y" or answer == "yes":
+        return True
+    elif answer == "n" or answer == "no":
+        return False
+    elif answer == "":
+        return default
+    else:
+        return None
+
+
+# http://stackoverflow.com/a/5164027
+def prettydate(d):
+    diff = datetime.datetime.utcnow() - d
+    s = diff.seconds
+    if diff.days > 7 or diff.days < 0:
+        return d.strftime('%d %b %y')
+    elif diff.days == 1:
+        return '1 day ago'
+    elif diff.days > 1:
+        return '{0} days ago'.format(diff.days)
+    elif s <= 1:
+        return 'just now'
+    elif s < 60:
+        return '{0} seconds ago'.format(s)
+    elif s < 120:
+        return '1 minute ago'
+    elif s < 3600:
+        return '{0} minutes ago'.format(s/60)
+    elif s < 7200:
+        return '1 hour ago'
+    else:
+        return '{0} hours ago'.format(s/3600)
+
+
+def mkdir(path, permissions=0700):
+    if not os.path.exists(path):
+        os.mkdir(path)
+
+    os.chmod(path, permissions)
+
+    return path

+ 8 - 0
plum/service_collection.py

@@ -29,6 +29,14 @@ class ServiceCollection(list):
             collection.append(Service(client=client, links=links, **service_dict))
         return collection
 
+    @classmethod
+    def from_config(cls, client, config):
+        dicts = []
+        for name, service in config.items():
+            service['name'] = name
+            dicts.append(service)
+        return cls.from_dicts(client, dicts)
+
     def get(self, name):
         for service in self:
             if service.name == name:

+ 2 - 0
requirements.txt

@@ -1 +1,3 @@
 git+git://github.com/dotcloud/docker-py.git@4fde1a242e1853cbf83e5a36371d8b4a49501c52
+docopt==0.6.1
+PyYAML==3.10

+ 1 - 1
setup.py

@@ -36,6 +36,6 @@ setup(
     dependency_links=[],
     entry_points="""
     [console_scripts]
-    plum=plum:main
+    plum=plum.cli.main:main
     """,
 )