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