network.py 11 KB

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