├── images └── .keep ├── logs └── .keep ├── ptys └── .keep ├── ssh ├── .keep ├── node1.sh └── node2.sh ├── requirements.txt ├── mesh.gif ├── scenarios ├── single.py ├── chain_10_nodes.py ├── chain_4_nodes.py ├── test_reconfigure.py ├── random_mesh.py ├── test_batctl_tp.py ├── bottle.py ├── test_iperf.py └── test_gluon_neighbour_info.py ├── .gitignore ├── Makefile ├── scripts └── ssh.sh ├── update_image.sh ├── setup.py ├── LICENSE ├── README.md └── pynet └── __init__.py /images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logs/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ptys/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ssh/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asyncssh 2 | -------------------------------------------------------------------------------- /ssh/node1.sh: -------------------------------------------------------------------------------- 1 | ../scripts/ssh.sh -------------------------------------------------------------------------------- /ssh/node2.sh: -------------------------------------------------------------------------------- 1 | ../scripts/ssh.sh -------------------------------------------------------------------------------- /mesh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freifunk-gluon/gluon-qemu-testlab/HEAD/mesh.gif -------------------------------------------------------------------------------- /scenarios/single.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | sys.path.append(".") 4 | from pynet import * 5 | 6 | a = Node() 7 | 8 | start() 9 | finish() 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | image*.img 2 | images/*.img 3 | ssh/*.key 4 | ssh/*.key.pub 5 | logs/*.log 6 | ptys/* 7 | !ptys/.keep 8 | __pycache__ 9 | secret/noise* 10 | dist 11 | build 12 | -------------------------------------------------------------------------------- /scenarios/chain_10_nodes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | sys.path.append(".") 4 | from pynet import * 5 | 6 | a = Node() 7 | for i in range(9): 8 | b = Node() 9 | connect(a, b) 10 | a = b 11 | 12 | start() 13 | finish() 14 | -------------------------------------------------------------------------------- /scenarios/chain_4_nodes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | sys.path.append(".") 4 | from pynet import * 5 | 6 | a = Node() 7 | for i in range(3): 8 | b = Node() 9 | connect(a, b) 10 | a = b 11 | 12 | start() 13 | finish() 14 | -------------------------------------------------------------------------------- /scenarios/test_reconfigure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | sys.path.append(".") 4 | from pynet import * 5 | 6 | a = Node() 7 | 8 | start() 9 | 10 | stdout = a.succeed('gluon-reconfigure') 11 | print(stdout) 12 | 13 | finish() 14 | 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | build: setup.py 3 | rm -f dist/* 4 | python3 setup.py sdist bdist_wheel 5 | 6 | testupload: 7 | python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* 8 | 9 | upload: 10 | python3 -m twine upload --repository-url https://upload.pypi.org/legacy/ dist/* 11 | -------------------------------------------------------------------------------- /scenarios/random_mesh.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import random 4 | sys.path.append(".") 5 | from pynet import * 6 | 7 | NODE_COUNT = 30 8 | 9 | nodes = [] 10 | 11 | for i in range(NODE_COUNT): 12 | n = Node() 13 | 14 | if len(nodes) > 0: 15 | connect(n, random.choice(nodes)) 16 | 17 | nodes.append(n) 18 | 19 | start() 20 | finish() 21 | 22 | -------------------------------------------------------------------------------- /scripts/ssh.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | script=$(basename "$0") 4 | 5 | if ! echo "$script" | grep node > /dev/null; then 6 | echo Do not call this script directly. Use the symlinks in \$PROJECT_DIR/ssh/. 7 | exit 1 8 | fi 9 | 10 | node_id=$(echo "$script" | sed 's_^node__' | sed 's_.sh__') 11 | 12 | ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i $(dirname "$0")/id_rsa.key root@localhost -p $(python -c "print(22100+$node_id)") 13 | -------------------------------------------------------------------------------- /update_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | URL="https://build.ffh.zone/job/gluon-nightly/ws/download/images/factory/" 4 | 5 | filename=$(curl -s ${URL} | grep 'gluon.*x86-generic.img.gz' | sed 's_^.*\(gluon.*x86-generic.img.gz\).*$_\1_g') 6 | 7 | tmpfile=/tmp/${filename%.gz} 8 | 9 | echo Downloading ${filename} to ${tmpfile}.gz 10 | curl -s ${URL}/${filename} -o ${tmpfile}.gz 11 | 12 | echo Unpacking ${tmpfile}.gz to ${tmpfile} 13 | gzip -d ${tmpfile}.gz 14 | 15 | echo Copy to here ./image.img 16 | cp ${tmpfile} ./image.img 17 | -------------------------------------------------------------------------------- /scenarios/test_batctl_tp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python36 2 | import sys 3 | sys.path.append(".") 4 | from pynet import * 5 | import asyncio 6 | import time 7 | 8 | a = Node() 9 | b = Node() 10 | 11 | connect(a, b) 12 | 13 | start() # This command boots the qemu instances 14 | 15 | print(""" 16 | WARNING: THIS TEST IS CURRENTLY BROKEN, AS THE BATCTL TPMETER ALWAYS RETURNS TRUE. 17 | """) 18 | 19 | addr = a.succeed('cat /sys/class/net/primary0/address') 20 | result = b.succeed(f'batctl tp {addr}') 21 | 22 | print(result) 23 | 24 | finish() 25 | -------------------------------------------------------------------------------- /scenarios/bottle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | sys.path.append(".") 4 | from pynet import * 5 | 6 | # see https://www.open-mesh.org/attachments/download/42/bottle.png 7 | 8 | a = Node() 9 | 10 | n1, n2 = Node(), Node() 11 | 12 | connect(a, n1) 13 | connect(n1, n2) 14 | 15 | n3, n4, n5 = Node(), Node(), Node() 16 | n6, n7, n8 = Node(), Node(), Node() 17 | 18 | connect(n2, n3) 19 | connect(n3, n4) 20 | connect(n4, n5) 21 | 22 | connect(n2, n6) 23 | connect(n6, n7) 24 | connect(n7, n8) 25 | 26 | b = Node() 27 | 28 | connect(b, n5) 29 | connect(b, n8) 30 | 31 | start() 32 | finish() 33 | 34 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="gluon-qemu-testlab", 8 | version="0.0.5", 9 | author="Leonardo Mörlein", 10 | author_email="me@irrelefant.net", 11 | description="Python scripts to run qemu and gluon based virtual mesh networks", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/freifunk-gluon/gluon-qemu-testlab", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | python_requires='>=3.6', 22 | install_requires=['asyncssh==2.1.0'], 23 | ) 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2019 Leonardo Mörlein 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 | -------------------------------------------------------------------------------- /scenarios/test_iperf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python36 2 | import sys 3 | sys.path.append(".") 4 | from pynet import * 5 | import asyncio 6 | import time 7 | 8 | a = Node() 9 | b = Node() 10 | 11 | connect(a, b) 12 | 13 | start() # This command boots the qemu instances 14 | 15 | def ensure_iperf3(node): 16 | status, _ = node.execute('test -f /usr/bin/iperf3') 17 | if status != 0: 18 | node.succeed('gluon-wan opkg update && gluon-wan opkg install iperf3') 19 | 20 | ensure_iperf3(a) 21 | ensure_iperf3(b) 22 | 23 | rule = """ 24 | config rule 'iperf3' 25 | option dest_port '5201' 26 | option src 'mesh' 27 | option name 'iperf3' 28 | option target 'ACCEPT' 29 | option proto 'tcp' 30 | """ 31 | 32 | status, _ = b.execute('grep iperf3 /etc/config/firewall > /dev/null') 33 | if status != 0: 34 | b.succeed('cat >> /etc/config/firewall < None: 38 | """Call the given function repeatedly, with 1 second intervals, 39 | until it returns True or a timeout is reached. 40 | """ 41 | for _ in range(180): 42 | if fn(False): 43 | return 44 | time.sleep(1) 45 | 46 | if not fn(True): 47 | raise Exception("action timed out") 48 | 49 | class Node(): 50 | 51 | max_id = 0 52 | max_port = 17321 53 | all_nodes = [] 54 | 55 | def __init__(self): 56 | Node.max_id += 1 57 | Node.all_nodes += [self] 58 | self.id = Node.max_id 59 | self.hostname = 'node' + str(self.id) 60 | self.mesh_links = [] 61 | self.if_index_max = 1 62 | self.uci_sets = [] 63 | self.uci_commits = [] 64 | self.domain_code = None 65 | self.configured = False 66 | self.addresses = [] 67 | self.dbg = debug_print(initial_time, self.hostname) 68 | 69 | def add_mesh_link(self, peer, _is_peer=False, _port=None): 70 | self.if_index_max += 1 71 | ifname = 'eth' + str(self.if_index_max) 72 | if _port is None: 73 | Node.max_port += 1 74 | port = Node.max_port 75 | conn_type = 'listen' 76 | else: 77 | port = _port 78 | conn_type = 'connect' 79 | self.mesh_links.append((ifname, peer, conn_type, port)) 80 | if not _is_peer: 81 | peer.add_mesh_link(self, _is_peer=True, _port=port) 82 | return ifname 83 | 84 | def set_fastd_secret(self, secret): 85 | assert(type(secret) == str) 86 | assert(len(secret) == 64) 87 | for k in secret: 88 | assert(k in "1234567890abcdef") 89 | self.uci_set('fastd', 'mesh_vpn', 'secret', secret) 90 | self.uci_set('fastd', 'mesh_vpn', 'enabled', 1) 91 | 92 | def uci_set(self, config, section, option, value): 93 | self.uci_sets += ["uci set {}.{}.{}='{}'".format( 94 | config, section, option, value)] 95 | self.uci_commits += ["uci commit {}".format(config)] 96 | 97 | def set_domain(self, domain_code): 98 | self.uci_set('gluon', 'core', 'domain', domain_code) 99 | self.domain_code = domain_code 100 | 101 | @property 102 | def if_client(self): 103 | return "client" + str(self.id) 104 | 105 | def execute_in_background(self, cmd, _msg=True): 106 | class bg_cmd: 107 | def __init__(self, node, cmd): 108 | self.cmd = cmd 109 | self.node = node 110 | self.process = None 111 | 112 | async def _async_execute(): 113 | async with Node.ssh_conn(self.node) as conn: 114 | res = await conn.create_process(cmd, stderr=asyncssh.STDOUT) 115 | self.process = res 116 | return await res.wait() 117 | 118 | self.task = loop.create_task(_async_execute()) 119 | 120 | if _msg: 121 | node.dbg(f'Command "{cmd}" started in background.') 122 | 123 | def cancel(self): 124 | self.process.send_signal('INT') 125 | loop.run_until_complete(self.task) 126 | self.node.dbg(f'Sent SIGINT to command "{self.cmd}".') 127 | 128 | return bg_cmd(self, cmd) 129 | 130 | def execute(self, cmd): 131 | t = self.execute_in_background(cmd, _msg=False).task 132 | loop.run_until_complete(t) 133 | 134 | res = t.result() 135 | return (res.exit_status, res.stdout.strip()) 136 | 137 | def succeed(self, cmd): 138 | status, stdout = self.execute(cmd) 139 | 140 | if status != 0: 141 | msg = f'Expected success: command "{cmd}" failed with exit status {status}.' 142 | self.dbg(msg) 143 | self.dbg('Stdout/stderr was:') 144 | for line in stdout.split('\n'): 145 | self.dbg('| ' + line) 146 | raise Exception(msg) 147 | else: 148 | self.dbg(f'Expected success: command "{cmd}" succeeded with exit status {status}.') 149 | return stdout 150 | 151 | def wait_until_succeeds(self, cmd): 152 | output = "" 153 | 154 | def check_success(is_last_attempt) -> bool: 155 | nonlocal output 156 | if is_last_attempt: 157 | output = self.succeed(cmd) 158 | return True 159 | else: 160 | status, output = self.execute(cmd) 161 | return status == 0 162 | 163 | self.dbg(f'Waiting until "{cmd}" succeeds.') 164 | retry(check_success) 165 | self.dbg(f'"{cmd}" succeeded.') 166 | return output 167 | 168 | class ssh_conn: 169 | 170 | def __init__(self, node): 171 | self.node = node 172 | 173 | async def __aenter__(self): 174 | if USE_CLIENT_TAP: 175 | # client iface link local addr 176 | ifname = self.node.if_client 177 | host_id = HOST_ID 178 | lladdr = "fe80::5054:%02xff:fe%02x:34%02x" % (host_id, self.node.id, 2) 179 | addr = lladdr + '%' + ifname 180 | port = 22 181 | else: 182 | addr = '127.0.0.1' 183 | port = 22000 + self.node.id 184 | if self.node.configured: 185 | port += 100 186 | 187 | keyfile = os.path.join(workdir, 'ssh', SSH_KEY_FILE) 188 | conn = lambda: asyncssh.connect(addr, username='root', port=port, known_hosts=None, client_keys=[keyfile]) 189 | 190 | # 100 retries 191 | for i in range(100): 192 | try: 193 | self.conn = await conn() 194 | return self.conn 195 | except asyncssh.misc.ConnectionLost: 196 | await asyncio.sleep(1) 197 | except ConnectionResetError: 198 | await asyncio.sleep(1) 199 | except OSError: 200 | await asyncio.sleep(1) 201 | 202 | self.conn = await conn() 203 | return self.conn 204 | 205 | async def __aexit__(self, type, value, traceback): 206 | self.conn.close() 207 | 208 | 209 | class MobileClient(): 210 | 211 | max_id = 0 212 | 213 | def __init__(self): 214 | MobileClient.max_id += 1 215 | self.current_node = None 216 | self.ifname_peer = 'mobile' + str(MobileClient.max_id) + '_peer' 217 | self.ifname = 'mobile' + str(MobileClient.max_id) 218 | self.netns = 'mobile' + str(MobileClient.max_id) 219 | 220 | run('ip netns add ' + self.netns) 221 | run_in_netns(self.netns, 'ip link del ' + self.ifname) 222 | run('ip link add ' + self.ifname + ' type veth peer name ' + self.ifname_peer) 223 | run('ip link set ' + self.ifname + ' address de:ad:be:ee:ff:01 netns ' + self.netns + ' up') 224 | run('ip link set ' + self.ifname + ' up') 225 | 226 | def move_to(self, node): 227 | netns_new = "%s_client" % node.hostname 228 | bridge_new = "br_" + node.if_client 229 | 230 | if self.current_node is not None: 231 | netns_old = "%s_client" % self.current_node.hostname 232 | run_in_netns(netns_old, 'ip link set ' + self.ifname_peer + ' netns ' + netns_new + ' up') 233 | else: 234 | run('ip link set ' + self.ifname_peer + ' netns ' + netns_new + ' up') 235 | 236 | run_in_netns(netns_new, 'ip link set ' + self.ifname_peer + ' master ' + bridge_new) 237 | 238 | self.current_node = node 239 | 240 | def run(cmd): 241 | subprocess.run(cmd, shell=True) 242 | 243 | def run_in_netns(netns, cmd): 244 | subprocess.run('ip netns exec ' + netns + ' ' + cmd, shell=True) 245 | 246 | stdout_buffers = {} 247 | processes = {} 248 | masters = {} 249 | workdir = "./" 250 | 251 | async def gen_qemu_call(image, node): 252 | 253 | imgdir = os.path.join(workdir, 'images') 254 | if not os.path.exists(imgdir): 255 | os.mkdir(imgdir) 256 | 257 | imgfile = os.path.join(imgdir, '%02x.img' % node.id) 258 | shutil.copyfile('./' + image, imgfile) 259 | 260 | # TODO: machine identifier 261 | host_id = HOST_ID 262 | nat_mac = "52:54:%02x:%02x:34:%02x" % (host_id, node.id, 1) 263 | client_mac = "52:54:%02x:%02x:34:%02x" % (host_id, node.id, 2) 264 | 265 | mesh_ifaces = [] 266 | mesh_id = 1 267 | 268 | eth_driver = 'rtl8139' 269 | # eth_driver = 'e1000' 270 | # eth_driver = 'pcnet' # driver is buggy 271 | # eth_driver = 'vmxnet3' # no driver in gluon 272 | # eth_driver = 'ne2k_pci' # driver seems buggy 273 | # eth_driver = 'virtio-net-pci' 274 | 275 | for _, _, conn_type, port in node.mesh_links: 276 | if conn_type not in ['listen', 'connect']: 277 | raise ValueError('conn_type invalid: ' + str(conn_type)) 278 | 279 | if conn_type == 'connect': 280 | await wait_bash_cmd('while ! ss -tlp4n | grep ":' + str(port) + '" &>/dev/null; do sleep 1; done;') 281 | 282 | mesh_ifaces += [ 283 | '-device', (eth_driver + ',addr=0x%02x,netdev=mynet%d,id=m_nic%d,mac=' + \ 284 | "52:54:%02x:%02x:34:%02x") % (10 + mesh_id, mesh_id, mesh_id, host_id, node.id, 10 + mesh_id), 285 | '-netdev', 'socket,id=mynet%d,%s=:%d' % (mesh_id, conn_type, port) 286 | ] 287 | 288 | mesh_id += 1 289 | 290 | ssh_port = 22000 + node.id 291 | ssh_port_configured = 22100 + node.id 292 | 293 | wan_netdev = 'user,id=hn1,hostfwd=tcp::' + str(ssh_port_configured) + '-10.0.2.15:22' 294 | 295 | if USE_CLIENT_TAP: 296 | client_netdev = 'tap,id=hn2,script=no,downscript=no,ifname=%s' % node.if_client 297 | else: 298 | # in config mode, the device is used for configuration with net 192.168.1.0/24 299 | client_netdev = 'user,id=hn2,hostfwd=tcp::' + str(ssh_port) + '-192.168.1.1:22,net=192.168.1.15/24' 300 | 301 | call = ['-nographic', 302 | '-enable-kvm', 303 | # '-no-hpet', 304 | # '-cpu', 'host', 305 | '-netdev', wan_netdev, 306 | '-device', eth_driver + ',addr=0x06,netdev=hn1,id=nic1,mac=' + nat_mac, 307 | '-netdev', client_netdev, 308 | '-device', eth_driver + ',addr=0x05,netdev=hn2,id=nic2,mac=' + client_mac] 309 | 310 | # '-d', 'guest_errors', '-d', 'cpu_reset', '-gdb', 'tcp::' + str(3000 + node.id), 311 | args = ['qemu-system-x86_64', 312 | '-drive', 'format=raw,file=' + imgfile] + call + mesh_ifaces 313 | 314 | master, slave = os.openpty() 315 | ptydir = os.path.join(workdir, 'ptys') 316 | if not os.path.exists(ptydir): 317 | os.mkdir(ptydir) 318 | pty_path = os.path.join(ptydir, 'node%d' % node.id) 319 | if os.path.islink(pty_path): 320 | os.remove(pty_path) 321 | os.symlink(os.ttyname(slave), pty_path) 322 | process = asyncio.create_subprocess_exec(*args, stdout=subprocess.PIPE, stdin=master) 323 | masters[node.id] = master 324 | 325 | p = await process 326 | atexit.register(p.terminate) 327 | processes[node.id] = p 328 | 329 | async def ssh_call(p, cmd): 330 | res = await p.run(cmd) 331 | return res.stdout 332 | 333 | async def set_mesh_devs(p, devs): 334 | for d in devs: 335 | await ssh_call(p, 'uci set network.' + d + '_mesh=interface') 336 | await ssh_call(p, 'uci set network.' + d + '_mesh.auto=1') 337 | await ssh_call(p, 'uci set network.' + d + '_mesh.proto=gluon_wired') 338 | await ssh_call(p, 'uci set network.' + d + '_mesh.ifname=' + d) 339 | 340 | # allow vxlan in firewall 341 | await ssh_call(p, 'uci add_list firewall.wired_mesh.network=' + d + '_mesh') 342 | 343 | await ssh_call(p, 'uci commit network') 344 | await ssh_call(p, 'uci commit firewall') 345 | 346 | async def add_ssh_key(p): 347 | keyfile = os.path.join(workdir, 'ssh', SSH_PUBKEY_FILE) 348 | with open(keyfile) as f: 349 | content = f.read() 350 | await ssh_call(p, 'cat >> /etc/dropbear/authorized_keys </dev/null; do sleep 1; done;') 367 | 368 | host_id = HOST_ID 369 | # set mac of client tap iface on host system 370 | client_iface_mac = "aa:54:%02x:%02x:34:%02x" % (host_id, node.id, 2) 371 | run('ip link set ' + ifname + ' address ' + client_iface_mac) 372 | run('ip link set ' + ifname + ' up') 373 | # await wait_bash_cmd('while ! ping -c 1 ' + addr + ' &>/dev/null; do sleep 1; done;') 374 | dbg('iface ' + ifname + ' appeared') 375 | 376 | def configure_netns(node): 377 | dbg = debug_print(initial_time, node.hostname) 378 | # create netns 379 | netns = "%s_client" % node.hostname 380 | ifname = node.if_client 381 | # TODO: delete them correctly 382 | # Issue with mountpoints yet http://man7.org/linux/man-pages/man7/mount_namespaces.7.html 383 | use_netns = False 384 | 385 | run('ip netns add ' + netns) 386 | gen_etc_hosts_for_netns(netns) 387 | 388 | # move iface to netns 389 | dbg('moving ' + ifname + ' to netns ' + netns) 390 | run('ip link set netns ' + netns + ' dev ' + ifname) 391 | run_in_netns(netns, 'ip link set lo up') 392 | run_in_netns(netns, 'ip link set ' + ifname + ' up') 393 | run_in_netns(netns, 'ip link delete br_' + ifname + ' type bridge 2> /dev/null || true') # force deletion 394 | run_in_netns(netns, 'ip link add name br_' + ifname + ' type bridge') 395 | run_in_netns(netns, 'ip link set ' + ifname + ' master br_' + ifname) 396 | run_in_netns(netns, 'ip link set br_' + ifname + ' up') 397 | 398 | async def configure_node(initial_time, node): 399 | dbg = debug_print(initial_time, node.hostname) 400 | 401 | if USE_CLIENT_TAP: 402 | await configure_client_if(node) 403 | 404 | dbg('configuring node') 405 | 406 | async with Node.ssh_conn(node) as conn: 407 | dbg('connection established') 408 | await config_node(initial_time, node, conn) 409 | 410 | dbg(node.hostname + ' configured') 411 | node.configured = True 412 | 413 | # wait till all nodes are configured 414 | for n in Node.all_nodes: 415 | while not n.configured: 416 | await asyncio.sleep(1) 417 | 418 | # add /etc/hosts entries 419 | async with Node.ssh_conn(node) as conn: 420 | await add_hosts(conn) 421 | dbg('/etc/hosts is now adjusted') 422 | 423 | if USE_CLIENT_TAP and USE_NETNS: 424 | configure_netns(node) 425 | 426 | async def install_client(initial_time, node): 427 | clientname = "client" + str(node.id) 428 | dbg = debug_print(initial_time, clientname) 429 | 430 | # spawn client shell 431 | shell = os.environ.get('SHELL') or '/bin/bash' 432 | spawn_in_tmux(clientname, 'ip netns exec ' + netns + ' ' + shell) 433 | 434 | # spawn ssh shell 435 | ssh_opts = '-o UserKnownHostsFile=/dev/null ' + \ 436 | '-o StrictHostKeyChecking=no ' + \ 437 | '-i ' + SSH_KEY_FILE + ' ' 438 | spawn_in_tmux(node.hostname, 'ip netns exec ' + netns + ' /bin/bash -c "while ! ssh ' + ssh_opts + ' root@' + node.next_node_addr + '; do sleep 1; done"') 439 | 440 | def spawn_in_tmux(title, cmd): 441 | run('tmux -S test new-window -d -n ' + title + ' ' + cmd) 442 | 443 | @asyncio.coroutine 444 | def read_to_buffer(node): 445 | while processes.get(node.id) is None: 446 | yield from asyncio.sleep(0) 447 | process = processes[node.id] 448 | master = masters[node.id] 449 | stdout_buffers[node.id] = b"" 450 | 451 | logdir = os.path.join(workdir, 'logs') 452 | if not os.path.exists(logdir): 453 | os.mkdir(logdir) 454 | 455 | with open(os.path.join(logdir, node.hostname + '.log'), 'wb') as f1: 456 | while True: 457 | b = yield from process.stdout.read(1) # TODO: is this unbuffered? 458 | stdout_buffers[node.id] += b 459 | try: 460 | os.write(master, b) 461 | except BlockingIOError: 462 | # ignore the blocking error, when slave side is not opened 463 | pass 464 | f1.write(b) 465 | if b == b'\n': 466 | f1.flush() 467 | 468 | @asyncio.coroutine 469 | def wait_for(node, b): 470 | i = node.id 471 | while stdout_buffers.get(i) is None: 472 | yield from asyncio.sleep(0) 473 | while True: 474 | if b.encode('utf-8') in stdout_buffers[i]: 475 | return 476 | yield from asyncio.sleep(0) 477 | 478 | async def add_hosts(p): 479 | host_entries = "" 480 | 481 | for n in Node.all_nodes: 482 | for a in n.addresses: 483 | host_entries += str(a) + " " + n.hostname + "\n" 484 | 485 | await ssh_call(p, 'cat >> /etc/hosts <> /etc/bat-hosts <8.2f} | {hostname:<9}] {message}'.format(delta=delta, hostname=hostname, message=message)) 492 | return printfn 493 | 494 | async def config_node(initial_time, node, ssh_conn): 495 | 496 | dbg = debug_print(initial_time, node.hostname) 497 | 498 | p = ssh_conn 499 | 500 | mesh_ifaces = list(map(itemgetter(0), node.mesh_links)) 501 | 502 | await set_mesh_devs(p, mesh_ifaces) 503 | await ssh_call(p, 'pretty-hostname ' + node.hostname) 504 | await add_ssh_key(p) 505 | 506 | # do uci configs 507 | for cmd in node.uci_sets: 508 | await ssh_call(p, cmd) 509 | for cmd in set(node.uci_commits): 510 | await ssh_call(p, cmd) 511 | 512 | if node.domain_code is not None: 513 | await ssh_call(p, "gluon-reconfigure") 514 | 515 | prefix = (await ssh_call(p, 'gluon-show-site | jsonfilter -e @.prefix6')).strip() 516 | prefix = ipaddress.ip_network(prefix) 517 | 518 | mac = (await ssh_call(p, 'uci get network.client.macaddr')).strip() 519 | node.addresses.append(mac_to_ip6(mac, prefix)) 520 | 521 | # reboot to operational mode 522 | await ssh_call(p, 'uci set gluon-setup-mode.@setup_mode[0].configured=\'1\'') 523 | await ssh_call(p, 'uci commit gluon-setup-mode') 524 | await ssh_call(p, 'reboot') 525 | 526 | await wait_for(node, 'reboot: Restarting system') 527 | dbg('leaving config mode (reboot)') 528 | # flush buffer 529 | stdout_buffers[node.id] = b''.join(stdout_buffers[node.id].split(b'reboot: Restarting system')[1:]) 530 | await wait_for(node, 'Please press Enter to activate this console.') 531 | dbg('console appeared (again)') 532 | 533 | #ssh_call(p, 'uci set fastd.mesh_vpn.enabled=0') 534 | #ssh_call(p, 'uci commit fastd') 535 | #ssh_call(p, '/etc/init.d/fastd stop mesh_vpn') 536 | 537 | def gen_etc_hosts_for_netns(netns): 538 | # use /etc/hosts and extend it 539 | with open('/etc/hosts') as h: 540 | p = '/etc/netns/' 541 | if not os.path.exists(p): 542 | os.mkdir(p) 543 | p += netns + '/' 544 | if not os.path.exists(p): 545 | os.mkdir(p) 546 | p += 'hosts' 547 | with open(p, 'w') as f: 548 | f.write(h.read()) 549 | f.write('\n') 550 | f.write(host_entries) 551 | 552 | host_entries = "" 553 | bathost_entries = "" 554 | configured = False 555 | global loop 556 | loop = None 557 | config_tasks = [] 558 | args = None 559 | 560 | def mac_to_ip6(mac, net): 561 | mac = list(map(lambda x: int(x, 16), mac.split(':'))) 562 | x = list(next(net.hosts()).packed) 563 | x[8:] = [mac[0] ^ 0x02] + mac[1:3] + [0xff, 0xfe] + mac[3:] 564 | return ipaddress.ip_address(bytes(x)) 565 | 566 | def start(): 567 | global workdir 568 | global configured 569 | if configured: 570 | return 571 | 572 | global args 573 | parser = argparse.ArgumentParser() 574 | parser.add_argument("--run-forever", help="", action="store_true") 575 | parser.add_argument("--run-tests-on-existing-instance", help="", action="store_true") 576 | parser.add_argument("--use-tmp-workdir", help="", action="store_true") 577 | args = parser.parse_args() 578 | 579 | if args.use_tmp_workdir: 580 | workdir = os.path.join('/tmp', 'gluon-qemu-testlab') 581 | 582 | if not os.path.exists(workdir): 583 | os.mkdir(workdir) 584 | 585 | #if os.environ.get('TMUX') is None and not 'notmux' in sys.argv: 586 | # os.execl('/usr/bin/tmux', 'tmux', '-S', 'test', 'new', sys.executable, '-i', *sys.argv) 587 | 588 | sshdir = os.path.join(workdir, 'ssh') 589 | if not os.path.exists(sshdir): 590 | os.mkdir(sshdir) 591 | 592 | if not os.path.exists(os.path.join(sshdir, SSH_PUBKEY_FILE)): 593 | run('ssh-keygen -t rsa -f ' + os.path.join(sshdir, SSH_KEY_FILE) + ' -N \'\'') 594 | 595 | global loop 596 | loop = asyncio.get_event_loop() 597 | 598 | if args.run_tests_on_existing_instance: 599 | # We expect the nodes to be already configured. 600 | for node in Node.all_nodes: 601 | node.configured = True 602 | 603 | return loop 604 | 605 | host_id = HOST_ID 606 | global host_entries 607 | global bathost_entries 608 | global config_tasks 609 | 610 | for node in Node.all_nodes: 611 | bathost_entries += "52:54:{host_id:02x}:{node.id:02x}:34:02 {node.hostname}\n".format(node=node, host_id=host_id) 612 | 613 | bathost_entries += "de:ad:be:ee:ff:01 mobile1\n" 614 | 615 | for node in Node.all_nodes: 616 | loop.create_task(gen_qemu_call(image, node)) 617 | loop.create_task(read_to_buffer(node)) 618 | config_tasks += [loop.create_task(configure_node(initial_time, node))] 619 | 620 | configured = True 621 | 622 | for config_task in config_tasks: 623 | loop.run_until_complete(config_task) 624 | 625 | return loop 626 | 627 | def finish(): 628 | if args.run_tests_on_existing_instance: 629 | return 630 | 631 | if args.run_forever: 632 | try: 633 | print('Running forever. Well, at least till CTRL + C is pressed.') 634 | loop.run_forever() 635 | except KeyboardInterrupt: 636 | print('Exiting now. Closing qemus.') 637 | 638 | def connect(a, b): 639 | a.add_mesh_link(b) 640 | 641 | def new_loop(): 642 | global loop 643 | loop = asyncio.get_event_loop() 644 | 645 | 646 | initial_time = time.time() 647 | --------------------------------------------------------------------------------