2013-12-18 12:37:51 +01:00
from docker . client import APIError
2013-12-16 11:51:22 +01:00
import logging
2013-12-09 13:19:27 +01:00
import re
2013-12-19 16:53:39 +01:00
import os
2013-12-18 19:45:25 +01:00
import sys
2013-12-18 19:37:48 +01:00
from . container import Container
2013-12-09 13:19:27 +01:00
2013-12-16 11:51:22 +01:00
log = logging . getLogger ( __name__ )
2013-12-09 13:19:27 +01:00
2013-12-18 17:12:53 +01:00
class BuildError ( Exception ) :
pass
2013-12-09 12:41:05 +01:00
class Service ( object ) :
2013-12-19 16:16:17 +01:00
def __init__ ( self , name , client = None , project = ' default ' , links = [ ] , * * options ) :
2013-12-19 15:47:43 +01:00
if not re . match ( ' ^[a-zA-Z0-9]+$ ' , name ) :
2013-12-09 13:19:27 +01:00
raise ValueError ( ' Invalid name: %s ' % name )
2013-12-19 16:16:17 +01:00
if not re . match ( ' ^[a-zA-Z0-9]+$ ' , project ) :
raise ValueError ( ' Invalid project: %s ' % project )
2013-12-13 21:36:10 +01:00
if ' image ' in options and ' build ' in options :
2013-12-18 12:15:59 +01:00
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 )
2013-12-09 13:19:27 +01:00
self . name = name
2013-12-09 12:41:05 +01:00
self . client = client
2013-12-19 16:16:17 +01:00
self . project = project
2013-12-09 15:09:18 +01:00
self . links = links or [ ]
2013-12-10 21:51:55 +01:00
self . options = options
2013-12-09 12:41:05 +01:00
2013-12-20 11:46:55 +01:00
def containers ( self , stopped = False , one_off = False ) :
2013-12-18 19:37:48 +01:00
l = [ ]
2013-12-20 11:46:55 +01:00
for container in self . client . containers ( all = stopped ) :
2013-12-18 15:54:28 +01:00
name = get_container_name ( container )
2013-12-20 13:51:20 +01:00
if not name or not is_valid_name ( name , one_off ) :
2013-12-19 16:16:17 +01:00
continue
project , name , number = parse_name ( name )
if project == self . project and name == self . name :
2013-12-18 19:37:48 +01:00
l . append ( Container . from_ps ( self . client , container ) )
return l
2013-12-09 12:41:05 +01:00
2013-12-20 17:22:54 +01:00
def start ( self , * * options ) :
for c in self . containers ( stopped = True ) :
if not c . is_running :
self . start_container ( c , * * options )
2013-12-09 12:41:05 +01:00
2013-12-20 17:22:54 +01:00
def stop ( self , * * options ) :
for c in self . containers ( ) :
c . stop ( * * options )
2013-12-09 12:41:05 +01:00
2013-12-20 17:53:07 +01:00
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 )
2013-12-20 11:46:55 +01:00
def create_container ( self , one_off = False , * * override_options ) :
2013-12-18 12:37:51 +01:00
"""
Create a container for this service . If the image doesn ' t exist, attempt to pull
it .
"""
2013-12-20 11:46:55 +01:00
container_options = self . _get_container_options ( override_options , one_off = one_off )
2013-12-18 12:37:51 +01:00
try :
2013-12-18 19:37:48 +01:00
return Container . create ( self . client , * * container_options )
2013-12-18 12:37:51 +01:00
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 ' ] )
2013-12-18 19:37:48 +01:00
return Container . create ( self . client , * * container_options )
2013-12-18 12:37:51 +01:00
raise
2013-12-17 15:13:12 +01:00
def start_container ( self , container = None , * * override_options ) :
if container is None :
container = self . create_container ( * * override_options )
2013-12-19 13:26:58 +01:00
options = self . options . copy ( )
options . update ( override_options )
2013-12-16 12:22:54 +01:00
port_bindings = { }
2013-12-19 13:26:58 +01:00
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
2013-12-19 16:53:39 +01:00
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
2013-12-18 19:37:48 +01:00
container . start (
2013-12-09 22:39:11 +01:00
links = self . _get_links ( ) ,
2013-12-16 12:22:54 +01:00
port_bindings = port_bindings ,
2013-12-19 16:53:39 +01:00
binds = volume_bindings ,
2013-12-09 22:39:11 +01:00
)
2013-12-17 15:13:12 +01:00
return container
2013-12-09 12:41:05 +01:00
2013-12-20 11:46:55 +01:00
def next_container_name ( self , one_off = False ) :
bits = [ self . project , self . name ]
if one_off :
bits . append ( ' run ' )
2013-12-20 13:55:45 +01:00
return ' _ ' . join ( bits + [ unicode ( self . next_container_number ( one_off = one_off ) ) ] )
2013-12-19 16:16:17 +01:00
2013-12-20 13:55:45 +01:00
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 ) ]
2013-12-09 16:00:41 +01:00
if len ( numbers ) == 0 :
return 1
else :
return max ( numbers ) + 1
2013-12-09 22:39:11 +01:00
def _get_links ( self ) :
links = { }
for service in self . links :
2013-12-18 19:37:48 +01:00
for container in service . containers ( ) :
2013-12-20 20:33:41 +01:00
links [ container . name ] = container . name
2013-12-09 22:39:11 +01:00
return links
2013-12-20 11:46:55 +01:00
def _get_container_options ( self , override_options , one_off = False ) :
2013-12-13 21:36:10 +01:00
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 )
2013-12-20 11:46:55 +01:00
container_options [ ' name ' ] = self . next_container_name ( one_off )
2013-12-13 21:36:10 +01:00
2013-12-18 12:14:14 +01:00
if ' ports ' in container_options :
container_options [ ' ports ' ] = [ unicode ( p ) . split ( ' : ' ) [ 0 ] for p in container_options [ ' ports ' ] ]
2013-12-19 16:53:39 +01:00
if ' volumes ' in container_options :
container_options [ ' volumes ' ] = dict ( ( v . split ( ' : ' ) [ 1 ] , { } ) for v in container_options [ ' volumes ' ] )
2014-01-02 16:28:33 +01:00
if self . can_be_built ( ) :
2013-12-20 17:23:40 +01:00
if len ( self . client . images ( name = self . _build_tag_name ( ) ) ) == 0 :
self . build ( )
container_options [ ' image ' ] = self . _build_tag_name ( )
2013-12-13 21:36:10 +01:00
return container_options
2013-12-09 22:39:11 +01:00
2013-12-18 17:12:53 +01:00
def build ( self ) :
2013-12-18 19:46:53 +01:00
log . info ( ' Building %s ... ' % self . name )
2013-12-18 17:12:53 +01:00
2013-12-20 17:23:40 +01:00
build_output = self . client . build (
self . options [ ' build ' ] ,
tag = self . _build_tag_name ( ) ,
stream = True
)
2013-12-18 17:12:53 +01:00
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 )
2013-12-18 19:45:25 +01:00
sys . stdout . write ( line )
2013-12-18 17:12:53 +01:00
if image_id is None :
raise BuildError ( )
return image_id
2014-01-02 16:28:33 +01:00
def can_be_built ( self ) :
return ' build ' in self . options
2013-12-20 17:23:40 +01:00
def _build_tag_name ( self ) :
"""
The tag to give to images built for this service .
"""
return ' %s _ %s ' % ( self . project , self . name )
2013-12-09 16:00:41 +01:00
2013-12-20 11:46:55 +01:00
NAME_RE = re . compile ( r ' ^([^_]+)_([^_]+)_(run_)?( \ d+)$ ' )
2013-12-09 16:00:41 +01:00
2013-12-20 11:46:55 +01:00
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
2013-12-09 16:00:41 +01:00
2013-12-20 11:46:55 +01:00
def parse_name ( name , one_off = False ) :
2013-12-19 21:09:54 +01:00
match = NAME_RE . match ( name )
2013-12-20 11:46:55 +01:00
( project , service_name , _ , suffix ) = match . groups ( )
2013-12-19 16:16:17 +01:00
return ( project , service_name , int ( suffix ) )
2013-12-09 16:00:41 +01:00
def get_container_name ( container ) :
2013-12-20 13:51:20 +01:00
if not container . get ( ' Name ' ) and not container . get ( ' Names ' ) :
return None
2013-12-18 17:12:53 +01:00
# inspect
if ' Name ' in container :
return container [ ' Name ' ]
# ps
2013-12-17 13:12:13 +01:00
for name in container [ ' Names ' ] :
if len ( name . split ( ' / ' ) ) == 2 :
return name [ 1 : ]