# -*- coding: utf-8 -*- """ Integration tests for the docker_network states """ # Import Python Libs from __future__ import absolute_import, print_function, unicode_literals import errno import functools import logging import os import subprocess import tempfile # Import Salt Libs import salt.utils.files import salt.utils.network import salt.utils.path from salt.exceptions import CommandExecutionError from tests.support.case import ModuleCase from tests.support.docker import random_name, with_network from tests.support.helpers import destructiveTest, requires_system_grains, slowTest from tests.support.mixins import SaltReturnAssertsMixin # Import Salt Testing Libs from tests.support.runtests import RUNTIME_VARS from tests.support.unit import skipIf log = logging.getLogger(__name__) IMAGE_NAME = random_name(prefix="salt_busybox_") IPV6_ENABLED = bool(salt.utils.network.ip_addrs6(include_loopback=True)) def network_name(func): """ Generate a randomized name for a network and clean it up afterward """ @functools.wraps(func) def wrapper(self, *args, **kwargs): name = random_name(prefix="salt_net_") try: return func(self, name, *args, **kwargs) finally: self.run_function("docker.disconnect_all_containers_from_network", [name]) try: self.run_function("docker.remove_network", [name]) except CommandExecutionError as exc: if "No such network" not in exc.__str__(): raise return wrapper def container_name(func): """ Generate a randomized name for a container and clean it up afterward """ def build_image(): # Create temp dir image_build_rootdir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP) script_path = os.path.join(RUNTIME_VARS.BASE_FILES, "mkimage-busybox-static") cmd = [script_path, image_build_rootdir, IMAGE_NAME] log.debug("Running '%s' to build busybox image", " ".join(cmd)) process = subprocess.Popen( cmd, close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) output = process.communicate()[0] log.debug("Output from mkimge-busybox-static:\n%s", output) if process.returncode != 0: raise Exception("Failed to build image") try: salt.utils.files.rm_rf(image_build_rootdir) except OSError as exc: if exc.errno != errno.ENOENT: raise @functools.wraps(func) def wrapper(self, *args, **kwargs): try: self.run_function("docker.inspect_image", [IMAGE_NAME]) except CommandExecutionError: pass else: build_image() name = random_name(prefix="salt_test_") self.run_function( "docker.create", name=name, image=IMAGE_NAME, command="sleep 600", start=True, ) try: return func(self, name, *args, **kwargs) finally: try: self.run_function("docker.rm", [name], force=True) except CommandExecutionError as exc: if "No such container" not in exc.__str__(): raise return wrapper @slowTest @destructiveTest @skipIf(not salt.utils.path.which("dockerd"), "Docker not installed") class DockerNetworkTestCase(ModuleCase, SaltReturnAssertsMixin): """ Test docker_network states """ @classmethod def tearDownClass(cls): """ Remove test image if present. Note that this will run a docker rmi even if no test which required the image was run. """ cmd = ["docker", "rmi", "--force", IMAGE_NAME] log.debug("Running '%s' to destroy busybox image", " ".join(cmd)) process = subprocess.Popen( cmd, close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) output = process.communicate()[0] log.debug("Output from %s:\n%s", " ".join(cmd), output) if process.returncode != 0 and "No such image" not in str(output): raise Exception("Failed to destroy image") def run_state(self, function, **kwargs): ret = super(DockerNetworkTestCase, self).run_state(function, **kwargs) log.debug("ret = %s", ret) return ret @with_network(create=False) @slowTest def test_absent(self, net): self.assertSaltTrueReturn( self.run_state("docker_network.present", name=net.name) ) ret = self.run_state("docker_network.absent", name=net.name) self.assertSaltTrueReturn(ret) ret = ret[next(iter(ret))] self.assertEqual(ret["changes"], {"removed": True}) self.assertEqual(ret["comment"], "Removed network '{0}'".format(net.name)) @container_name @with_network(create=False) @slowTest def test_absent_with_disconnected_container(self, net, container_name): self.assertSaltTrueReturn( self.run_state( "docker_network.present", name=net.name, containers=[container_name] ) ) ret = self.run_state("docker_network.absent", name=net.name) self.assertSaltTrueReturn(ret) ret = ret[next(iter(ret))] self.assertEqual( ret["changes"], {"removed": True, "disconnected": [container_name]} ) self.assertEqual(ret["comment"], "Removed network '{0}'".format(net.name)) @with_network(create=False) @slowTest def test_absent_when_not_present(self, net): ret = self.run_state("docker_network.absent", name=net.name) self.assertSaltTrueReturn(ret) ret = ret[next(iter(ret))] self.assertEqual(ret["changes"], {}) self.assertEqual( ret["comment"], "Network '{0}' already absent".format(net.name) ) @with_network(create=False) @slowTest def test_present(self, net): ret = self.run_state("docker_network.present", name=net.name) self.assertSaltTrueReturn(ret) ret = ret[next(iter(ret))] # Make sure the state return is what we expect self.assertEqual(ret["changes"], {"created": True}) self.assertEqual(ret["comment"], "Network '{0}' created".format(net.name)) # Now check to see that the network actually exists. If it doesn't, # this next function call will raise an exception. self.run_function("docker.inspect_network", [net.name]) @container_name @with_network(create=False) @slowTest def test_present_with_containers(self, net, container_name): ret = self.run_state( "docker_network.present", name=net.name, containers=[container_name] ) self.assertSaltTrueReturn(ret) ret = ret[next(iter(ret))] self.assertEqual( ret["changes"], {"created": True, "connected": [container_name]} ) self.assertEqual(ret["comment"], "Network '{0}' created".format(net.name)) # Now check to see that the network actually exists. If it doesn't, # this next function call will raise an exception. self.run_function("docker.inspect_network", [net.name]) def _test_present_reconnect(self, net, container_name, reconnect=True): ret = self.run_state("docker_network.present", name=net.name, driver="bridge") self.assertSaltTrueReturn(ret) ret = ret[next(iter(ret))] self.assertEqual(ret["changes"], {"created": True}) self.assertEqual(ret["comment"], "Network '{0}' created".format(net.name)) # Connect the container self.run_function( "docker.connect_container_to_network", [container_name, net.name] ) # Change the driver to force the network to be replaced ret = self.run_state( "docker_network.present", name=net.name, driver="macvlan", reconnect=reconnect, ) self.assertSaltTrueReturn(ret) ret = ret[next(iter(ret))] self.assertEqual( ret["changes"], { "recreated": True, "reconnected" if reconnect else "disconnected": [container_name], net.name: {"Driver": {"old": "bridge", "new": "macvlan"}}, }, ) self.assertEqual( ret["comment"], "Network '{0}' was replaced with updated config".format(net.name), ) @container_name @with_network(create=False) @slowTest def test_present_with_reconnect(self, net, container_name): """ Test reconnecting with containers not passed to state """ self._test_present_reconnect(net, container_name, reconnect=True) @container_name @with_network(create=False) @slowTest def test_present_with_no_reconnect(self, net, container_name): """ Test reconnecting with containers not passed to state """ self._test_present_reconnect(net, container_name, reconnect=False) @with_network() @slowTest def test_present_internal(self, net): self.assertSaltTrueReturn( self.run_state("docker_network.present", name=net.name, internal=True,) ) net_info = self.run_function("docker.inspect_network", [net.name]) self.assertIs(net_info["Internal"], True) @with_network() @slowTest def test_present_labels(self, net): # Test a mix of different ways of specifying labels self.assertSaltTrueReturn( self.run_state( "docker_network.present", name=net.name, labels=["foo", "bar=baz", {"hello": "world"}], ) ) net_info = self.run_function("docker.inspect_network", [net.name]) self.assertEqual( net_info["Labels"], {"foo": "", "bar": "baz", "hello": "world"}, ) @with_network(subnet="fe3f:2180:26:1::/123") @with_network(subnet="10.247.197.96/27") @skipIf(not IPV6_ENABLED, "IPv6 not enabled") @slowTest def test_present_enable_ipv6(self, net1, net2): self.assertSaltTrueReturn( self.run_state( "docker_network.present", name=net1.name, enable_ipv6=True, ipam_pools=[{"subnet": net1.subnet}, {"subnet": net2.subnet}], ) ) net_info = self.run_function("docker.inspect_network", [net1.name]) self.assertIs(net_info["EnableIPv6"], True) @requires_system_grains @with_network() @slowTest def test_present_attachable(self, net, grains): if grains["os_family"] == "RedHat" and grains.get("osmajorrelease", 0) <= 7: self.skipTest("Cannot reliably manage attachable on RHEL <= 7") self.assertSaltTrueReturn( self.run_state("docker_network.present", name=net.name, attachable=True,) ) net_info = self.run_function("docker.inspect_network", [net.name]) self.assertIs(net_info["Attachable"], True) @skipIf(True, "Skip until we can set up docker swarm testing") @with_network() def test_present_scope(self, net): self.assertSaltTrueReturn( self.run_state("docker_network.present", name=net.name, scope="global",) ) net_info = self.run_function("docker.inspect_network", [net.name]) self.assertIs(net_info["Scope"], "global") @skipIf(True, "Skip until we can set up docker swarm testing") @with_network() def test_present_ingress(self, net): self.assertSaltTrueReturn( self.run_state("docker_network.present", name=net.name, ingress=True,) ) net_info = self.run_function("docker.inspect_network", [net.name]) self.assertIs(net_info["Ingress"], True) @with_network(subnet="10.247.197.128/27") @with_network(subnet="10.247.197.96/27") @slowTest def test_present_with_custom_ipv4(self, net1, net2): # First run will test passing the IPAM arguments individually self.assertSaltTrueReturn( self.run_state( "docker_network.present", name=net1.name, subnet=net1.subnet, gateway=net1.gateway, ) ) # Second run will pass them in the ipam_pools argument ret = self.run_state( "docker_network.present", name=net1.name, # We want to keep the same network name ipam_pools=[{"subnet": net2.subnet, "gateway": net2.gateway}], ) self.assertSaltTrueReturn(ret) ret = ret[next(iter(ret))] # Docker requires there to be IPv4, even when only an IPv6 subnet was # provided. So, there will be both an IPv4 and IPv6 pool in the # configuration. expected = { "recreated": True, net1.name: { "IPAM": { "Config": { "old": [{"Subnet": net1.subnet, "Gateway": net1.gateway}], "new": [{"Subnet": net2.subnet, "Gateway": net2.gateway}], } } }, } self.assertEqual(ret["changes"], expected) self.assertEqual( ret["comment"], "Network '{0}' was replaced with updated config".format(net1.name), ) @with_network(subnet="fe3f:2180:26:1::20/123") @with_network(subnet="fe3f:2180:26:1::/123") @with_network(subnet="10.247.197.96/27") @skipIf(not IPV6_ENABLED, "IPv6 not enabled") @slowTest def test_present_with_custom_ipv6(self, ipv4_net, ipv6_net1, ipv6_net2): self.assertSaltTrueReturn( self.run_state( "docker_network.present", name=ipv4_net.name, enable_ipv6=True, ipam_pools=[ {"subnet": ipv4_net.subnet, "gateway": ipv4_net.gateway}, {"subnet": ipv6_net1.subnet, "gateway": ipv6_net1.gateway}, ], ) ) ret = self.run_state( "docker_network.present", name=ipv4_net.name, # We want to keep the same network name enable_ipv6=True, ipam_pools=[ {"subnet": ipv4_net.subnet, "gateway": ipv4_net.gateway}, {"subnet": ipv6_net2.subnet, "gateway": ipv6_net2.gateway}, ], ) self.assertSaltTrueReturn(ret) ret = ret[next(iter(ret))] # Docker requires there to be IPv4, even when only an IPv6 subnet was # provided. So, there will be both an IPv4 and IPv6 pool in the # configuration. expected = { "recreated": True, ipv4_net.name: { "IPAM": { "Config": { "old": [ {"Subnet": ipv4_net.subnet, "Gateway": ipv4_net.gateway}, {"Subnet": ipv6_net1.subnet, "Gateway": ipv6_net1.gateway}, ], "new": [ {"Subnet": ipv4_net.subnet, "Gateway": ipv4_net.gateway}, {"Subnet": ipv6_net2.subnet, "Gateway": ipv6_net2.gateway}, ], } } }, } self.assertEqual(ret["changes"], expected) self.assertEqual( ret["comment"], "Network '{0}' was replaced with updated config".format(ipv4_net.name), )