├── .gitignore ├── LICENSE.md ├── README.md ├── circle.yml ├── docker_iptables.py ├── setup.py └── tests ├── __init__.py └── test_docker_iptables.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | # Packages 3 | *.egg 4 | *.egg-info 5 | dist 6 | build 7 | eggs 8 | parts 9 | bin 10 | var 11 | sdist 12 | develop-eggs 13 | .installed.cfg 14 | 15 | # Installer logs 16 | pip-log.txt 17 | 18 | # Unit test / coverage reports 19 | .coverage 20 | .tox 21 | 22 | #Translations 23 | *.mo 24 | 25 | # virtualenv 26 | venv 27 | .venv 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Pantheon Systems, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | docker_iptables 2 | =============== 3 | 4 | This script is intended to handle Docker iptables port-forwardings manually, for those times when 5 | you can't let Docker manage iptables on its own because it conflicts with other things on the 6 | system that are also trying to manage iptables. 7 | 8 | Usage 9 | ----- 10 | 11 | ### Assumptions / Pre-reqs: 12 | 13 | - `systemd` is your init and containers will be managed by systemd .service units. 14 | - Canonical source for iptables rules is `/etc/iptables.d/` and `/etc/ip6tables.d/` 15 | - A service named `iptables.service` exists and when stated or restarted will flush all running 16 | iptables rules and reload rulesfrom the .d directories. 17 | 18 | ### Using this script: 19 | 20 | 1. The docker daemon should be configured to start with `--iptables=false`. 21 | 2. Create an `/etc/iptables.d/10_docker` file with the following rules. These are the base rules that 22 | would normally get created when the docker daemon is started. NOTE: You must use a chain name other 23 | than DOCKER because even with `--iptables=false` docker will remove this chain on startup (bug in docker?). 24 | 25 | *nat 26 | :DOCKER_CONTAINERS - [0:0] 27 | -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER_CONTAINERS 28 | -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER_CONTAINERS 29 | -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE 30 | COMMIT 31 | *filter 32 | -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 33 | -A FORWARD -i docker0 ! -o docker0 -j ACCEPT 34 | -A FORWARD -i docker0 -o docker0 -j ACCEPT 35 | COMMIT 36 | 37 | 3. `ExecStartPost=` and `ExecStopPost=` commands in each docker container .service: 38 | 39 | [Unit] 40 | Description=hello-world docker service 41 | After=docker.service 42 | Requires=docker.service 43 | 44 | [Service] 45 | SyslogIdentifier=hello-world 46 | ExecStartPre=-/usr/bin/docker rm hello-world 47 | ExecStart=/usr/bin/docker run --name="hello-world" --publish="5000:5000" --rm=true quay.io/getpantheon/hello-world:master 48 | ExecStartPost=/opt/titan/utilities/docker/docker-iptables.py create hello-world 49 | ExecStop=-/usr/bin/docker stop hello-world 50 | ExecStopPost=/opt/titan/utilities/docker/docker-iptables.py delete hello-world 51 | Restart=always 52 | RestartSec=10s 53 | TimeoutStartSec=120 54 | TimeoutStopSec=15 55 | 56 | [Install] 57 | WantedBy=multi-user.target 58 | 59 | ### Manual usage: 60 | 61 | The script can also be run against any running container: 62 | 63 | docker-iptables.py create container-name 64 | docker-iptables.py delete container-name 65 | 66 | The above commands will read the port mappings from `docker inspect` of the running container and 67 | add or delete the rules from `/etc/iptables.d`. It will also call `systemctl restart iptables.service` 68 | to load or remove the rules from the running config. 69 | 70 | Testing 71 | ------- 72 | 73 | 1. run unit tests: `python setup.py test` 74 | 2. run pylint test: `pylint docker_iptables.py` 75 | 76 | Contributing 77 | ------------ 78 | 79 | 1. Do work in a branch 80 | 2. Add tests, ensure tests pass on Circle 81 | 3. Bump version in `setup.py` in your branch 82 | 4. Send PR to `master` branch 83 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | override: 3 | - pip install pylint 4 | 5 | test: 6 | override: 7 | - pylint ./docker_iptables.py 8 | - python setup.py test 9 | -------------------------------------------------------------------------------- /docker_iptables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | #pylint: disable=line-too-long,invalid-name,missing-docstring,redefined-outer-name,too-many-arguments,too-many-locals,too-many-statements,logging-format-interpolation 4 | # 5 | 6 | import os 7 | import sys 8 | import time 9 | import json 10 | import subprocess 11 | 12 | import logging as log 13 | 14 | 15 | def setup_logging(loglevel=log.INFO): 16 | """initialize logging. 17 | Currently only logging to stdout and not directly to the journal to avoid double-logging 18 | since this script is intended to run from an ExecStartPost= of a systemd unit, its stdout 19 | will already be aggregated with that unit's journal logs, and it makes sense to combine this 20 | script's logs with the container unit's logs rather than making them separate. 21 | 22 | This uses the standard python logger instead of titan.pantheon.twistedLog for future 23 | portability and because we are not logging to the journal directly. 24 | """ 25 | log.basicConfig(level=loglevel, format='%(levelname)-8s: %(message)s') 26 | 27 | 28 | def is_ipv4(addr): 29 | """return true if addr looks like an ipv4 address, false otherwise""" 30 | if addr == '0/0' or '.' in addr: 31 | return True 32 | else: 33 | return False 34 | 35 | def is_ipv6(addr): 36 | """return true if addr looks like an ipv6 address, false otherwise""" 37 | if addr == '0/0' or ':' in addr: 38 | return True 39 | else: 40 | return False 41 | 42 | 43 | def docker_inspect(container_name, max_attempts=20): 44 | """Runs `docker inspect ` and parses its json output, returning a python dict. 45 | 46 | raises subprocess.CalledProcessError on non-zero exit status from docker 47 | """ 48 | attempts = 0 49 | success = False 50 | while attempts < max_attempts and not success: 51 | try: 52 | result = subprocess.check_output('docker inspect {}'.format(container_name), 53 | stderr=subprocess.STDOUT, 54 | shell=True) 55 | success = True 56 | except subprocess.CalledProcessError: 57 | time.sleep(0.1) # 100ms!! 58 | 59 | attempts += 1 60 | 61 | if success: 62 | return json.loads(result)[0] 63 | else: 64 | raise RuntimeError('Retries exhausted waiting for {} to become available'.format(container_name)) 65 | 66 | 67 | def wait_until_running(container_name, max_attempts=20): 68 | attempts = 0 69 | success = False 70 | while attempts < max_attempts and not success: 71 | container_data = docker_inspect(container_name) 72 | if container_data['State']['Running'] is True: 73 | return True 74 | time.sleep(0.1) 75 | attempts += 1 76 | raise RuntimeError('timed out waiting for {} to enter running state.'.format(container_name)) 77 | 78 | 79 | def create_ipv4_nat_rule(chain, bridge, proto, host_port, container_ip, container_port): 80 | """return a iptables v4 nat rule for forwarding a host port to a container IP:port""" 81 | return '-A {chain} ! -i {bridge} -p {proto} -m {proto}' \ 82 | ' --dport {host_port} -j DNAT' \ 83 | ' --to-destination {container_ip}:{container_port}'.format(chain=chain, 84 | bridge=bridge, 85 | proto=proto, 86 | host_port=host_port, 87 | container_ip=container_ip, 88 | container_port=container_port) 89 | 90 | def create_ipv4_filter_rule(container_ip, bridge, proto, container_port): 91 | """return a iptables v4 filter rule for forwarding a host port to a container IP:port""" 92 | return '-A FORWARD -d {container_ip} ! -i {bridge} -o {bridge}' \ 93 | ' -p {proto} -m {proto} --dport {container_port}'\ 94 | ' -j ACCEPT\n'.format(container_ip=container_ip, 95 | bridge=bridge, 96 | proto=proto, 97 | container_port=container_port) 98 | 99 | 100 | def create_ipv6_nat_rule(chain, bridge, proto, host_port, container_ip, container_port): 101 | """return a iptables v6 nat rule for forwarding a host port to a container IP:port""" 102 | return '-A {chain} ! -i {bridge} -p {proto} -m {proto}' \ 103 | ' --dport {host_port} -j DNAT' \ 104 | ' --to-destination [{container_ip}]:{container_port}'.format(chain=chain, 105 | bridge=bridge, 106 | proto=proto, 107 | host_port=host_port, 108 | container_ip=container_ip, 109 | container_port=container_port) 110 | 111 | def create_ipv6_filter_rule(container_ip, bridge, proto, container_port): 112 | """return a iptables v4 filter rule for forwarding a host port to a container IP:port""" 113 | return '-A FORWARD -d {container_ip} ! -i {bridge} -o {bridge}' \ 114 | ' -p {proto} -m {proto} --dport {container_port}'\ 115 | ' -j ACCEPT\n'.format(container_ip=container_ip, 116 | bridge=bridge, 117 | proto=proto, 118 | container_port=container_port) 119 | 120 | 121 | def write_iptables_file(filename, nat_rules, filter_rules): 122 | with open(filename, 'w') as f: 123 | f.write('*nat\n') 124 | for rule in nat_rules: 125 | f.write(rule + '\n') 126 | f.write('COMMIT\n') 127 | 128 | f.write('*filter\n') 129 | for rule in filter_rules: 130 | f.write(rule + '\n') 131 | f.write('COMMIT\n') 132 | 133 | 134 | def remove_iptables_file(filename): 135 | if os.path.exists(filename): 136 | os.remove(filename) 137 | 138 | 139 | def restart_iptables(): 140 | subprocess.call('systemctl restart iptables.service', shell=True) 141 | 142 | 143 | def restart_ip6tables(): 144 | subprocess.call('systemctl restart ip6tables.service', shell=True) 145 | 146 | 147 | def main(args): 148 | """args should be a Namespace() with following attributes: 149 | 150 | - action (string): "create" or "delete" 151 | - container_name (string): name of a docker container 152 | - debug (bool): enable debug logging 153 | - chain (string): iptables name to attach docker container rules onto 154 | - ipv6 (bool): enable generating ip6tables (ipv6) rules in addition to ipv4 155 | - iptables_dir (string): directory containing iptables rules files 156 | - ip6tables_dir (string): directory containing ip6tables rules files 157 | """ 158 | 159 | loglevel = log.DEBUG if args.debug else log.INFO 160 | setup_logging(loglevel) 161 | 162 | log.debug(args) 163 | 164 | container = args.container_name 165 | chain = args.chain 166 | enable_ipv6 = args.ipv6 167 | iptables_file = os.path.join(args.iptables_dir, '11-docker-container_' + container) 168 | ip6tables_file = os.path.join(args.ip6tables_dir, '11-docker-container_' + container) 169 | 170 | if args.action == 'create': 171 | nat4_rules = [] 172 | filter4_rules = [] 173 | nat6_rules = [] 174 | filter6_rules = [] 175 | 176 | try: 177 | wait_until_running(container) 178 | except RuntimeError as e: 179 | log.error('Error occurred while waiting for container to start: {}'.format(e)) 180 | sys.exit(1) 181 | 182 | try: 183 | container_data = docker_inspect(container) 184 | except RuntimeError as e: 185 | log.error('Error retrieving container data, container: {}": {}'.format(container, e)) 186 | sys.exit(1) 187 | 188 | network = container_data['NetworkSettings'] 189 | container_ip = network['IPAddress'] 190 | bridge = network['Bridge'] 191 | mappings = network['Ports'] 192 | 193 | if len(mappings) == 0: 194 | log.info('container {} does not have any port mappings, nothing to do.'.format(container)) 195 | sys.exit(0) 196 | 197 | for (container_map, host_map) in mappings.iteritems(): 198 | # `container_map` example: (String): "5000/tcp" 199 | # `host_map` example: list of single dict: [{u'HostPort': u'5001', u'HostIp': u'0.0.0.0'}] 200 | # host_map may also be None in the case of an EXPOSED port that is not published. 201 | if not host_map: 202 | log.debug('exposed port {} is not published, skipping.'.format(container_map)) 203 | continue 204 | (container_port, proto) = container_map.split('/') 205 | host_ip = host_map[0]['HostIp'] 206 | host_port = host_map[0]['HostPort'] 207 | 208 | # convert 0.0.0.0 to 0/0 which is accepted by both iptables and ip6tables 209 | host_ip = '0/0' if host_ip == '0.0.0.0' else host_ip 210 | 211 | # iptables (ipv4) rules 212 | if is_ipv4(host_ip): 213 | nat4_rules.append(create_ipv4_nat_rule(chain, bridge, proto, host_port, container_ip, container_port)) 214 | filter4_rules.append(create_ipv4_filter_rule(container_ip, bridge, proto, container_port)) 215 | 216 | # ip6tables (ipv6) rules 217 | # NOTE: ipv6 nat'ing is a weird concept but linux supports it since 3.7+. At this time 218 | # Docker (1.2) does not seem to support it well but we at least want to support 219 | # the case of making our apps available on the host's ipv6 addr, so we try to make 220 | # some rules to publish our services on ipv4 and ipv6. 221 | if enable_ipv6 and is_ipv6(host_ip): 222 | nat6_rules.append(create_ipv6_nat_rule(chain, bridge, proto, host_port, container_ip, container_port)) 223 | filter6_rules.append(create_ipv6_filter_rule(container_ip, bridge, proto, container_port)) 224 | 225 | log.debug('nat4_rules:\n{}\n'.format(nat4_rules)) 226 | log.debug('filter4_rules:\n{}\n'.format(filter4_rules)) 227 | log.debug('nat6_rules:\n{}\n'.format(nat6_rules)) 228 | log.debug('filter6_rules:\n{}\n'.format(filter6_rules)) 229 | 230 | log.info('Writing iptables rules to {} and initiating iptables reload'.format(iptables_file)) 231 | write_iptables_file(iptables_file, nat4_rules, filter4_rules) 232 | restart_iptables() 233 | 234 | if enable_ipv6: 235 | log.info('Writing ipt6ables rules to {} and initiating iptables reload'.format(ip6tables_file)) 236 | write_iptables_file(ip6tables_file, nat6_rules, filter6_rules) 237 | restart_ip6tables() 238 | 239 | if args.action == 'delete': 240 | log.info('Deleting iptables rule files: {}, {}'.format(iptables_file, ip6tables_file)) 241 | remove_iptables_file(iptables_file) 242 | remove_iptables_file(ip6tables_file) 243 | 244 | log.info('reloading iptables rules') 245 | restart_iptables() 246 | 247 | if enable_ipv6: 248 | restart_ip6tables() 249 | 250 | 251 | if __name__ == '__main__': 252 | import argparse 253 | 254 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) 255 | 256 | # required, positional args 257 | parser.add_argument('action', choices=['create', 'delete']) 258 | parser.add_argument('container_name', help='name of container') 259 | 260 | # optional, flag based args 261 | parser.add_argument('--iptables-dir', help='Directory to store iptables files', default='/etc/iptables.d') 262 | parser.add_argument('--ip6tables-dir', help='Directory to store ip6tables files', default='/etc/ip6tables.d') 263 | parser.add_argument('--chain', help='Name of docker iptables chain', default='DOCKER_CONTAINERS') 264 | parser.add_argument('--ipv6', help='Enable ip6tables. Experimental. Docker ipv6 support requires the lxc exec driver.', action='store_true') 265 | parser.add_argument('--debug', help='Enable debug logging.', action='store_true') 266 | args = parser.parse_args() 267 | 268 | main(args) 269 | 270 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='docker_iptables', 5 | scripts=['docker_iptables.py'], 6 | version='0.0.3', 7 | tests_require=['pylint', 'mock'], 8 | test_suite="tests", 9 | ) 10 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pantheon-systems/docker_iptables/5047f53a7596e7738195064b6d2f54d883ae2b28/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_docker_iptables.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import argparse 4 | import unittest 5 | 6 | from docker_iptables import * 7 | 8 | import mock 9 | 10 | 11 | class TestDockerIptables(unittest.TestCase): 12 | 13 | def setUp(self): 14 | self.chain = 'CHAIN' 15 | self.bridge = 'docker0' 16 | self.proto = 'tcp' 17 | self.host_ip = '0/0' 18 | self.host_port = 5001 19 | self.container_ip = ' 172.24.0.2' 20 | self.container_port = 5000 21 | 22 | def test_is_ipv4(self): 23 | self.assertTrue(is_ipv4('0/0')) 24 | self.assertTrue(is_ipv4('10.0.0.1')) 25 | self.assertFalse(is_ipv4('2001:24:203:4f20::dead:beef')) 26 | 27 | def test_is_ipv6(self): 28 | self.assertTrue(is_ipv6('0/0')) 29 | self.assertTrue(is_ipv6('2001:24:203:4f20::dead:beef')) 30 | self.assertFalse(is_ipv6('10.0.0.1')) 31 | 32 | def test_create_ipv4_nat_rule(self): 33 | expected = '-A {chain} ! -i {bridge} -p {proto} -m {proto}' \ 34 | ' --dport {host_port} -j DNAT' \ 35 | ' --to-destination {container_ip}:{container_port}'.format(chain=self.chain, 36 | bridge=self.bridge, 37 | proto=self.proto, 38 | host_port=self.host_port, 39 | container_ip=self.container_ip, 40 | container_port=self.container_port) 41 | rule = create_ipv4_nat_rule(self.chain, 42 | self.bridge, 43 | self.proto, 44 | self.host_port, 45 | self.container_ip, 46 | self.container_port) 47 | self.assertEqual(expected, rule) 48 | 49 | def test_create_ipv4_filter_rule(self): 50 | expected = '-A FORWARD -d {container_ip} ! -i {bridge} -o {bridge}' \ 51 | ' -p {proto} -m {proto} --dport {container_port}'\ 52 | ' -j ACCEPT\n'.format(container_ip=self.container_ip, 53 | bridge=self.bridge, 54 | proto=self.proto, 55 | container_port=self.container_port) 56 | rule = create_ipv4_filter_rule(self.container_ip, 57 | self.bridge, 58 | self.proto, 59 | self.container_port) 60 | self.assertEqual(expected, rule) 61 | 62 | def test_create_ipv6_nat_rule(self): 63 | expected = '-A {chain} ! -i {bridge} -p {proto} -m {proto}' \ 64 | ' --dport {host_port} -j DNAT' \ 65 | ' --to-destination [{container_ip}]:{container_port}'.format(chain=self.chain, 66 | bridge=self.bridge, 67 | proto=self.proto, 68 | host_port=self.host_port, 69 | container_ip=self.container_ip, 70 | container_port=self.container_port) 71 | rule = create_ipv6_nat_rule(self.chain, 72 | self.bridge, 73 | self.proto, 74 | self.host_port, 75 | self.container_ip, 76 | self.container_port) 77 | self.assertEqual(expected, rule) 78 | 79 | def test_create_ipv6_filter_rule(self): 80 | expected = '-A FORWARD -d {container_ip} ! -i {bridge} -o {bridge}' \ 81 | ' -p {proto} -m {proto} --dport {container_port}'\ 82 | ' -j ACCEPT\n'.format(container_ip=self.container_ip, 83 | bridge=self.bridge, 84 | proto=self.proto, 85 | container_port=self.container_port) 86 | rule = create_ipv6_filter_rule(self.container_ip, 87 | self.bridge, 88 | self.proto, 89 | self.container_port) 90 | self.assertEqual(expected, rule) 91 | 92 | @mock.patch('docker_iptables.docker_inspect') 93 | def test_wait_until_running(self, mock_inspect): 94 | inspect_json = """ 95 | { 96 | "State": { 97 | "ExitCode": 0, 98 | "FinishedAt": "0001-01-01T00:00:00Z", 99 | "Paused": false, 100 | "Pid": 16274, 101 | "Restarting": false, 102 | "Running": true, 103 | "StartedAt": "2014-10-23T18:42:33.833945172Z" 104 | } 105 | } 106 | """ 107 | mock_inspect.return_value = json.loads(inspect_json) 108 | self.assertTrue(wait_until_running('container-1')) 109 | 110 | @mock.patch('docker_iptables.docker_inspect') 111 | @mock.patch('subprocess.call') 112 | @mock.patch('os.path.exists') 113 | @mock.patch('os.remove') 114 | @mock.patch('__builtin__.open') 115 | def test_main(self, mock_open, mock_remove, mock_exists, mock_call, mock_inspect): 116 | args = argparse.Namespace() 117 | args.action = 'create' 118 | args.container_name = 'test-container' 119 | args.debug = True 120 | args.chain = self.chain 121 | args.ipv6 = True 122 | args.iptables_dir = '/tmp/iptables.d' 123 | args.ip6tables_dir = '/tmp/ip6tables.d' 124 | 125 | # we mock the (partial) json output of a call to exec `docker inspect` 126 | docker_inspect_hello_world = """ 127 | { 128 | "NetworkSettings": { 129 | "Bridge": "docker0", 130 | "Gateway": "172.17.42.1", 131 | "IPAddress": "172.17.0.36", 132 | "IPPrefixLen": 16, 133 | "PortMapping": null, 134 | "Ports": { 135 | "8000/tcp": null, 136 | "5001/tcp": [ 137 | { 138 | "HostIp": "0.0.0.0", 139 | "HostPort": "5000" 140 | } 141 | ] 142 | } 143 | }, 144 | "State": { 145 | "ExitCode": 0, 146 | "FinishedAt": "0001-01-01T00:00:00Z", 147 | "Paused": false, 148 | "Pid": 16274, 149 | "Restarting": false, 150 | "Running": true, 151 | "StartedAt": "2014-10-23T18:42:33.833945172Z" 152 | } 153 | } 154 | """ 155 | mock_inspect.return_value = json.loads(docker_inspect_hello_world) 156 | 157 | # run the app, check critical actions happened as we expected. 158 | main(args) 159 | 160 | # print mock_open 161 | # print mock_open.mock_calls 162 | 163 | # verify `docker inspect ` was executed 164 | mock_inspect.assert_any_call(args.container_name) 165 | 166 | # verify iptables and ip6tables were created with proper paths 167 | mock_open.assert_any_call(os.path.join(args.iptables_dir, '11-docker-container_' + args.container_name), 'w') 168 | mock_open.assert_any_call(os.path.join(args.ip6tables_dir, '11-docker-container_' + args.container_name), 'w') 169 | # we could also test the contents were written as expected by checking each write call, but 170 | # it is probably sufficient to only test the functions that create the rule strings. 171 | 172 | # verify iptables services were restarted 173 | mock_call.assert_any_call('systemctl restart iptables.service', shell=True) 174 | mock_call.assert_any_call('systemctl restart ip6tables.service', shell=True) 175 | 176 | 177 | if __name__ == '__main__': 178 | unittest.main() 179 | --------------------------------------------------------------------------------