1
0

network.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. from __future__ import absolute_import
  2. from __future__ import unicode_literals
  3. import logging
  4. from collections import OrderedDict
  5. from docker.errors import NotFound
  6. from docker.types import IPAMConfig
  7. from docker.types import IPAMPool
  8. from docker.utils import version_gte
  9. from docker.utils import version_lt
  10. from .config import ConfigurationError
  11. from .const import LABEL_NETWORK
  12. from .const import LABEL_PROJECT
  13. log = logging.getLogger(__name__)
  14. OPTS_EXCEPTIONS = [
  15. 'com.docker.network.driver.overlay.vxlanid_list',
  16. 'com.docker.network.windowsshim.hnsid',
  17. 'com.docker.network.windowsshim.networkname'
  18. ]
  19. class Network(object):
  20. def __init__(self, client, project, name, driver=None, driver_opts=None,
  21. ipam=None, external=False, internal=False, enable_ipv6=False,
  22. labels=None, custom_name=False):
  23. self.client = client
  24. self.project = project
  25. self.name = name
  26. self.driver = driver
  27. self.driver_opts = driver_opts
  28. self.ipam = create_ipam_config_from_dict(ipam)
  29. self.external = external
  30. self.internal = internal
  31. self.enable_ipv6 = enable_ipv6
  32. self.labels = labels
  33. self.custom_name = custom_name
  34. def ensure(self):
  35. if self.external:
  36. if self.driver == 'overlay':
  37. # Swarm nodes do not register overlay networks that were
  38. # created on a different node unless they're in use.
  39. # See docker/compose#4399
  40. return
  41. try:
  42. self.inspect()
  43. log.debug(
  44. 'Network {0} declared as external. No new '
  45. 'network will be created.'.format(self.name)
  46. )
  47. except NotFound:
  48. raise ConfigurationError(
  49. 'Network {name} declared as external, but could'
  50. ' not be found. Please create the network manually'
  51. ' using `{command} {name}` and try again.'.format(
  52. name=self.full_name,
  53. command='docker network create'
  54. )
  55. )
  56. return
  57. try:
  58. data = self.inspect()
  59. check_remote_network_config(data, self)
  60. except NotFound:
  61. driver_name = 'the default driver'
  62. if self.driver:
  63. driver_name = 'driver "{}"'.format(self.driver)
  64. log.info(
  65. 'Creating network "{}" with {}'
  66. .format(self.full_name, driver_name)
  67. )
  68. self.client.create_network(
  69. name=self.full_name,
  70. driver=self.driver,
  71. options=self.driver_opts,
  72. ipam=self.ipam,
  73. internal=self.internal,
  74. enable_ipv6=self.enable_ipv6,
  75. labels=self._labels,
  76. attachable=version_gte(self.client._version, '1.24') or None,
  77. check_duplicate=True,
  78. )
  79. def remove(self):
  80. if self.external:
  81. log.info("Network %s is external, skipping", self.full_name)
  82. return
  83. log.info("Removing network {}".format(self.full_name))
  84. self.client.remove_network(self.full_name)
  85. def inspect(self):
  86. return self.client.inspect_network(self.full_name)
  87. @property
  88. def full_name(self):
  89. if self.custom_name:
  90. return self.name
  91. return '{0}_{1}'.format(self.project, self.name)
  92. @property
  93. def _labels(self):
  94. if version_lt(self.client._version, '1.23'):
  95. return None
  96. labels = self.labels.copy() if self.labels else {}
  97. labels.update({
  98. LABEL_PROJECT: self.project,
  99. LABEL_NETWORK: self.name,
  100. })
  101. return labels
  102. def create_ipam_config_from_dict(ipam_dict):
  103. if not ipam_dict:
  104. return None
  105. return IPAMConfig(
  106. driver=ipam_dict.get('driver') or 'default',
  107. pool_configs=[
  108. IPAMPool(
  109. subnet=config.get('subnet'),
  110. iprange=config.get('ip_range'),
  111. gateway=config.get('gateway'),
  112. aux_addresses=config.get('aux_addresses'),
  113. )
  114. for config in ipam_dict.get('config', [])
  115. ],
  116. options=ipam_dict.get('options')
  117. )
  118. class NetworkConfigChangedError(ConfigurationError):
  119. def __init__(self, net_name, property_name):
  120. super(NetworkConfigChangedError, self).__init__(
  121. 'Network "{}" needs to be recreated - {} has changed'.format(
  122. net_name, property_name
  123. )
  124. )
  125. def check_remote_ipam_config(remote, local):
  126. remote_ipam = remote.get('IPAM')
  127. ipam_dict = create_ipam_config_from_dict(local.ipam)
  128. if local.ipam.get('driver') and local.ipam.get('driver') != remote_ipam.get('Driver'):
  129. raise NetworkConfigChangedError(local.full_name, 'IPAM driver')
  130. if len(ipam_dict['Config']) != 0:
  131. if len(ipam_dict['Config']) != len(remote_ipam['Config']):
  132. raise NetworkConfigChangedError(local.full_name, 'IPAM configs')
  133. remote_configs = sorted(remote_ipam['Config'], key='Subnet')
  134. local_configs = sorted(ipam_dict['Config'], key='Subnet')
  135. while local_configs:
  136. lc = local_configs.pop()
  137. rc = remote_configs.pop()
  138. if lc.get('Subnet') != rc.get('Subnet'):
  139. raise NetworkConfigChangedError(local.full_name, 'IPAM config subnet')
  140. if lc.get('Gateway') is not None and lc.get('Gateway') != rc.get('Gateway'):
  141. raise NetworkConfigChangedError(local.full_name, 'IPAM config gateway')
  142. if lc.get('IPRange') != rc.get('IPRange'):
  143. raise NetworkConfigChangedError(local.full_name, 'IPAM config ip_range')
  144. if sorted(lc.get('AuxiliaryAddresses')) != sorted(rc.get('AuxiliaryAddresses')):
  145. raise NetworkConfigChangedError(local.full_name, 'IPAM config aux_addresses')
  146. remote_opts = remote_ipam.get('Options') or {}
  147. local_opts = local.ipam.get('options') or {}
  148. for k in set.union(set(remote_opts.keys()), set(local_opts.keys())):
  149. if remote_opts.get(k) != local_opts.get(k):
  150. raise NetworkConfigChangedError(local.full_name, 'IPAM option "{}"'.format(k))
  151. def check_remote_network_config(remote, local):
  152. if local.driver and remote.get('Driver') != local.driver:
  153. raise NetworkConfigChangedError(local.full_name, 'driver')
  154. local_opts = local.driver_opts or {}
  155. remote_opts = remote.get('Options') or {}
  156. for k in set.union(set(remote_opts.keys()), set(local_opts.keys())):
  157. if k in OPTS_EXCEPTIONS:
  158. continue
  159. if remote_opts.get(k) != local_opts.get(k):
  160. raise NetworkConfigChangedError(local.full_name, 'option "{}"'.format(k))
  161. if local.ipam is not None:
  162. check_remote_ipam_config(remote, local)
  163. if local.internal is not None and local.internal != remote.get('Internal', False):
  164. raise NetworkConfigChangedError(local.full_name, 'internal')
  165. if local.enable_ipv6 is not None and local.enable_ipv6 != remote.get('EnableIPv6', False):
  166. raise NetworkConfigChangedError(local.full_name, 'enable_ipv6')
  167. local_labels = local.labels or {}
  168. remote_labels = remote.get('Labels', {})
  169. for k in set.union(set(remote_labels.keys()), set(local_labels.keys())):
  170. if k.startswith('com.docker.'): # We are only interested in user-specified labels
  171. continue
  172. if remote_labels.get(k) != local_labels.get(k):
  173. log.warn(
  174. 'Network {}: label "{}" has changed. It may need to be'
  175. ' recreated.'.format(local.full_name, k)
  176. )
  177. def build_networks(name, config_data, client):
  178. network_config = config_data.networks or {}
  179. networks = {
  180. network_name: Network(
  181. client=client, project=name,
  182. name=data.get('name', network_name),
  183. driver=data.get('driver'),
  184. driver_opts=data.get('driver_opts'),
  185. ipam=data.get('ipam'),
  186. external=bool(data.get('external', False)),
  187. internal=data.get('internal'),
  188. enable_ipv6=data.get('enable_ipv6'),
  189. labels=data.get('labels'),
  190. custom_name=data.get('name') is not None,
  191. )
  192. for network_name, data in network_config.items()
  193. }
  194. if 'default' not in networks:
  195. networks['default'] = Network(client, name, 'default')
  196. return networks
  197. class ProjectNetworks(object):
  198. def __init__(self, networks, use_networking):
  199. self.networks = networks or {}
  200. self.use_networking = use_networking
  201. @classmethod
  202. def from_services(cls, services, networks, use_networking):
  203. service_networks = {
  204. network: networks.get(network)
  205. for service in services
  206. for network in get_network_names_for_service(service)
  207. }
  208. unused = set(networks) - set(service_networks) - {'default'}
  209. if unused:
  210. log.warn(
  211. "Some networks were defined but are not used by any service: "
  212. "{}".format(", ".join(unused)))
  213. return cls(service_networks, use_networking)
  214. def remove(self):
  215. if not self.use_networking:
  216. return
  217. for network in self.networks.values():
  218. try:
  219. network.remove()
  220. except NotFound:
  221. log.warn("Network %s not found.", network.full_name)
  222. def initialize(self):
  223. if not self.use_networking:
  224. return
  225. for network in self.networks.values():
  226. network.ensure()
  227. def get_network_defs_for_service(service_dict):
  228. if 'network_mode' in service_dict:
  229. return {}
  230. networks = service_dict.get('networks', {'default': None})
  231. return dict(
  232. (net, (config or {}))
  233. for net, config in networks.items()
  234. )
  235. def get_network_names_for_service(service_dict):
  236. return get_network_defs_for_service(service_dict).keys()
  237. def get_networks(service_dict, network_definitions):
  238. networks = {}
  239. for name, netdef in get_network_defs_for_service(service_dict).items():
  240. network = network_definitions.get(name)
  241. if network:
  242. networks[network.full_name] = netdef
  243. else:
  244. raise ConfigurationError(
  245. 'Service "{}" uses an undefined network "{}"'
  246. .format(service_dict['name'], name))
  247. return OrderedDict(sorted(
  248. networks.items(),
  249. key=lambda t: t[1].get('priority') or 0, reverse=True
  250. ))