plum -> fig

This commit is contained in:
Aanand Prasad 2013-12-20 20:28:24 +00:00
commit 0cafdc9c6c
21 changed files with 33 additions and 33 deletions

3
fig/__init__.py Normal file
View file

@ -0,0 +1,3 @@
from .service import Service
__version__ = '1.0.0'

0
fig/cli/__init__.py Normal file
View file

41
fig/cli/colors.py Normal file
View file

@ -0,0 +1,41 @@
NAMES = [
'grey',
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'white'
]
def get_pairs():
for i, name in enumerate(NAMES):
yield(name, str(30 + i))
yield('intense_' + name, str(30 + i) + ';1')
def ansi(code):
return '\033[{0}m'.format(code)
def ansi_color(code, s):
return '{0}{1}{2}'.format(ansi(code), s, ansi(0))
def make_color_fn(code):
return lambda s: ansi_color(code, s)
for (name, code) in get_pairs():
globals()[name] = make_color_fn(code)
def rainbow():
cs = ['cyan', 'yellow', 'green', 'magenta', 'red', 'blue',
'intense_cyan', 'intense_yellow', 'intense_green',
'intense_magenta', 'intense_red', 'intense_blue']
for c in cs:
yield globals()[c]

38
fig/cli/command.py Normal file
View file

@ -0,0 +1,38 @@
from docker import Client
import logging
import os
import re
import yaml
from ..project import Project
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 project(self):
config = yaml.load(open('fig.yml'))
return Project.from_config(self.project_name, config, self.client)
@cached_property
def project_name(self):
project = os.path.basename(os.getcwd())
project = re.sub(r'[^a-zA-Z0-9]', '', project)
if not project:
project = 'default'
return project
@cached_property
def formatter(self):
return Formatter()

52
fig/cli/docopt_command.py Normal file
View file

@ -0,0 +1,52 @@
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 docopt_options(self):
return {'options_first': True}
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, **self.docopt_options())
command = options['COMMAND']
if command is None:
raise SystemExit(getdoc(self))
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
fig/cli/errors.py Normal file
View file

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

15
fig/cli/formatter.py Normal file
View file

@ -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()

66
fig/cli/log_printer.py Normal file
View file

@ -0,0 +1,66 @@
import sys
from itertools import cycle
from .multiplexer import Multiplexer
from . import colors
class LogPrinter(object):
def __init__(self, containers, attach_params=None):
self.containers = containers
self.attach_params = attach_params or {}
self.generators = self._make_log_generators()
def run(self):
mux = Multiplexer(self.generators)
for line in mux.loop():
sys.stdout.write(line)
def _make_log_generators(self):
color_fns = cycle(colors.rainbow())
generators = []
for container in self.containers:
color_fn = color_fns.next()
generators.append(self._make_log_generator(container, color_fn))
return generators
def _make_log_generator(self, container, color_fn):
prefix = color_fn(container.name + " | ")
websocket = self._attach(container)
return (prefix + line for line in split_buffer(read_websocket(websocket), '\n'))
def _attach(self, container):
params = {
'stdin': False,
'stdout': True,
'stderr': True,
'logs': False,
'stream': True,
}
params.update(self.attach_params)
params = dict((name, 1 if value else 0) for (name, value) in params.items())
return container.attach_socket(params=params, ws=True)
def read_websocket(websocket):
while True:
data = websocket.recv()
if data:
yield data
else:
break
def split_buffer(reader, separator):
buffered = ''
for data in reader:
lines = (buffered + data).split(separator)
for line in lines[:-1]:
yield line + separator
if len(lines) > 1:
buffered = lines[-1]
if len(buffered) > 0:
yield buffered

253
fig/cli/main.py Normal file
View file

