1#!/usr/bin/env python3 2# 3# Copyright (c) 2022, The OpenThread Authors. 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions are met: 8# 1. Redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer. 10# 2. Redistributions in binary form must reproduce the above copyright 11# notice, this list of conditions and the following disclaimer in the 12# documentation and/or other materials provided with the distribution. 13# 3. Neither the name of the copyright holder nor the 14# names of its contributors may be used to endorse or promote products 15# derived from this software without specific prior written permission. 16# 17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27# POSSIBILITY OF SUCH DAMAGE. 28# 29 30import logging 31import os 32import re 33import subprocess 34import time 35 36 37class OtbrDocker: 38 device_pattern = re.compile('(?<=PTY is )/dev/.+$') 39 40 def __init__(self, nodeid: int, ot_path: str, ot_rcp_path: str, docker_image: str, docker_name: str): 41 self.nodeid = nodeid 42 self.ot_path = ot_path 43 self.ot_rcp_path = ot_rcp_path 44 self.docker_image = docker_image 45 self.docker_name = docker_name 46 47 self.logger = logging.getLogger('otbr_docker.OtbrDocker') 48 self.logger.setLevel(logging.INFO) 49 50 self._socat_proc = None 51 self._ot_rcp_proc = None 52 53 self._rcp_device_pty = None 54 self._rcp_device = None 55 56 self._launch() 57 58 def __repr__(self) -> str: 59 return f'OTBR<{self.nodeid}>' 60 61 def _launch(self): 62 self.logger.info('Launching %r ...', self) 63 self._launch_socat() 64 self._launch_ot_rcp() 65 self._launch_docker() 66 self.logger.info('Launched %r successfully', self) 67 68 def close(self): 69 self.logger.info('Shutting down %r ...', self) 70 self._shutdown_docker() 71 self._shutdown_ot_rcp() 72 self._shutdown_socat() 73 self.logger.info('Shut down %r successfully', self) 74 75 def _launch_socat(self): 76 self._socat_proc = subprocess.Popen(['socat', '-d', '-d', 'pty,raw,echo=0', 'pty,raw,echo=0'], 77 stderr=subprocess.PIPE, 78 stdin=subprocess.DEVNULL, 79 stdout=subprocess.DEVNULL) 80 81 line = self._socat_proc.stderr.readline().decode('ascii').strip() 82 self._rcp_device_pty = self.device_pattern.findall(line)[0] 83 line = self._socat_proc.stderr.readline().decode('ascii').strip() 84 self._rcp_device = self.device_pattern.findall(line)[0] 85 self.logger.info(f"socat running: device PTY: {self._rcp_device_pty}, device: {self._rcp_device}") 86 87 def _shutdown_socat(self): 88 if self._socat_proc is None: 89 return 90 91 self._socat_proc.stderr.close() 92 self._socat_proc.terminate() 93 self._socat_proc.wait() 94 self._socat_proc = None 95 96 self._rcp_device_pty = None 97 self._rcp_device = None 98 99 def _launch_ot_rcp(self): 100 self._ot_rcp_proc = subprocess.Popen( 101 f'{self.ot_rcp_path} {self.nodeid} > {self._rcp_device_pty} < {self._rcp_device_pty}', 102 shell=True, 103 stdin=subprocess.DEVNULL, 104 stdout=subprocess.DEVNULL, 105 stderr=subprocess.DEVNULL) 106 try: 107 self._ot_rcp_proc.wait(1) 108 except subprocess.TimeoutExpired: 109 # We expect ot-rcp not to quit in 1 second. 110 pass 111 else: 112 raise Exception(f"ot-rcp {self.nodeid} exited unexpectedly!") 113 114 def _shutdown_ot_rcp(self): 115 if self._ot_rcp_proc is None: 116 return 117 118 self._ot_rcp_proc.terminate() 119 self._ot_rcp_proc.wait() 120 self._ot_rcp_proc = None 121 122 def _launch_docker(self): 123 local_cmd_path = f'/tmp/{self.docker_name}' 124 os.makedirs(local_cmd_path, exist_ok=True) 125 126 cmd = [ 127 'docker', 128 'run', 129 '--rm', 130 '--name', 131 self.docker_name, 132 '-d', 133 '--sysctl', 134 'net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1', 135 '--privileged', 136 '-v', 137 f'{self._rcp_device}:/dev/ttyUSB0', 138 '-v', 139 f'{self.ot_path.rstrip("/")}:/home/pi/repo/openthread', 140 self.docker_image, 141 ] 142 self.logger.info('Launching docker: %s', ' '.join(cmd)) 143 launch_proc = subprocess.Popen(cmd, 144 stdin=subprocess.DEVNULL, 145 stdout=subprocess.DEVNULL, 146 stderr=subprocess.DEVNULL) 147 148 launch_docker_deadline = time.time() + 60 149 launch_ok = False 150 time.sleep(5) 151 152 while time.time() < launch_docker_deadline: 153 try: 154 subprocess.check_call(['docker', 'exec', self.docker_name, 'ot-ctl', 'state'], 155 stdin=subprocess.DEVNULL, 156 stdout=subprocess.DEVNULL, 157 stderr=subprocess.DEVNULL) 158 launch_ok = True 159 logging.info("OTBR Docker %s is ready!", self.docker_name) 160 break 161 except subprocess.CalledProcessError: 162 time.sleep(5) 163 continue 164 165 if not launch_ok: 166 raise RuntimeError('Cannot start OTBR Docker %s!' % self.docker_name) 167 launch_proc.wait() 168 169 def _shutdown_docker(self): 170 subprocess.run(['docker', 'stop', self.docker_name]) 171