mirror of https://github.com/docker/compose.git
Add basic CLI
This commit is contained in:
parent
523fb99d79
commit
3b654ad349
|
@ -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()
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
|
||||||
|
class UserError(Exception):
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = dedent(msg).strip()
|
|
@ -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()
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
|
@ -29,6 +29,14 @@ class ServiceCollection(list):
|
||||||
collection.append(Service(client=client, links=links, **service_dict))
|
collection.append(Service(client=client, links=links, **service_dict))
|
||||||
return collection
|
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):
|
def get(self, name):
|
||||||
for service in self:
|
for service in self:
|
||||||
if service.name == name:
|
if service.name == name:
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
git+git://github.com/dotcloud/docker-py.git@4fde1a242e1853cbf83e5a36371d8b4a49501c52
|
git+git://github.com/dotcloud/docker-py.git@4fde1a242e1853cbf83e5a36371d8b4a49501c52
|
||||||
|
docopt==0.6.1
|
||||||
|
PyYAML==3.10
|
||||||
|
|
Loading…
Reference in New Issue