test_docker_network.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. """
  2. Integration tests for the docker_network states
  3. """
  4. # Import Python Libs
  5. import errno
  6. import functools
  7. import logging
  8. import os
  9. import subprocess
  10. import tempfile
  11. # Import Salt Libs
  12. import salt.utils.files
  13. import salt.utils.network
  14. import salt.utils.path
  15. from salt.exceptions import CommandExecutionError
  16. from tests.support.case import ModuleCase
  17. from tests.support.docker import random_name, with_network
  18. from tests.support.helpers import destructiveTest, requires_system_grains, slowTest
  19. from tests.support.mixins import SaltReturnAssertsMixin
  20. # Import Salt Testing Libs
  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. @slowTest
  91. @destructiveTest
  92. @skipIf(salt.utils.platform.is_freebsd(), "No Docker on FreeBSD available")
  93. @skipIf(not salt.utils.path.which("dockerd"), "Docker not installed")
  94. class DockerNetworkTestCase(ModuleCase, SaltReturnAssertsMixin):
  95. """
  96. Test docker_network states
  97. """
  98. @classmethod
  99. def tearDownClass(cls):
  100. """
  101. Remove test image if present. Note that this will run a docker rmi even
  102. if no test which required the image was run.
  103. """
  104. cmd = ["docker", "rmi", "--force", IMAGE_NAME]
  105. log.debug("Running '%s' to destroy busybox image", " ".join(cmd))
  106. process = subprocess.Popen(
  107. cmd, close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
  108. )
  109. output = process.communicate()[0]
  110. log.debug("Output from %s:\n%s", " ".join(cmd), output)
  111. if process.returncode != 0 and "No such image" not in str(output):
  112. raise Exception("Failed to destroy image")
  113. def run_state(self, function, **kwargs):
  114. ret = super().run_state(function, **kwargs)
  115. log.debug("ret = %s", ret)
  116. return ret
  117. @with_network(create=False)
  118. @slowTest
  119. def test_absent(self, net):
  120. self.assertSaltTrueReturn(
  121. self.run_state("docker_network.present", name=net.name)
  122. )
  123. ret = self.run_state("docker_network.absent", name=net.name)
  124. self.assertSaltTrueReturn(ret)
  125. ret = ret[next(iter(ret))]
  126. self.assertEqual(ret["changes"], {"removed": True})
  127. self.assertEqual(ret["comment"], "Removed network '{}'".format(net.name))
  128. @container_name
  129. @with_network(create=False)
  130. @slowTest
  131. def test_absent_with_disconnected_container(self, net, container_name):
  132. self.assertSaltTrueReturn(
  133. self.run_state(
  134. "docker_network.present", name=net.name, containers=[container_name]
  135. )
  136. )
  137. ret = self.run_state("docker_network.absent", name=net.name)
  138. self.assertSaltTrueReturn(ret)
  139. ret = ret[next(iter(ret))]
  140. self.assertEqual(
  141. ret["changes"], {"removed": True, "disconnected": [container_name]}
  142. )
  143. self.assertEqual(ret["comment"], "Removed network '{}'".format(net.name))
  144. @with_network(create=False)
  145. @slowTest
  146. def test_absent_when_not_present(self, net):
  147. ret = self.run_state("docker_network.absent", name=net.name)
  148. self.assertSaltTrueReturn(ret)
  149. ret = ret[next(iter(ret))]
  150. self.assertEqual(ret["changes"], {})
  151. self.assertEqual(ret["comment"], "Network '{}' already absent".format(net.name))
  152. @with_network(create=False)
  153. @slowTest
  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 '{}' 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. @slowTest
  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 '{}' 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 '{}' 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 '{}' was replaced with updated config".format(net.name),
  210. )
  211. @container_name
  212. @with_network(create=False)
  213. @slowTest
  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. @slowTest
  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. @slowTest
  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. @slowTest
  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. @slowTest
  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. @slowTest
  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. @slowTest
  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 '{}' 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. @slowTest
  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 '{}' was replaced with updated config".format(ipv4_net.name),
  384. )