単一マシン内でネットワークテスト用の半仮想環境を用意する

ネットワークテストをするためにはポートを分けることが一般的に行われますが、IPアドレス自体を分離したいこともあると思います。unshareを使えば比較的(?)容易に行えます。

## worker

masterからNICを受け取るまで待ち、IPを設定してupするだけ。
ただしNIC名は15文字以下でないといけないらしい。

```py
#!/usr/bin/python
from subprocess import call
from subprocess import check_call
from os.path import isdir
from time import sleep

check_call(['mount', '-t', 'sysfs', '/sys', '/sys'])
while not isdir('/sys/devices/virtual/net/wrk-container'):
    sleep(1)

try:
    check_call(['ip', 'link', 'set', 'lo', 'up'])
    check_call(['mount', '--rbind', '/tmp/wrk-container/log', '/var/log'])
    check_call(['mount', '--rbind', '/tmp/wrk-container/run', '/run'])
    # todo: add other binds if required
    check_call(['ip', 'addr', 'add', '172.127.0.2/24', 'dev', 'wrk-container'])
    check_call(['ip', 'link', 'set', 'wrk-container', 'up'])

    # todo: use proper daemon program such as https://pypi.org/project/supervisor/
    check_call(['python','-c','''
import signal
import sys
import socket
from time import sleep

def on_signal(signum, frame):
    print('[worker] signal %d' % signum)
    sys.exit(0)

signal.signal(signal.SIGINT, on_signal)
signal.signal(signal.SIGTERM, on_signal)

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('', 12345))
sock.listen(1)
sleep(9999)
    '''])  
finally:
    call(['umount','-R','/run'])
    call(['umount','-R','/var/log'])
    call(['umount','/sys'])
```

## master

- workerをunshareを用いて起動する
- `ip link add wrk-host type veth peer wrk-container`で仮想NICを作成する
- ホスト側のIPを設定
- `ip link set wrk-container netns str(proc.pid)`でワーカー端のNICをワーカーのnet namespace内に移動する
- unshareをkillするだけだとworker daemonをkillできないようなので、worker daemonをプロセスツリーをたどってkillしています。詳細不明。

```py
#!/usr/bin/python

from os import kill
from os.path import join
from os.path import dirname
from os.path import realpath
from shutil import rmtree
from shutil import copytree
from subprocess import check_call
from signal import SIGTERM
from time import sleep

try:
    from pathlib import Path
except ImportError:
    from pathlib2 import Path

from psutil import Process
from checkpopen import CheckPopen  # https://pypi.org/project/checkpopen

def main():
    with CheckPopen(['unshare','-m','-i','-n','-u','-p','--mount-proc','--kill-child=SIGTERM','--fork','--root=/',join(dirname(realpath(__file__)), 'worker.py')]) as proc:
        try:
            # rmtree('/tmp/worker-container/log', ignore_errors=True)
            Path('/tmp/wrk-container/log').mkdir(parents=True, exist_ok=True)
            rmtree('/tmp/worker-container/run', ignore_errors=True)
            Path('/tmp/wrk-container/run').mkdir(parents=True, exist_ok=True)
            # todo: transfer files to /tmp/wrk-container/foo
            check_call(['ip','link','add','wrk-host','type','veth','peer','wrk-container'])
            check_call(['ip','addr','add','172.127.0.1/24','dev','wrk-host'])
            check_call(['ip','link','set','up','wrk-host'])
            check_call(['ip','link','set','wrk-container','netns',str(proc.pid)])
            try:
                import socket
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.connect(('172.127.0.2', 12345))  # check connection
            finally:
                kill(Process(proc.pid).children()[0].children()[0].pid, SIGTERM)
                print('sent sigterm to worker daemon')
        finally:
            rmtree('/var/log/wrk-container', ignore_errors=True)
            copytree('/tmp/wrk-container/log', '/var/log/wrk-container', symlinks=True)
            try:
                kill(Process(proc.pid).children()[0].pid, SIGTERM)
                print('sent sigterm to worker launcher')
            except IndexError:
                # probably worker daemon terminated quickly
                pass
            proc.terminate()
            print('sent sigterm to unshare')

if __name__ == '__main__':
    main()
```