test_docker_network.py 15 KB

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