network.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  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 = None
  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. self._set_legacy_flag()
  62. try:
  63. data = self.inspect(legacy=self.legacy)
  64. check_remote_network_config(data, self)
  65. except NotFound:
  66. driver_name = 'the default driver'
  67. if self.driver:
  68. driver_name = 'driver "{}"'.format(self.driver)
  69. log.info(
  70. 'Creating network "{}" with {}'.format(self.full_name, driver_name)
  71. )
  72. self.client.create_network(
  73. name=self.full_name,
  74. driver=self.driver,
  75. options=self.driver_opts,
  76. ipam=self.ipam,
  77. internal=self.internal,
  78. enable_ipv6=self.enable_ipv6,
  79. labels=self._labels,
  80. attachable=version_gte(self.client._version, '1.24') or None,
  81. check_duplicate=True,
  82. )
  83. def remove(self):
  84. if self.external:
  85. log.info("Network %s is external, skipping", self.true_name)
  86. return
  87. log.info("Removing network {}".format(self.true_name))
  88. self.client.remove_network(self.true_name)
  89. def inspect(self, legacy=False):
  90. if legacy:
  91. return self.client.inspect_network(self.legacy_full_name)
  92. return self.client.inspect_network(self.full_name)
  93. @property
  94. def legacy_full_name(self):
  95. if self.custom_name:
  96. return self.name
  97. return '{0}_{1}'.format(
  98. re.sub(r'[_-]', '', self.project), self.name
  99. )
  100. @property
  101. def full_name(self):
  102. if self.custom_name:
  103. return self.name
  104. return '{0}_{1}'.format(self.project, self.name)
  105. @property
  106. def true_name(self):
  107. self._set_legacy_flag()
  108. if self.legacy:
  109. return self.legacy_full_name
  110. return self.full_name
  111. @property
  112. def _labels(self):
  113. if version_lt(self.client._version, '1.23'):
  114. return None
  115. labels = self.labels.copy() if self.labels else {}
  116. labels.update({
  117. LABEL_PROJECT: self.project,
  118. LABEL_NETWORK: self.name,
  119. LABEL_VERSION: __version__,
  120. })
  121. return labels
  122. def _set_legacy_flag(self):
  123. if self.legacy is not None:
  124. return
  125. try:
  126. data = self.inspect(legacy=True)
  127. self.legacy = data is not None
  128. except NotFound:
  129. self.legacy = False
  130. def create_ipam_config_from_dict(ipam_dict):
  131. if not ipam_dict:
  132. return None
  133. return IPAMConfig(
  134. driver=ipam_dict.get('driver') or 'default',
  135. pool_configs=[
  136. IPAMPool(
  137. subnet=config.get('subnet'),
  138. iprange=config.get('ip_range'),
  139. gateway=config.get('gateway'),
  140. aux_addresses=config.get('aux_addresses'),
  141. )
  142. for config in ipam_dict.get('config', [])
  143. ],
  144. options=ipam_dict.get('options')
  145. )
  146. class NetworkConfigChangedError(ConfigurationError):
  147. def __init__(self, net_name, property_name):
  148. super(NetworkConfigChangedError, self).__init__(
  149. 'Network "{}" needs to be recreated - {} has changed'.format(
  150. net_name, property_name
  151. )
  152. )
  153. def check_remote_ipam_config(remote, local):
  154. remote_ipam = remote.get('IPAM')
  155. ipam_dict = create_ipam_config_from_dict(local.ipam)
  156. if local.ipam.get('driver') and local.ipam.get('driver') != remote_ipam.get('Driver'):
  157. raise NetworkConfigChangedError(local.true_name, 'IPAM driver')
  158. if len(ipam_dict['Config']) != 0:
  159. if len(ipam_dict['Config']) != len(remote_ipam['Config']):
  160. raise NetworkConfigChangedError(local.true_name, 'IPAM configs')
  161. remote_configs = sorted(remote_ipam['Config'], key='Subnet')
  162. local_configs = sorted(ipam_dict['Config'], key='Subnet')
  163. while local_configs:
  164. lc = local_configs.pop()
  165. rc = remote_configs.pop()
  166. if lc.get('Subnet') != rc.get('Subnet'):
  167. raise NetworkConfigChangedError(local.true_name, 'IPAM config subnet')
  168. if lc.get('Gateway') is not None and lc.get('Gateway') != rc.get('Gateway'):
  169. raise NetworkConfigChangedError(local.true_name, 'IPAM config gateway')
  170. if lc.get('IPRange') != rc.get('IPRange'):
  171. raise NetworkConfigChangedError(local.true_name, 'IPAM config ip_range')
  172. if sorted(lc.get('AuxiliaryAddresses')) != sorted(rc.get('AuxiliaryAddresses')):
  173. raise NetworkConfigChangedError(local.true_name, 'IPAM config aux_addresses')
  174. remote_opts = remote_ipam.get('Options') or {}
  175. local_opts = local.ipam.get('Options') or {}
  176. for k in set.union(set(remote_opts.keys()), set(local_opts.keys())):
  177. if remote_opts.get(k) != local_opts.get(k):
  178. raise NetworkConfigChangedError(local.true_name, 'IPAM option "{}"'.format(k))
  179. def check_remote_network_config(remote, local):
  180. if local.driver and remote.get('Driver') != local.driver:
  181. raise NetworkConfigChangedError(local.true_name, 'driver')
  182. local_opts = local.driver_opts or {}
  183. remote_opts = remote.get('Options') or {}
  184. for k in set.union(set(remote_opts.keys()), set(local_opts.keys())):
  185. if k in OPTS_EXCEPTIONS:
  186. continue
  187. if remote_opts.get(k) != local_opts.get(k):
  188. raise NetworkConfigChangedError(local.true_name, 'option "{}"'.format(k))
  189. if local.ipam is not None:
  190. check_remote_ipam_config(remote, local)
  191. if local.internal is not None and local.internal != remote.get('Internal', False):
  192. raise NetworkConfigChangedError(local.true_name, 'internal')
  193. if local.enable_ipv6 is not None and local.enable_ipv6 != remote.get('EnableIPv6', False):
  194. raise NetworkConfigChangedError(local.true_name, 'enable_ipv6')
  195. local_labels = local.labels or {}
  196. remote_labels = remote.get('Labels', {})
  197. for k in set.union(set(remote_labels.keys()), set(local_labels.keys())):
  198. if k.startswith('com.docker.'): # We are only interested in user-specified labels
  199. continue
  200. if remote_labels.get(k) != local_labels.get(k):
  201. log.warn(
  202. 'Network {}: label "{}" has changed. It may need to be'
  203. ' recreated.'.format(local.true_name, k)
  204. )
  205. def build_networks(name, config_data, client):
  206. network_config = config_data.networks or {}
  207. networks = {
  208. network_name: Network(
  209. client=client, project=name,
  210. name=data.get('name', network_name),
  211. driver=data.get('driver'),
  212. driver_opts=data.get('driver_opts'),
  213. ipam=data.get('ipam'),
  214. external=bool(data.get('external', False)),
  215. internal=data.get('internal'),
  216. enable_ipv6=data.get('enable_ipv6'),
  217. labels=data.get('labels'),
  218. custom_name=data.get('name') is not None,
  219. )
  220. for network_name, data in network_config.items()
  221. }
  222. if 'default' not in networks:
  223. networks['default'] = Network(client, name, 'default')
  224. return networks
  225. class ProjectNetworks(object):
  226. def __init__(self, networks, use_networking):
  227. self.networks = networks or {}
  228. self.use_networking = use_networking
  229. @classmethod
  230. def from_services(cls, services, networks, use_networking):
  231. service_networks = {
  232. network: networks.get(network)
  233. for service in services
  234. for network in get_network_names_for_service(service)
  235. }
  236. unused = set(networks) - set(service_networks) - {'default'}
  237. if unused:
  238. log.warn(
  239. "Some networks were defined but are not used by any service: "
  240. "{}".format(", ".join(unused)))
  241. return cls(service_networks, use_networking)
  242. def remove(self):
  243. if not self.use_networking:
  244. return
  245. for network in self.networks.values():
  246. try:
  247. network.remove()
  248. except NotFound:
  249. log.warn("Network %s not found.", network.true_name)
  250. def initialize(self):
  251. if not self.use_networking:
  252. return
  253. for network in self.networks.values():
  254. network.ensure()
  255. def get_network_defs_for_service(service_dict):
  256. if 'network_mode' in service_dict:
  257. return {}
  258. networks = service_dict.get('networks', {'default': None})
  259. return dict(
  260. (net, (config or {}))
  261. for net, config in networks.items()
  262. )
  263. def get_network_names_for_service(service_dict):
  264. return get_network_defs_for_service(service_dict).keys()
  265. def get_networks(service_dict, network_definitions):
  266. networks = {}
  267. for name, netdef in get_network_defs_for_service(service_dict).items():
  268. network = network_definitions.get(name)
  269. if network:
  270. networks[network.true_name] = netdef
  271. else:
  272. raise ConfigurationError(
  273. 'Service "{}" uses an undefined network "{}"'
  274. .format(service_dict['name'], name))
  275. if any([v.get('priority') for v in networks.values()]):
  276. return OrderedDict(sorted(
  277. networks.items(),
  278. key=lambda t: t[1].get('priority') or 0, reverse=True
  279. ))
  280. else:
  281. # Ensure Compose will pick a consistent primary network if no
  282. # priority is set
  283. return OrderedDict(sorted(networks.items(), key=lambda t: t[0]))