network.py 11 KB

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