service.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. from docker.client import APIError
  2. import logging
  3. import re
  4. log = logging.getLogger(__name__)
  5. class BuildError(Exception):
  6. pass
  7. class Service(object):
  8. def __init__(self, name, client=None, links=[], **options):
  9. if not re.match('^[a-zA-Z0-9_]+$', name):
  10. raise ValueError('Invalid name: %s' % name)
  11. if 'image' in options and 'build' in options:
  12. 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)
  13. self.name = name
  14. self.client = client
  15. self.links = links or []
  16. self.options = options
  17. @property
  18. def containers(self):
  19. return list(self.get_containers(all=True))
  20. def get_containers(self, all):
  21. for container in self.client.containers(all=all):
  22. name = get_container_name(container)
  23. if is_valid_name(name) and parse_name(name)[0] == self.name:
  24. yield container
  25. def start(self):
  26. if len(self.containers) == 0:
  27. return self.start_container()
  28. def stop(self):
  29. self.scale(0)
  30. def scale(self, num):
  31. while len(self.containers) < num:
  32. self.start_container()
  33. while len(self.containers) > num:
  34. self.stop_container()
  35. def create_container(self, **override_options):
  36. """
  37. Create a container for this service. If the image doesn't exist, attempt to pull
  38. it.
  39. """
  40. container_options = self._get_container_options(override_options)
  41. try:
  42. return self.client.create_container(**container_options)
  43. except APIError, e:
  44. if e.response.status_code == 404 and e.explanation and 'No such image' in e.explanation:
  45. log.info('Pulling image %s...' % container_options['image'])
  46. self.client.pull(container_options['image'])
  47. return self.client.create_container(**container_options)
  48. raise
  49. def start_container(self, container=None, **override_options):
  50. if container is None:
  51. container = self.create_container(**override_options)
  52. port_bindings = {}
  53. for port in self.options.get('ports', []):
  54. port = unicode(port)
  55. if ':' in port:
  56. internal_port, external_port = port.split(':', 1)
  57. port_bindings[int(internal_port)] = int(external_port)
  58. else:
  59. port_bindings[int(port)] = None
  60. log.info("Starting %s..." % container['Id'])
  61. self.client.start(
  62. container['Id'],
  63. links=self._get_links(),
  64. port_bindings=port_bindings,
  65. )
  66. return container
  67. def stop_container(self):
  68. container = self.containers[-1]
  69. log.info("Stopping and removing %s..." % get_container_name(container))
  70. self.client.kill(container)
  71. self.client.remove_container(container)
  72. def next_container_number(self):
  73. numbers = [parse_name(get_container_name(c))[1] for c in self.containers]
  74. if len(numbers) == 0:
  75. return 1
  76. else:
  77. return max(numbers) + 1
  78. def get_names(self):
  79. return [get_container_name(c) for c in self.containers]
  80. def inspect(self):
  81. return [self.client.inspect_container(c['Id']) for c in self.containers]
  82. def _get_links(self):
  83. links = {}
  84. for service in self.links:
  85. for name in service.get_names():
  86. links[name] = name
  87. return links
  88. def _get_container_options(self, override_options):
  89. keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from']
  90. container_options = dict((k, self.options[k]) for k in keys if k in self.options)
  91. container_options.update(override_options)
  92. number = self.next_container_number()
  93. container_options['name'] = make_name(self.name, number)
  94. if 'ports' in container_options:
  95. container_options['ports'] = [unicode(p).split(':')[0] for p in container_options['ports']]
  96. if 'build' in self.options:
  97. container_options['image'] = self.build()
  98. return container_options
  99. def build(self):
  100. log.info('Building %s from %s...' % (self.name, self.options['build']))
  101. build_output = self.client.build(self.options['build'], stream=True)
  102. image_id = None
  103. for line in build_output:
  104. if line:
  105. match = re.search(r'Successfully built ([0-9a-f]+)', line)
  106. if match:
  107. image_id = match.group(1)
  108. print line
  109. if image_id is None:
  110. raise BuildError()
  111. return image_id
  112. name_regex = '^(.+)_(\d+)$'
  113. def make_name(prefix, number):
  114. return '%s_%s' % (prefix, number)
  115. def is_valid_name(name):
  116. return (re.match(name_regex, name) is not None)
  117. def parse_name(name):
  118. match = re.match(name_regex, name)
  119. (service_name, suffix) = match.groups()
  120. return (service_name, int(suffix))
  121. def get_container_name(container):
  122. # inspect
  123. if 'Name' in container:
  124. return container['Name']
  125. # ps
  126. for name in container['Names']:
  127. if len(name.split('/')) == 2:
  128. return name[1:]