network.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. from __future__ import absolute_import
  2. from __future__ import unicode_literals
  3. import logging
  4. from docker.errors import NotFound
  5. from docker.types import IPAMConfig
  6. from docker.types import IPAMPool
  7. from docker.utils import version_gte
  8. from docker.utils import version_lt
  9. from .config import ConfigurationError
  10. from .const import LABEL_NETWORK
  11. from .const import LABEL_PROJECT
  12. log = logging.getLogger(__name__)
  13. OPTS_EXCEPTIONS = [
  14. 'com.docker.network.driver.overlay.vxlanid_list',
  15. ]
  16. class Network(object):
  17. def __init__(self, client, project, name, driver=None, driver_opts=None,
  18. ipam=None, external_name=None, internal=False, enable_ipv6=False,
  19. labels=None):
  20. self.client = client
  21. self.project = project
  22. self.name = name
  23. self.driver = driver
  24. self.driver_opts = driver_opts
  25. self.ipam = create_ipam_config_from_dict(ipam)
  26. self.external_name = external_name
  27. self.internal = internal
  28. self.enable_ipv6 = enable_ipv6
  29. self.labels = labels
  30. def ensure(self):
  31. if self.external_name:
  32. try:
  33. self.inspect()
  34. log.debug(
  35. 'Network {0} declared as external. No new '
  36. 'network will be created.'.format(self.name)
  37. )
  38. except NotFound:
  39. raise ConfigurationError(
  40. 'Network {name} declared as external, but could'
  41. ' not be found. Please create the network manually'
  42. ' using `{command} {name}` and try again.'.format(
  43. name=self.external_name,
  44. command='docker network create'
  45. )
  46. )
  47. return
  48. try:
  49. data = self.inspect()
  50. check_remote_network_config(data, self)
  51. except NotFound:
  52. driver_name = 'the default driver'
  53. if self.driver:
  54. driver_name = 'driver "{}"'.format(self.driver)
  55. log.info(
  56. 'Creating network "{}" with {}'
  57. .format(self.full_name, driver_name)
  58. )
  59. self.client.create_network(
  60. name=self.full_name,
  61. driver=self.driver,
  62. options=self.driver_opts,
  63. ipam=self.ipam,
  64. internal=self.internal,
  65. enable_ipv6=self.enable_ipv6,
  66. labels=self._labels,
  67. attachable=version_gte(self.client._version, '1.24') or None,
  68. )
  69. def remove(self):
  70. if self.external_name:
  71. log.info("Network %s is external, skipping", self.full_name)
  72. return
  73. log.info("Removing network {}".format(self.full_name))
  74. self.client.remove_network(self.full_name)
  75. def inspect(self):
  76. return self.client.inspect_network(self.full_name)
  77. @property
  78. def full_name(self):
  79. if self.external_name:
  80. return self.external_name
  81. return '{0}_{1}'.format(self.project, self.name)
  82. @property
  83. def _labels(self):
  84. if version_lt(self.client._version, '1.23'):
  85. return None
  86. labels = self.labels.copy() if self.labels else {}
  87. labels.update({
  88. LABEL_PROJECT: self.project,
  89. LABEL_NETWORK: self.name,
  90. })
  91. return labels
  92. def create_ipam_config_from_dict(ipam_dict):
  93. if not ipam_dict:
  94. return None
  95. return IPAMConfig(
  96. driver=ipam_dict.get('driver'),
  97. pool_configs=[
  98. IPAMPool(
  99. subnet=config.get('subnet'),
  100. iprange=config.get('ip_range'),
  101. gateway=config.get('gateway'),
  102. aux_addresses=config.get('aux_addresses'),
  103. )
  104. for config in ipam_dict.get('config', [])
  105. ],
  106. )
  107. def check_remote_network_config(remote, local):
  108. if local.driver and remote.get('Driver') != local.driver:
  109. raise ConfigurationError(
  110. 'Network "{}" needs to be recreated - driver has changed'
  111. .format(local.full_name)
  112. )
  113. local_opts = local.driver_opts or {}
  114. remote_opts = remote.get('Options') or {}
  115. for k in set.union(set(remote_opts.keys()), set(local_opts.keys())):
  116. if k in OPTS_EXCEPTIONS:
  117. continue
  118. if remote_opts.get(k) != local_opts.get(k):
  119. raise ConfigurationError(
  120. 'Network "{}" needs to be recreated - options have changed'
  121. .format(local.full_name)
  122. )
  123. def build_networks(name, config_data, client):
  124. network_config = config_data.networks or {}
  125. networks = {
  126. network_name: Network(
  127. client=client, project=name, name=network_name,
  128. driver=data.get('driver'),
  129. driver_opts=data.get('driver_opts'),
  130. ipam=data.get('ipam'),
  131. external_name=data.get('external_name'),
  132. internal=data.get('internal'),
  133. enable_ipv6=data.get('enable_ipv6'),
  134. labels=data.get('labels'),
  135. )
  136. for network_name, data in network_config.items()
  137. }
  138. if 'default' not in networks:
  139. networks['default'] = Network(client, name, 'default')
  140. return networks
  141. class ProjectNetworks(object):
  142. def __init__(self, networks, use_networking):
  143. self.networks = networks or {}
  144. self.use_networking = use_networking
  145. @classmethod
  146. def from_services(cls, services, networks, use_networking):
  147. service_networks = {
  148. network: networks.get(network)
  149. for service in services
  150. for network in get_network_names_for_service(service)
  151. }
  152. unused = set(networks) - set(service_networks) - {'default'}
  153. if unused:
  154. log.warn(
  155. "Some networks were defined but are not used by any service: "
  156. "{}".format(", ".join(unused)))
  157. return cls(service_networks, use_networking)
  158. def remove(self):
  159. if not self.use_networking:
  160. return
  161. for network in self.networks.values():
  162. try:
  163. network.remove()
  164. except NotFound:
  165. log.warn("Network %s not found.", network.full_name)
  166. def initialize(self):
  167. if not self.use_networking:
  168. return
  169. for network in self.networks.values():
  170. network.ensure()
  171. def get_network_defs_for_service(service_dict):
  172. if 'network_mode' in service_dict:
  173. return {}
  174. networks = service_dict.get('networks', {'default': None})
  175. return dict(
  176. (net, (config or {}))
  177. for net, config in networks.items()
  178. )
  179. def get_network_names_for_service(service_dict):
  180. return get_network_defs_for_service(service_dict).keys()
  181. def get_networks(service_dict, network_definitions):
  182. networks = {}
  183. for name, netdef in get_network_defs_for_service(service_dict).items():
  184. network = network_definitions.get(name)
  185. if network:
  186. networks[network.full_name] = netdef
  187. else:
  188. raise ConfigurationError(
  189. 'Service "{}" uses an undefined network "{}"'
  190. .format(service_dict['name'], name))
  191. return networks