test_docker_network.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  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. class DockerNetworkTestCase(ModuleCase, SaltReturnAssertsMixin):
  93. """
  94. Test docker_network states
  95. """
  96. @classmethod
  97. def tearDownClass(cls):
  98. """
  99. Remove test image if present. Note that this will run a docker rmi even
  100. if no test which required the image was run.
  101. """
  102. cmd = ["docker", "rmi", "--force", IMAGE_NAME]
  103. log.debug("Running '%s' to destroy busybox image", " ".join(cmd))
  104. process = subprocess.Popen(
  105. cmd, close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
  106. )
  107. output = process.communicate()[0]
  108. log.debug("Output from %s:\n%s", " ".join(cmd), output)
  109. if process.returncode != 0 and "No such image" not in output:
  110. raise Exception("Failed to destroy image")
  111. def run_state(self, function, **kwargs):
  112. ret = super(DockerNetworkTestCase, self).run_state(function, **kwargs)
  113. log.debug("ret = %s", ret)
  114. return ret
  115. @with_network(create=False)
  116. @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
  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. @pytest.mark.slow_test(seconds=10) # Test takes >5 and <=10 seconds
  129. def test_absent_with_disconnected_container(self, net, container_name):
  130. self.assertSaltTrueReturn(
  131. self.run_state(
  132. "docker_network.present", name=net.name, containers=[container_name]
  133. )
  134. )
  135. ret = self.run_state("docker_network.absent", name=net.name)
  136. self.assertSaltTrueReturn(ret)
  137. ret = ret[next(iter(ret))]
  138. self.assertEqual(
  139. ret["changes"], {"removed": True, "disconnected": [container_name]}
  140. )
  141. self.assertEqual(ret["comment"], "Removed network '{0}'".format(net.name))
  142. @with_network(create=False)
  143. @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
  144. def test_absent_when_not_present(self, net):
  145. ret = self.run_state("docker_network.absent", name=net.name)
  146. self.assertSaltTrueReturn(ret)
  147. ret = ret[next(iter(ret))]
  148. self.assertEqual(ret["changes"], {})
  149. self.assertEqual(
  150. ret["comment"], "Network '{0}' already absent".format(net.name)
  151. )
  152. @with_network(create=False)
  153. @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
  154. def test_present(self, net):
  155. ret = self.run_state("docker_network.present", name=net.name)
  156. self.assertSaltTrueReturn(ret)
  157. ret = ret[next(iter(ret))]
  158. # Make sure the state return is what we expect
  159. self.assertEqual(ret["changes"], {"created": True})
  160. self.assertEqual(ret["comment"], "Network '{0}' created".format(net.name))
  161. # Now check to see that the network actually exists. If it doesn't,
  162. # this next function call will raise an exception.
  163. self.run_function("docker.inspect_network", [net.name])
  164. @container_name
  165. @with_network(create=False)
  166. @pytest.mark.slow_test(seconds=10) # Test takes >5 and <=10 seconds
  167. def test_present_with_containers(self, net, container_name):
  168. ret = self.run_state(
  169. "docker_network.present", name=net.name, containers=[container_name]
  170. )
  171. self.assertSaltTrueReturn(ret)
  172. ret = ret[next(iter(ret))]
  173. self.assertEqual(
  174. ret["changes"], {"created": True, "connected": [container_name]}
  175. )
  176. self.assertEqual(ret["comment"], "Network '{0}' created".format(net.name))
  177. # Now check to see that the network actually exists. If it doesn't,
  178. # this next function call will raise an exception.
  179. self.run_function("docker.inspect_network", [net.name])
  180. def _test_present_reconnect(self, net, container_name, reconnect=True):
  181. ret = self.run_state("docker_network.present", name=net.name, driver="bridge")
  182. self.assertSaltTrueReturn(ret)
  183. ret = ret[next(iter(ret))]
  184. self.assertEqual(ret["changes"], {"created": True})
  185. self.assertEqual(ret["comment"], "Network '{0}' created".format(net.name))
  186. # Connect the container
  187. self.run_function(
  188. "docker.connect_container_to_network", [container_name, net.name]
  189. )
  190. # Change the driver to force the network to be replaced
  191. ret = self.run_state(
  192. "docker_network.present",
  193. name=net.name,
  194. driver="macvlan",
  195. reconnect=reconnect,
  196. )
  197. self.assertSaltTrueReturn(ret)
  198. ret = ret[next(iter(ret))]
  199. self.assertEqual(
  200. ret["changes"],
  201. {
  202. "recreated": True,
  203. "reconnected" if reconnect else "disconnected": [container_name],
  204. net.name: {"Driver": {"old": "bridge", "new": "macvlan"}},
  205. },
  206. )
  207. self.assertEqual(
  208. ret["comment"],
  209. "Network '{0}' was replaced with updated config".format(net.name),
  210. )
  211. @container_name
  212. @with_network(create=False)
  213. @pytest.mark.slow_test(seconds=10) # Test takes >5 and <=10 seconds
  214. def test_present_with_reconnect(self, net, container_name):
  215. """
  216. Test reconnecting with containers not passed to state
  217. """
  218. self._test_present_reconnect(net, container_name, reconnect=True)
  219. @container_name
  220. @with_network(create=False)
  221. @pytest.mark.slow_test(seconds=10) # Test takes >5 and <=10 seconds
  222. def test_present_with_no_reconnect(self, net, container_name):
  223. """
  224. Test reconnecting with containers not passed to state
  225. """
  226. self._test_present_reconnect(net, container_name, reconnect=False)
  227. @with_network()
  228. @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
  229. def test_present_internal(self, net):
  230. self.assertSaltTrueReturn(
  231. self.run_state("docker_network.present", name=net.name, internal=True,)
  232. )
  233. net_info = self.run_function("docker.inspect_network", [net.name])
  234. self.assertIs(net_info["Internal"], True)
  235. @with_network()
  236. @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
  237. def test_present_labels(self, net):
  238. # Test a mix of different ways of specifying labels
  239. self.assertSaltTrueReturn(
  240. self.run_state(
  241. "docker_network.present",
  242. name=net.name,
  243. labels=["foo", "bar=baz", {"hello": "world"}],
  244. )
  245. )
  246. net_info = self.run_function("docker.inspect_network", [net.name])
  247. self.assertEqual(
  248. net_info["Labels"], {"foo": "", "bar": "baz", "hello": "world"},
  249. )
  250. @with_network(subnet="fe3f:2180:26:1::/123")
  251. @with_network(subnet="10.247.197.96/27")
  252. @skipIf(not IPV6_ENABLED, "IPv6 not enabled")
  253. @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
  254. def test_present_enable_ipv6(self, net1, net2):
  255. self.assertSaltTrueReturn(
  256. self.run_state(
  257. "docker_network.present",
  258. name=net1.name,
  259. enable_ipv6=True,
  260. ipam_pools=[{"subnet": net1.subnet}, {"subnet": net2.subnet}],
  261. )
  262. )
  263. net_info = self.run_function("docker.inspect_network", [net1.name])
  264. self.assertIs(net_info["EnableIPv6"], True)
  265. @requires_system_grains
  266. @with_network()
  267. @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
  268. def test_present_attachable(self, net, grains):
  269. if grains["os_family"] == "RedHat" and grains.get("osmajorrelease", 0) <= 7:
  270. self.skipTest("Cannot reliably manage attachable on RHEL <= 7")
  271. self.assertSaltTrueReturn(
  272. self.run_state("docker_network.present", name=net.name, attachable=True,)
  273. )
  274. net_info = self.run_function("docker.inspect_network", [net.name])
  275. self.assertIs(net_info["Attachable"], True)
  276. @skipIf(True, "Skip until we can set up docker swarm testing")
  277. @with_network()
  278. def test_present_scope(self, net):
  279. self.assertSaltTrueReturn(
  280. self.run_state("docker_network.present", name=net.name, scope="global",)
  281. )
  282. net_info = self.run_function("docker.inspect_network", [net.name])
  283. self.assertIs(net_info["Scope"], "global")
  284. @skipIf(True, "Skip until we can set up docker swarm testing")
  285. @with_network()
  286. def test_present_ingress(self, net):
  287. self.assertSaltTrueReturn(
  288. self.run_state("docker_network.present", name=net.name, ingress=True,)
  289. )
  290. net_info = self.run_function("docker.inspect_network", [net.name])
  291. self.assertIs(net_info["Ingress"], True)
  292. @with_network(subnet="10.247.197.128/27")
  293. @with_network(subnet="10.247.197.96/27")
  294. @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
  295. def test_present_with_custom_ipv4(self, net1, net2):
  296. # First run will test passing the IPAM arguments individually
  297. self.assertSaltTrueReturn(
  298. self.run_state(
  299. "docker_network.present",
  300. name=net1.name,
  301. subnet=net1.subnet,
  302. gateway=net1.gateway,
  303. )
  304. )
  305. # Second run will pass them in the ipam_pools argument
  306. ret = self.run_state(
  307. "docker_network.present",
  308. name=net1.name, # We want to keep the same network name
  309. ipam_pools=[{"subnet": net2.subnet, "gateway": net2.gateway}],
  310. )
  311. self.assertSaltTrueReturn(ret)
  312. ret = ret[next(iter(ret))]
  313. # Docker requires there to be IPv4, even when only an IPv6 subnet was
  314. # provided. So, there will be both an IPv4 and IPv6 pool in the
  315. # configuration.
  316. expected = {
  317. "recreated": True,
  318. net1.name: {
  319. "IPAM": {
  320. "Config": {
  321. "old": [{"Subnet": net1.subnet, "Gateway": net1.gateway}],
  322. "new": [{"Subnet": net2.subnet, "Gateway": net2.gateway}],
  323. }
  324. }
  325. },
  326. }
  327. self.assertEqual(ret["changes"], expected)
  328. self.assertEqual(
  329. ret["comment"],
  330. "Network '{0}' was replaced with updated config".format(net1.name),
  331. )
  332. @with_network(subnet="fe3f:2180:26:1::20/123")
  333. @with_network(subnet="fe3f:2180:26:1::/123")
  334. @with_network(subnet="10.247.197.96/27")
  335. @skipIf(not IPV6_ENABLED, "IPv6 not enabled")
  336. @pytest.mark.slow_test(seconds=5) # Test takes >1 and <=5 seconds
  337. def test_present_with_custom_ipv6(self, ipv4_net, ipv6_net1, ipv6_net2):
  338. self.assertSaltTrueReturn(
  339. self.run_state(
  340. "docker_network.present",
  341. name=ipv4_net.name,
  342. enable_ipv6=True,
  343. ipam_pools=[
  344. {"subnet": ipv4_net.subnet, "gateway": ipv4_net.gateway},
  345. {"subnet": ipv6_net1.subnet, "gateway": ipv6_net1.gateway},
  346. ],
  347. )
  348. )
  349. ret = self.run_state(
  350. "docker_network.present",
  351. name=ipv4_net.name, # We want to keep the same network name
  352. enable_ipv6=True,
  353. ipam_pools=[
  354. {"subnet": ipv4_net.subnet, "gateway": ipv4_net.gateway},
  355. {"subnet": ipv6_net2.subnet, "gateway": ipv6_net2.gateway},
  356. ],
  357. )
  358. self.assertSaltTrueReturn(ret)
  359. ret = ret[next(iter(ret))]
  360. # Docker requires there to be IPv4, even when only an IPv6 subnet was
  361. # provided. So, there will be both an IPv4 and IPv6 pool in the
  362. # configuration.
  363. expected = {
  364. "recreated": True,
  365. ipv4_net.name: {
  366. "IPAM": {
  367. "Config": {
  368. "old": [
  369. {"Subnet": ipv4_net.subnet, "Gateway": ipv4_net.gateway},
  370. {"Subnet": ipv6_net1.subnet, "Gateway": ipv6_net1.gateway},
  371. ],
  372. "new": [
  373. {"Subnet": ipv4_net.subnet, "Gateway": ipv4_net.gateway},
  374. {"Subnet": ipv6_net2.subnet, "Gateway": ipv6_net2.gateway},
  375. ],
  376. }
  377. }
  378. },
  379. }
  380. self.assertEqual(ret["changes"], expected)
  381. self.assertEqual(
  382. ret["comment"],
  383. "Network '{0}' was replaced with updated config".format(ipv4_net.name),
  384. )