network.py 11 KB

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