@ -0,0 +1,253 @@
import logging
import sys
import re
from inspect import getdoc
from .. import __version__
from ..project import NoSuchService
from .command import Command
from .formatter import Formatter
from .log_printer import LogPrinter
from docker.client import APIError
from .errors import UserError
from .docopt_command import NoSuchCommand
from .socketclient import SocketClient
log = logging.getLogger(__name__)
def main():
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter())
console_handler.setLevel(logging.INFO)
root_logger = logging.getLogger()
root_logger.addHandler(console_handler)
root_logger.setLevel(logging.DEBUG)
# Disable requests logging
logging.getLogger("requests").propagate = False
try:
command = TopLevelCommand()
command.sys_dispatch()
except KeyboardInterrupt:
log.error("\nAborting.")
exit(1)
except UserError, e:
log.error(e.msg)
exit(1)
except NoSuchService, 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)
except APIError, e:
log.error(e.explanation)
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:
fig [options] [COMMAND] [ARGS...]
fig -h|--help
Options:
--verbose Show more output
--version Print version and exit
Commands:
up Create and start containers
logs View output from containers
ps List containers
run Run a one-off command
start Start services
stop Stop services
kill Kill containers
rm Remove stopped containers
"""
def docopt_options(self):
options = super(TopLevelCommand, self).docopt_options()
options['version'] = "fig %s" % __version__
return options
def ps(self, options):
"""
List containers.
Usage: ps [options] [SERVICE...]
Options:
-q Only display IDs
"""
containers = self.project.containers(service_names=options['SERVICE'], stopped=True) + self.project.containers(service_names=options['SERVICE'], one_off=True)
if options['-q']:
for container in containers:
print container.id
else:
headers = [
'Name',
'Command',
'State',
'Ports',
]
rows = []
for container in containers:
rows.append([
container.name,
container.human_readable_command,
container.human_readable_state,
container.human_readable_ports,
])
print Formatter().table(headers, rows)
def run(self, options):
"""
Run a one-off command.
Usage: run [options] SERVICE COMMAND [ARGS...]
Options:
-d Detached mode: Run container in the background, print new container name
"""
service = self.project.get_service(options['SERVICE'])
container_options = {
'command': [options['COMMAND']] + options['ARGS'],
'tty': not options['-d'],
'stdin_open': not options['-d'],
}
container = service.create_container(one_off=True, **container_options)
if options['-d']:
service.start_container(container, ports=None)
print container.name
else:
with self._attach_to_container(
container.id,
interactive=True,
logs=True,
raw=True
) as c:
service.start_container(container, ports=None)
c.run()
def up(self, options):
"""
Create and start containers.
Usage: up [options] [SERVICE...]
Options:
-d Detached mode: Run containers in the background, print new container names
"""
detached = options['-d']
unstarted = self.project.create_containers(service_names=options['SERVICE'])
if not detached:
to_attach = self.project.containers(service_names=options['SERVICE']) + [c for (s, c) in unstarted]
print "Attaching to", list_containers(to_attach)
log_printer = LogPrinter(to_attach, attach_params={'logs': True})
for (s, c) in unstarted:
s.start_container(c)
if detached:
for (s, c) in unstarted:
print c.name
else:
try:
log_printer.run()
finally:
self.project.kill_and_remove(unstarted)
def start(self, options):
"""
Start existing containers.
Usage: start [SERVICE...]
"""
self.project.start(service_names=options['SERVICE'])
def stop(self, options):
"""
Stop running containers.
Usage: stop [SERVICE...]
"""
self.project.stop(service_names=options['SERVICE'])
def kill(self, options):
"""
Kill containers.
Usage: kill [SERVICE...]
"""
self.project.kill(service_names=options['SERVICE'])
def rm(self, options):
"""
Remove stopped containers
Usage: rm [SERVICE...]
"""
self.project.remove_stopped(service_names=options['SERVICE'])
def logs(self, options):
"""
View output from containers.
Usage: logs [SERVICE...]
"""
containers = self.project.containers(service_names=options['SERVICE'], stopped=False)
print "Attaching to", list_containers(containers)
LogPrinter(containers, attach_params={'logs': True}).run()
def _attach_to_container(self, container_id, interactive, logs=False, stream=True, raw=False):
stdio = self.client.attach_socket(
container_id,
params={
'stdin': 1 if interactive else 0,
'stdout': 1,
'stderr': 0,
'logs': 1 if logs else 0,
'stream': 1 if stream else 0
},
ws=True,
)
stderr = self.client.attach_socket(
container_id,
params={
'stdin': 0,
'stdout': 0,
'stderr': 1,
'logs': 1 if logs else 0,
'stream': 1 if stream else 0
},
ws=True,
)
return SocketClient(
socket_in=stdio,
socket_out=stdio,
socket_err=stderr,
raw=raw,
)
def list_containers(containers):
return ", ".join(c.name for c in containers)

32
fig/cli/multiplexer.py Normal file
View file

@ -0,0 +1,32 @@
from threading import Thread
try:
from Queue import Queue, Empty
except ImportError:
from queue import Queue, Empty # Python 3.x
class Multiplexer(object):
def __init__(self, generators):
self.generators = generators
self.queue = Queue()
def loop(self):
self._init_readers()
while True:
try:
yield self.queue.get(timeout=0.1)
except Empty:
pass
def _init_readers(self):
for generator in self.generators:
t = Thread(target=_enqueue_output, args=(generator, self.queue))
t.daemon = True
t.start()
def _enqueue_output(generator, queue):
for item in generator:
queue.put(item)

129
fig/cli/socketclient.py Normal file
View file

@ -0,0 +1,129 @@
# Adapted from https://github.com/benthor/remotty/blob/master/socketclient.py
from select import select
import sys
import tty
import fcntl
import os
import termios
import threading
import errno
import logging
log = logging.getLogger(__name__)
class SocketClient:
def __init__(self,
socket_in=None,
socket_out=None,
socket_err=None,
raw=True,
):
self.socket_in = socket_in
self.socket_out = socket_out
self.socket_err = socket_err
self.raw = raw
self.stdin_fileno = sys.stdin.fileno()
def __enter__(self):
self.create()
return self
def __exit__(self, type, value, trace):
self.destroy()
def create(self):
if os.isatty(sys.stdin.fileno()):
self.settings = termios.tcgetattr(sys.stdin.fileno())
else:
self.settings = None
if self.socket_in is not None:
self.set_blocking(sys.stdin, False)
self.set_blocking(sys.stdout, True)
self.set_blocking(sys.stderr, True)
if self.raw:
tty.setraw(sys.stdin.fileno())
def set_blocking(self, file, blocking):
fd = file.fileno()
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
flags = (flags & ~os.O_NONBLOCK) if blocking else (flags | os.O_NONBLOCK)
fcntl.fcntl(fd, fcntl.F_SETFL, flags)
def run(self):
if self.socket_in is not None:
self.start_background_thread(target=self.send_ws, args=(self.socket_in, sys.stdin))
recv_threads = []
if self.socket_out is not None:
recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_out, sys.stdout)))
if self.socket_err is not None:
recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_err, sys.stderr)))
for t in recv_threads:
t.join()
def start_background_thread(self, **kwargs):
thread = threading.Thread(**kwargs)
thread.daemon = True
thread.start()
return thread
def recv_ws(self, socket, stream):
try:
while True:
chunk = socket.recv()
if chunk:
stream.write(chunk)
stream.flush()
else:
break
except Exception, e:
log.debug(e)
def send_ws(self, socket, stream):
while True:
r, w, e = select([stream.fileno()], [], [])
if r:
chunk = stream.read(1)
if chunk == '':
socket.send_close()
break
else:
try:
socket.send(chunk)
except Exception, e:
if hasattr(e, 'errno') and e.errno == errno.EPIPE:
break
else:
raise e
def destroy(self):
if self.settings is not None:
termios.tcsetattr(self.stdin_fileno, termios.TCSADRAIN, self.settings)
sys.stdout.flush()
if __name__ == '__main__':
import websocket
if len(sys.argv) != 2:
sys.stderr.write("Usage: python socketclient.py WEBSOCKET_URL\n")
exit(1)
url = sys.argv[1]
socket = websocket.create_connection(url)
print "connected\r"
with SocketClient(socket, interactive=True) as client:
client.run()

76
fig/cli/utils.py Normal file
View file

@ -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

139
fig/container.py Normal file
View file

@ -0,0 +1,139 @@
import logging
log = logging.getLogger(__name__)
class Container(object):
"""
Represents a Docker container, constructed from the output of
GET /containers/:id:/json.
"""
def __init__(self, client, dictionary, has_been_inspected=False):
self.client = client
self.dictionary = dictionary
self.has_been_inspected = has_been_inspected
@classmethod
def from_ps(cls, client, dictionary, **kwargs):
"""
Construct a container object from the output of GET /containers/json.
"""
new_dictionary = {
'ID': dictionary['Id'],
'Image': dictionary['Image'],
}
for name in dictionary.get('Names', []):
if len(name.split('/')) == 2:
new_dictionary['Name'] = name
return cls(client, new_dictionary, **kwargs)
@classmethod
def from_id(cls, client, id):
return cls(client, client.inspect_container(id))
@classmethod
def create(cls, client, **options):
response = client.create_container(**options)
return cls.from_id(client, response['Id'])
@property
def id(self):
return self.dictionary['ID']
@property
def short_id(self):
return self.id[:10]
@property
def name(self):
return self.dictionary['Name'][1:]
@property
def human_readable_ports(self):
self.inspect_if_not_inspected()
if not self.dictionary['NetworkSettings']['Ports']:
return ''
ports = []
for private, public in self.dictionary['NetworkSettings']['Ports'].items():
if public:
ports.append('%s->%s' % (public[0]['HostPort'], private))
return ', '.join(ports)
@property
def human_readable_state(self):
self.inspect_if_not_inspected()
if self.dictionary['State']['Running']:
if self.dictionary['State']['Ghost']:
return 'Ghost'
else:
return 'Up'
else:
return 'Exit %s' % self.dictionary['State']['ExitCode']
@property
def human_readable_command(self):
self.inspect_if_not_inspected()
return ' '.join(self.dictionary['Config']['Cmd'])
@property
def environment(self):
self.inspect_if_not_inspected()
out = {}
for var in self.dictionary.get('Config', {}).get('Env', []):
k, v = var.split('=', 1)
out[k] = v
return out
@property
def is_running(self):
self.inspect_if_not_inspected()
return self.dictionary['State']['Running']
def start(self, **options):
log.info("Starting %s..." % self.name)
return self.client.start(self.id, **options)
def stop(self, **options):
log.info("Stopping %s..." % self.name)
return self.client.stop(self.id, **options)
def kill(self):
log.info("Killing %s..." % self.name)
return self.client.kill(self.id)
def remove(self):
log.info("Removing %s..." % self.name)
return self.client.remove_container(self.id)
def inspect_if_not_inspected(self):
if not self.has_been_inspected:
self.inspect()
def wait(self):
return self.client.wait(self.id)
def logs(self, *args, **kwargs):
return self.client.logs(self.id, *args, **kwargs)
def inspect(self):
self.dictionary = self.client.inspect_container(self.id)
return self.dictionary
def links(self):
links = []
for container in self.client.containers():
for name in container['Names']:
bits = name.split('/')
if len(bits) > 2 and bits[1] == self.name:
links.append(bits[2])
return links
def attach_socket(self, **kwargs):
return self.client.attach_socket(self.id, **kwargs)
def __repr__(self):
return '<Container: %s>' % self.name
def __eq__(self, other):
if type(self) != type(other):
return False
return self.id == other.id

122
fig/project.py Normal file
View file

@ -0,0 +1,122 @@
from .service import Service
def sort_service_dicts(services):
# Sort in dependency order
def cmp(x, y):
x_deps_y = y['name'] in x.get('links', [])
y_deps_x = x['name'] in y.get('links', [])
if x_deps_y and not y_deps_x:
return 1
elif y_deps_x and not x_deps_y:
return -1
return 0
return sorted(services, cmp=cmp)
class Project(object):
"""
A collection of services.
"""
def __init__(self, name, services, client):
self.name = name
self.services = services
self.client = client
@classmethod
def from_dicts(cls, name, service_dicts, client):
"""
Construct a ServiceCollection from a list of dicts representing services.
"""
project = cls(name, [], client)
for service_dict in sort_service_dicts(service_dicts):
# Reference links by object
links = []
if 'links' in service_dict:
for service_name in service_dict.get('links', []):
links.append(project.get_service(service_name))
del service_dict['links']
project.services.append(Service(client=client, project=name, links=links, **service_dict))
return project
@classmethod
def from_config(cls, name, config, client):
dicts = []
for service_name, service in config.items():
service['name'] = service_name
dicts.append(service)
return cls.from_dicts(name, dicts, client)
def get_service(self, name):
"""
Retrieve a service by name. Raises NoSuchService
if the named service does not exist.
"""
for service in self.services:
if service.name == name:
return service
raise NoSuchService(name)
def get_services(self, service_names=None):
"""
Returns a list of this project's services filtered
by the provided list of names, or all services if
service_names is None or [].
Preserves the original order of self.services.
Raises NoSuchService if any of the named services
do not exist.
"""
if service_names is None or len(service_names) == 0:
return self.services
else:
unsorted = [self.get_service(name) for name in service_names]
return [s for s in self.services if s in unsorted]
def create_containers(self, service_names=None):
"""
Returns a list of (service, container) tuples,
one for each service with no running containers.
"""
containers = []
for service in self.get_services(service_names):
if len(service.containers()) == 0:
containers.append((service, service.create_container()))
return containers
def kill_and_remove(self, tuples):
for (service, container) in tuples:
container.kill()
container.remove()
def start(self, service_names=None, **options):
for service in self.get_services(service_names):
service.start(**options)
def stop(self, service_names=None, **options):
for service in self.get_services(service_names):
service.stop(**options)
def kill(self, service_names=None, **options):
for service in self.get_services(service_names):
service.kill(**options)
def remove_stopped(self, service_names=None, **options):
for service in self.get_services(service_names):
service.remove_stopped(**options)
def containers(self, service_names=None, *args, **kwargs):
l = []
for service in self.get_services(service_names):
for container in service.containers(*args, **kwargs):
l.append(container)
return l
class NoSuchService(Exception):
def __init__(self, name):
self.name = name
self.msg = "No such service: %s" % self.name
def __str__(self):
return self.msg

205
fig/service.py Normal file
View file

@ -0,0 +1,205 @@
from docker.client import APIError
import logging
import re
import os
import sys
from .container import Container
log = logging.getLogger(__name__)
class BuildError(Exception):
pass
class Service(object):
def __init__(self, name, client=None, project='default', links=[], **options):
if not re.match('^[a-zA-Z0-9]+$', name):
raise ValueError('Invalid name: %s' % name)
if not re.match('^[a-zA-Z0-9]+$', project):
raise ValueError('Invalid project: %s' % project)
if 'image' in options and 'build' in options:
raise ValueError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
self.name = name
self.client = client
self.project = project
self.links = links or []
self.options = options
def containers(self, stopped=False, one_off=False):
l = []
for container in self.client.containers(all=stopped):
name = get_container_name(container)
if not name or not is_valid_name(name, one_off):
continue
project, name, number = parse_name(name)
if project == self.project and name == self.name:
l.append(Container.from_ps(self.client, container))
return l
def start(self, **options):
for c in self.containers(stopped=True):
if not c.is_running:
self.start_container(c, **options)
def stop(self, **options):
for c in self.containers():
c.stop(**options)
def kill(self, **options):
for c in self.containers():
c.kill(**options)
def remove_stopped(self, **options):
for c in self.containers(stopped=True):
if not c.is_running:
c.remove(**options)
def create_container(self, one_off=False, **override_options):
"""
Create a container for this service. If the image doesn't exist, attempt to pull
it.
"""
container_options = self._get_container_options(override_options, one_off=one_off)
try:
return Container.create(self.client, **container_options)
except APIError, e:
if e.response.status_code == 404 and e.explanation and 'No such image' in e.explanation:
log.info('Pulling image %s...' % container_options['image'])
self.client.pull(container_options['image'])
return Container.create(self.client, **container_options)
raise
def start_container(self, container=None, **override_options):
if container is None:
container = self.create_container(**override_options)
options = self.options.copy()
options.update(override_options)
port_bindings = {}
if options.get('ports', None) is not None:
for port in options['ports']:
port = unicode(port)
if ':' in port:
internal_port, external_port = port.split(':', 1)
port_bindings[int(internal_port)] = int(external_port)
else:
port_bindings[int(port)] = None
volume_bindings = {}
if options.get('volumes', None) is not None:
for volume in options['volumes']:
external_dir, internal_dir = volume.split(':')
volume_bindings[os.path.abspath(external_dir)] = internal_dir
container.start(
links=self._get_links(),
port_bindings=port_bindings,
binds=volume_bindings,
)
return container
def next_container_name(self, one_off=False):
bits = [self.project, self.name]
if one_off:
bits.append('run')
return '_'.join(bits + [unicode(self.next_container_number(one_off=one_off))])
def next_container_number(self, one_off=False):
numbers = [parse_name(c.name)[2] for c in self.containers(stopped=True, one_off=one_off)]
if len(numbers) == 0:
return 1
else:
return max(numbers) + 1
def _get_links(self):
links = {}
for service in self.links:
for container in service.containers():
links[container.name] = container.name
return links
def _get_container_options(self, override_options, one_off=False):
keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from']
container_options = dict((k, self.options[k]) for k in keys if k in self.options)
container_options.update(override_options)
container_options['name'] = self.next_container_name(one_off)
if 'ports' in container_options:
container_options['ports'] = [unicode(p).split(':')[0] for p in container_options['ports']]
if 'volumes' in container_options:
container_options['volumes'] = dict((v.split(':')[1], {}) for v in container_options['volumes'])
if 'build' in self.options:
if len(self.client.images(name=self._build_tag_name())) == 0:
self.build()
container_options['image'] = self._build_tag_name()
return container_options
def build(self):
log.info('Building %s...' % self.name)
build_output = self.client.build(
self.options['build'],
tag=self._build_tag_name(),
stream=True
)
image_id = None
for line in build_output:
if line:
match = re.search(r'Successfully built ([0-9a-f]+)', line)
if match:
image_id = match.group(1)
sys.stdout.write(line)
if image_id is None:
raise BuildError()
return image_id
def _build_tag_name(self):
"""
The tag to give to images built for this service.
"""
return '%s_%s' % (self.project, self.name)
NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')
def is_valid_name(name, one_off=False):
match = NAME_RE.match(name)
if match is None:
return False
if one_off:
return match.group(3) == 'run_'
else:
return match.group(3) is None
def parse_name(name, one_off=False):
match = NAME_RE.match(name)
(project, service_name, _, suffix) = match.groups()
return (project, service_name, int(suffix))
def get_container_name(container):
if not container.get('Name') and not container.get('Names'):
return None
# inspect
if 'Name' in container:
return container['Name']
# ps
for name in container['Names']:
if len(name.split('/')) == 2:
return name[1:]