# -*- 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 Testing Libs from tests.support.unit import skipIf from tests.support.case import ModuleCase from tests.support.docker import with_network, random_name from tests.support.paths import FILES, TMP from tests.support.helpers import destructiveTest, requires_system_grains from tests.support.mixins import SaltReturnAssertsMixin # Import Salt Libs import salt.utils.files import salt.utils.network import salt.utils.path from salt.exceptions import CommandExecutionError 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=TMP) script_path = \ os.path.join(FILES, 'file/base/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 @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 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) 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) 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) 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) 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) 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) 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) 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() 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() 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') 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() 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') 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') 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 ) )