test_docker_network.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. # -*- coding: utf-8 -*-
  2. """
  3. Integration tests for the docker_network states
  4. """
  5. from __future__ import absolute_import, print_function, unicode_literals
  6. import errno
  7. import functools
  8. import logging
  9. import os
  10. import subprocess
  11. import tempfile
  12. import pytest
  13. import salt.utils.files
  14. import salt.utils.network
  15. import salt.utils.path
  16. from salt.exceptions import CommandExecutionError
  17. from tests.support.case import ModuleCase
  18. from tests.support.docker import random_name, with_network
  19. from tests.support.helpers import requires_system_grains
  20. from tests.support.mixins import SaltReturnAssertsMixin
  21. from tests.support.runtests import RUNTIME_VARS
  22. from tests.support.unit import skipIf
  23. log = logging.getLogger(__name__)
  24. IMAGE_NAME = random_name(prefix="salt_busybox_")
  25. IPV6_ENABLED = bool(salt.utils.network.ip_addrs6(include_loopback=True))
  26. def network_name(func):
  27. """
  28. Generate a randomized name for a network and clean it up afterward
  29. """
  30. @functools.wraps(func)
  31. def wrapper(self, *args, **kwargs):
  32. name = random_name(prefix="salt_net_")
  33. try:
  34. return func(self, name, *args, **kwargs)
  35. finally:
  36. self.run_function("docker.disconnect_all_containers_from_network", [name])
  37. try:
  38. self.run_function("docker.remove_network", [name])
  39. except CommandExecutionError as exc:
  40. if "No such network" not in exc.__str__():
  41. raise
  42. return wrapper
  43. def container_name(func):
  44. """
  45. Generate a randomized name for a container and clean it up afterward
  46. """
  47. def build_image():
  48. # Create temp dir
  49. image_build_rootdir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  50. script_path = os.path.join(RUNTIME_VARS.BASE_FILES, "mkimage-busybox-static")
  51. cmd = [script_path, image_build_rootdir, IMAGE_NAME]
  52. log.debug("Running '%s' to build busybox image", " ".join(cmd))
  53. process = subprocess.Popen(
  54. cmd, close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
  55. )
  56. output = process.communicate()[0]
  57. log.debug("Output from mkimge-busybox-static:\n%s", output)
  58. if process.returncode != 0:
  59. raise Exception("Failed to build image")
  60. try:
  61. salt.utils.files.rm_rf(image_build_rootdir)
  62. except OSError as exc:
  63. if exc.errno != errno.ENOENT:
  64. raise
  65. @functools.wraps(func)
  66. def wrapper(self, *args, **kwargs):
  67. try:
  68. self.run_function("docker.inspect_image", [IMAGE_NAME])
  69. except CommandExecutionError:
  70. pass
  71. else:
  72. build_image()
  73. name = random_name(prefix="salt_test_")
  74. self.run_function(
  75. "docker.create",
  76. name=name,
  77. image=IMAGE_NAME,
  78. command="sleep 600",
  79. start=True,
  80. )
  81. try:
  82. return func(self, name, *args, **kwargs)
  83. finally:
  84. try:
  85. self.run_function("docker.rm", [name], force=True)
  86. except CommandExecutionError as exc:
  87. if "No such container" not in exc.__str__():
  88. raise
  89. return wrapper
  90. @pytest.mark.destructive_test
  91. @skipIf(not salt.utils.path.which("dockerd"), "Docker not installed")
  92. @pytest.mark.slow_test(seconds=10) # The whole test class needs to run.
  93. class DockerNetworkTestCase(ModuleCase, SaltReturnAssertsMixin):
  94. """
  95. Test docker_network states
  96. """
  97. @classmethod
  98. def tearDownClass(cls):
  99. """
  100. Remove test image if present. Note that this will run a docker rmi even
  101. if no test which required the image was run.
  102. """
  103. cmd = ["docker", "rmi", "--force", IMAGE_NAME]
  104. log.debug("Running '%s' to destroy busybox image", " ".join(cmd))
  105. process = subprocess.Popen(
  106. cmd, close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
  107. )
  108. output = process.communicate()[0]
  109. log.debug("Output from %s:\n%s", " ".join(cmd), output)
  110. if process.returncode != 0 and "No such image" not in output:
  111. raise Exception("Failed to destroy image")
  112. def run_state(self, function, **kwargs):
  113. ret = super(DockerNetworkTestCase, self).run_state(function, **kwargs)
  114. log.debug("ret = %s", ret)
  115. return ret
  116. @with_network(create=False)
  117. def test_absent(self, net):
  118. self.assertSaltTrueReturn(
  119. self.run_state("docker_network.present", name=net.name)
  120. )
  121. ret = self.run_state("docker_network.absent", name=net.name)
  122. self.assertSaltTrueReturn(ret)
  123. ret = ret[next(iter(ret))]
  124. self.assertEqual(ret["changes"], {"removed": True})
  125. self.assertEqual(ret["comment"], "Removed network '{0}'".format(net.name))
  126. @container_name
  127. @with_network(create=False)
  128. def test_absent_with_disconnected_container(self, net, container_name):
  129. self.assertSaltTrueReturn(
  130. self.run_state(
  131. "docker_network.present", name=net.name, containers=[container_name]
  132. )
  133. )
  134. ret = self.run_state("docker_network.absent", name=net.name)
  135. self.assertSaltTrueReturn(ret)
  136. ret = ret[next(iter(ret))]
  137. self.assertEqual(
  138. ret["changes"], {"removed": True, "disconnected": [container_name]}
  139. )
  140. self.assertEqual(ret["comment"], "Removed network '{0}'".format(net.name))
  141. @with_network(create=False)
  142. def test_absent_when_not_present(self, net):
  143. ret = self.run_state("docker_network.absent", name=net.name)
  144. self.assertSaltTrueReturn(ret)
  145. ret = ret[next(iter(ret))]
  146. self.assertEqual(ret["changes"], {})
  147. self.assertEqual(
  148. ret["comment"], "Network '{0}' already absent".format(net.name)
  149. )
  150. @with_network(create=False)
  151. def test_present(self, net):
  152. ret = self.run_state("docker_network.present", name=net.name)
  153. self.assertSaltTrueReturn(ret)
  154. ret = ret[next(iter(ret))]
  155. # Make sure the state return is what we expect
  156. self.assertEqual(ret["changes"], {"created": True})
  157. self.assertEqual(ret["comment"], "Network '{0}' created".format(net.name))
  158. # Now check to see that the network actually exists. If it doesn't,
  159. # this next function call will raise an exception.
  160. self.run_function("docker.inspect_network", [net.name])
  161. @container_name
  162. @with_network(create=False)
  163. def test_present_with_containers(self, net, container_name):
  164. ret = self.run_state(
  165. "docker_network.present", name=net.name, containers=[container_name]
  166. )
  167. self.assertSaltTrueReturn(ret)
  168. ret = ret[next(iter(ret))]
  169. self.assertEqual(
  170. ret["changes"], {"created": True, "connected": [container_name]}
  171. )
  172. self.assertEqual(ret["comment"], "Network '{0}' created".format(net.name))
  173. # Now check to see that the network actually exists. If it doesn't,
  174. # this next function call will raise an exception.
  175. self.run_function("docker.inspect_network", [net.name])
  176. def _test_present_reconnect(self, net, container_name, reconnect=True):
  177. ret = self.run_state("docker_network.present", name=net.name, driver="bridge")
  178. self.assertSaltTrueReturn(ret)
  179. ret = ret[next(iter(ret))]
  180. self.assertEqual(ret["changes"], {"created": True})
  181. self.assertEqual(ret["comment"], "Network '{0}' created".format(net.name))
  182. # Connect the container
  183. self.run_function(
  184. "docker.connect_container_to_network", [container_name, net.name]
  185. )
  186. # Change the driver to force the network to be replaced
  187. ret = self.run_state(
  188. "docker_network.present",
  189. name=net.name,
  190. driver="macvlan",
  191. reconnect=reconnect,
  192. )
  193. self.assertSaltTrueReturn(ret)
  194. ret = ret[next(iter(ret))]
  195. self.assertEqual(
  196. ret["changes"],
  197. {
  198. "recreated": True,
  199. "reconnected" if reconnect else "disconnected": [container_name],
  200. net.name: {"Driver": {"old": "bridge", "new": "macvlan"}},
  201. },
  202. )
  203. self.assertEqual(
  204. ret["comment"],
  205. "Network '{0}' was replaced with updated config".format(net.name),
  206. )
  207. @container_name
  208. @with_network(create=False)
  209. def test_present_with_reconnect(self, net, container_name):
  210. """
  211. Test reconnecting with containers not passed to state
  212. """
  213. self._test_present_reconnect(net, container_name, reconnect=True)
  214. @container_name
  215. @with_network(create=False)
  216. def test_present_with_no_reconnect(self, net, container_name):
  217. """
  218. Test reconnecting with containers not passed to state
  219. """
  220. self._test_present_reconnect(net, container_name, reconnect=False)
  221. @with_network()
  222. def test_present_internal(self, net):
  223. self.assertSaltTrueReturn(
  224. self.run_state("docker_network.present", name=net.name, internal=True,)
  225. )
  226. net_info = self.run_function("docker.inspect_network", [net.name])
  227. self.assertIs(net_info["Internal"], True)
  228. @with_network()
  229. def test_present_labels(self, net):
  230. # Test a mix of different ways of specifying labels
  231. self.assertSaltTrueReturn(
  232. self.run_state(
  233. "docker_network.present",
  234. name=net.name,
  235. labels=["foo", "bar=baz", {"hello": "world"}],
  236. )
  237. )
  238. net_info = self.run_function("docker.inspect_network", [net.name])
  239. self.assertEqual(
  240. net_info["Labels"], {"foo": "", "bar": "baz", "hello": "world"},
  241. )
  242. @with_network(subnet="fe3f:2180:26:1::/123")
  243. @with_network(subnet="10.247.197.96/27")
  244. @skipIf(not IPV6_ENABLED, "IPv6 not enabled")
  245. def test_present_enable_ipv6(self, net1, net2):
  246. self.assertSaltTrueReturn(
  247. self.run_state(
  248. "docker_network.present",
  249. name=net1.name,
  250. enable_ipv6=True,
  251. ipam_pools=[{"subnet": net1.subnet}, {"subnet": net2.subnet}],
  252. )
  253. )
  254. net_info = self.run_function("docker.inspect_network", [net1.name])
  255. self.assertIs(net_info["EnableIPv6"], True)
  256. @requires_system_grains
  257. @with_network()
  258. def test_present_attachable(self, net, grains):
  259. if grains["os_family"] == "RedHat" and grains.get("osmajorrelease", 0) <= 7:
  260. self.skipTest("Cannot reliably manage attachable on RHEL <= 7")
  261. self.assertSaltTrueReturn(
  262. self.run_state("docker_network.present", name=net.name, attachable=True,)
  263. )
  264. net_info = self.run_function("docker.inspect_network", [net.name])
  265. self.assertIs(net_info["Attachable"], True)
  266. @skipIf(True, "Skip until we can set up docker swarm testing")
  267. @with_network()
  268. def test_present_scope(self, net):
  269. self.assertSaltTrueReturn(
  270. self.run_state("docker_network.present", name=net.name, scope="global",)
  271. )
  272. net_info = self.run_function("docker.inspect_network", [net.name])
  273. self.assertIs(net_info["Scope"], "global")
  274. @skipIf(True, "Skip until we can set up docker swarm testing")
  275. @with_network()
  276. def test_present_ingress(self, net):
  277. self.assertSaltTrueReturn(
  278. self.run_state("docker_network.present", name=net.name, ingress=True,)
  279. )
  280. net_info = self.run_function("docker.inspect_network", [net.name])
  281. self.assertIs(net_info["Ingress"], True)
  282. @with_network(subnet="10.247.197.128/27")
  283. @with_network(subnet="10.247.197.96/27")
  284. def test_present_with_custom_ipv4(self, net1, net2):
  285. # First run will test passing the IPAM arguments individually
  286. self.assertSaltTrueReturn(
  287. self.run_state(
  288. "docker_network.present",
  289. name=net1.name,
  290. subnet=net1.subnet,
  291. gateway=net1.gateway,
  292. )
  293. )
  294. # Second run will pass them in the ipam_pools argument
  295. ret = self.run_state(
  296. "docker_network.present",
  297. name=net1.name, # We want to keep the same network name
  298. ipam_pools=[{"subnet": net2.subnet, "gateway": net2.gateway}],
  299. )
  300. self.assertSaltTrueReturn(ret)
  301. ret = ret[next(iter(ret))]
  302. # Docker requires there to be IPv4, even when only an IPv6 subnet was
  303. # provided. So, there will be both an IPv4 and IPv6 pool in the
  304. # configuration.
  305. expected = {
  306. "recreated": True,
  307. net1.name: {
  308. "IPAM": {
  309. "Config": {
  310. "old": [{"Subnet": net1.subnet, "Gateway": net1.gateway}],
  311. "new": [{"Subnet": net2.subnet, "Gateway": net2.gateway}],
  312. }
  313. }
  314. },
  315. }
  316. self.assertEqual(ret["changes"], expected)
  317. self.assertEqual(
  318. ret["comment"],
  319. "Network '{0}' was replaced with updated config".format(net1.name),
  320. )
  321. @with_network(subnet="fe3f:2180:26:1::20/123")
  322. @with_network(subnet="fe3f:2180:26:1::/123")
  323. @with_network(subnet="10.247.197.96/27")
  324. @skipIf(not IPV6_ENABLED, "IPv6 not enabled")
  325. def test_present_with_custom_ipv6(self, ipv4_net, ipv6_net1, ipv6_net2):
  326. self.assertSaltTrueReturn(
  327. self.run_state(
  328. "docker_network.present",
  329. name=ipv4_net.name,
  330. enable_ipv6=True,
  331. ipam_pools=[
  332. {"subnet": ipv4_net.subnet, "gateway": ipv4_net.gateway},
  333. {"subnet": ipv6_net1.subnet, "gateway": ipv6_net1.gateway},
  334. ],
  335. )
  336. )
  337. ret = self.run_state(
  338. "docker_network.present",
  339. name=ipv4_net.name, # We want to keep the same network name
  340. enable_ipv6=True,
  341. ipam_pools=[
  342. {"subnet": ipv4_net.subnet, "gateway": ipv4_net.gateway},
  343. {"subnet": ipv6_net2.subnet, "gateway": ipv6_net2.gateway},
  344. ],
  345. )
  346. self.assertSaltTrueReturn(ret)
  347. ret = ret[next(iter(ret))]
  348. # Docker requires there to be IPv4, even when only an IPv6 subnet was
  349. # provided. So, there will be both an IPv4 and IPv6 pool in the
  350. # configuration.
  351. expected = {
  352. "recreated": True,
  353. ipv4_net.name: {
  354. "IPAM": {
  355. "Config": {
  356. "old": [
  357. {"Subnet": ipv4_net.subnet, "Gateway": ipv4_net.gateway},
  358. {"Subnet": ipv6_net1.subnet, "Gateway": ipv6_net1.gateway},
  359. ],
  360. "new": [
  361. {"Subnet": ipv4_net.subnet, "Gateway": ipv4_net.gateway},
  362. {"Subnet": ipv6_net2.subnet, "Gateway": ipv6_net2.gateway},
  363. ],
  364. }
  365. }
  366. },
  367. }
  368. self.assertEqual(ret["changes"], expected)
  369. self.assertEqual(
  370. ret["comment"],
  371. "Network '{0}' was replaced with updated config".format(ipv4_net.name),
  372. )