2014-01-06 03:26:32 +01:00
from __future__ import unicode_literals
from __future__ import absolute_import
2014-01-16 13:56:36 +01:00
from . packages . 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 :
2014-01-15 19:06:49 +01:00
log . info ( " Starting %s ... " % c . name )
2013-12-20 17:22:54 +01:00
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 ( ) :
2014-01-15 19:06:49 +01:00
log . info ( " Stopping %s ... " % c . name )
2013-12-20 17:22:54 +01:00
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 ( ) :
2014-01-15 19:06:49 +01:00
log . info ( " Killing %s ... " % c . name )
2013-12-20 17:53:07 +01:00
c . kill ( * * options )
def remove_stopped ( self , * * options ) :
for c in self . containers ( stopped = True ) :
if not c . is_running :
2014-01-15 19:06:49 +01:00
log . info ( " Removing %s ... " % c . name )
2013-12-20 17:53:07 +01:00
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 )
2014-01-06 03:26:32 +01:00
except APIError as e :
2014-01-06 04:32:06 +01:00
if e . response . status_code == 404 and e . explanation and ' No such image ' in str ( e . explanation ) :
2013-12-18 12:37:51 +01:00
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
2014-01-03 12:18:59 +01:00
def recreate_containers ( self , * * override_options ) :
"""
If a container for this service doesn ' t exist, create one. If there are
2014-01-15 14:06:25 +01:00
any , stop them and create new ones . Does not remove the old containers .
2014-01-03 12:18:59 +01:00
"""
2014-01-15 17:15:46 +01:00
containers = self . containers ( stopped = True )
if len ( containers ) == 0 :
2014-01-15 19:06:49 +01:00
log . info ( " Creating %s ... " % self . next_container_name ( ) )
2014-01-15 14:17:39 +01:00
return ( [ ] , [ self . create_container ( * * override_options ) ] )
2014-01-03 12:18:59 +01:00
else :
2014-01-15 17:15:46 +01:00
old_containers = [ ]
2014-01-03 12:18:59 +01:00
new_containers = [ ]
2014-01-15 17:15:46 +01:00
for c in containers :
2014-01-15 19:06:49 +01:00
log . info ( " Recreating %s ... " % c . name )
2014-01-15 17:15:46 +01:00
( old_container , new_container ) = self . recreate_container ( c , * * override_options )
old_containers . append ( old_container )
new_containers . append ( new_container )
2014-01-15 14:06:25 +01:00
return ( old_containers , new_containers )
2014-01-03 12:18:59 +01:00
2014-01-15 17:15:46 +01:00
def recreate_container ( self , container , * * override_options ) :
if container . is_running :
container . stop ( timeout = 1 )
2014-01-15 18:06:16 +01:00
intermediate_container = Container . create (
self . client ,
image = ' ubuntu ' ,
command = ' echo ' ,
volumes_from = container . id ,
)
intermediate_container . start ( )
intermediate_container . wait ( )
container . remove ( )
2014-01-15 17:15:46 +01:00
options = dict ( override_options )
2014-01-15 18:06:16 +01:00
options [ ' volumes_from ' ] = intermediate_container . id
new_container = self . create_container ( * * options )
2014-01-15 17:15:46 +01:00
2014-01-15 18:06:16 +01:00
return ( intermediate_container , new_container )
2014-01-15 17:15:46 +01:00
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 ' ] :
2014-01-06 03:26:32 +01:00
port = str ( port )
2013-12-19 13:26:58 +01:00
if ' : ' in port :
2014-01-16 02:54:05 +01:00
external_port , internal_port = port . split ( ' : ' , 1 )
2013-12-19 13:26:58 +01:00
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 ' ] :
2014-01-15 13:43:40 +01:00
if ' : ' in volume :
external_dir , internal_dir = volume . split ( ' : ' )
volume_bindings [ os . path . abspath ( external_dir ) ] = internal_dir
2013-12-19 16:53:39 +01:00
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 ' )
2014-01-06 03:26:32 +01:00
return ' _ ' . join ( bits + [ str ( 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 :
2014-01-16 02:54:05 +01:00
ports = [ ]
for port in container_options [ ' ports ' ] :
port = str ( port )
if ' : ' in port :
port = port . split ( ' : ' ) [ - 1 ]
ports . append ( port )
container_options [ ' ports ' ] = ports
2013-12-18 12:14:14 +01:00
2013-12-19 16:53:39 +01:00
if ' volumes ' in container_options :
2014-01-15 13:43:40 +01:00
container_options [ ' volumes ' ] = dict ( ( split_volume ( v ) [ 1 ] , { } ) for v in container_options [ ' volumes ' ] )
2013-12-19 16:53:39 +01:00
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 : ]
2014-01-15 13:43:40 +01:00
def split_volume ( v ) :
"""
If v is of the format EXTERNAL : INTERNAL , returns ( EXTERNAL , INTERNAL ) .
If v is of the format INTERNAL , returns ( None , INTERNAL ) .
"""
if ' : ' in v :
return v . split ( ' : ' , 1 )
else :
return ( None , v )