xref: /aosp_15_r20/external/openthread/tools/harness-simulation/posix/launch_testbed.py (revision cfb92d1480a9e65faed56933e9c12405f45898b4)
1*cfb92d14SAndroid Build Coastguard Worker#!/usr/bin/env python3
2*cfb92d14SAndroid Build Coastguard Worker#
3*cfb92d14SAndroid Build Coastguard Worker#  Copyright (c) 2022, The OpenThread Authors.
4*cfb92d14SAndroid Build Coastguard Worker#  All rights reserved.
5*cfb92d14SAndroid Build Coastguard Worker#
6*cfb92d14SAndroid Build Coastguard Worker#  Redistribution and use in source and binary forms, with or without
7*cfb92d14SAndroid Build Coastguard Worker#  modification, are permitted provided that the following conditions are met:
8*cfb92d14SAndroid Build Coastguard Worker#  1. Redistributions of source code must retain the above copyright
9*cfb92d14SAndroid Build Coastguard Worker#     notice, this list of conditions and the following disclaimer.
10*cfb92d14SAndroid Build Coastguard Worker#  2. Redistributions in binary form must reproduce the above copyright
11*cfb92d14SAndroid Build Coastguard Worker#     notice, this list of conditions and the following disclaimer in the
12*cfb92d14SAndroid Build Coastguard Worker#     documentation and/or other materials provided with the distribution.
13*cfb92d14SAndroid Build Coastguard Worker#  3. Neither the name of the copyright holder nor the
14*cfb92d14SAndroid Build Coastguard Worker#     names of its contributors may be used to endorse or promote products
15*cfb92d14SAndroid Build Coastguard Worker#     derived from this software without specific prior written permission.
16*cfb92d14SAndroid Build Coastguard Worker#
17*cfb92d14SAndroid Build Coastguard Worker#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18*cfb92d14SAndroid Build Coastguard Worker#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19*cfb92d14SAndroid Build Coastguard Worker#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20*cfb92d14SAndroid Build Coastguard Worker#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21*cfb92d14SAndroid Build Coastguard Worker#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22*cfb92d14SAndroid Build Coastguard Worker#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23*cfb92d14SAndroid Build Coastguard Worker#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24*cfb92d14SAndroid Build Coastguard Worker#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25*cfb92d14SAndroid Build Coastguard Worker#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26*cfb92d14SAndroid Build Coastguard Worker#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27*cfb92d14SAndroid Build Coastguard Worker#  POSSIBILITY OF SUCH DAMAGE.
28*cfb92d14SAndroid Build Coastguard Worker#
29*cfb92d14SAndroid Build Coastguard Worker
30*cfb92d14SAndroid Build Coastguard Workerimport argparse
31*cfb92d14SAndroid Build Coastguard Workerimport ctypes
32*cfb92d14SAndroid Build Coastguard Workerimport ctypes.util
33*cfb92d14SAndroid Build Coastguard Workerimport ipaddress
34*cfb92d14SAndroid Build Coastguard Workerimport json
35*cfb92d14SAndroid Build Coastguard Workerimport logging
36*cfb92d14SAndroid Build Coastguard Workerimport os
37*cfb92d14SAndroid Build Coastguard Workerimport signal
38*cfb92d14SAndroid Build Coastguard Workerimport socket
39*cfb92d14SAndroid Build Coastguard Workerimport struct
40*cfb92d14SAndroid Build Coastguard Workerimport subprocess
41*cfb92d14SAndroid Build Coastguard Workerimport sys
42*cfb92d14SAndroid Build Coastguard Workerfrom typing import Iterable
43*cfb92d14SAndroid Build Coastguard Workerimport yaml
44*cfb92d14SAndroid Build Coastguard Worker
45*cfb92d14SAndroid Build Coastguard Workerfrom otbr_sim import otbr_docker
46*cfb92d14SAndroid Build Coastguard Worker
47*cfb92d14SAndroid Build Coastguard WorkerGROUP = 'ff02::114'
48*cfb92d14SAndroid Build Coastguard WorkerPORT = 12345
49*cfb92d14SAndroid Build Coastguard Worker
50*cfb92d14SAndroid Build Coastguard Worker
51*cfb92d14SAndroid Build Coastguard Workerdef if_nametoindex(ifname: str) -> int:
52*cfb92d14SAndroid Build Coastguard Worker    libc = ctypes.CDLL(ctypes.util.find_library('c'))
53*cfb92d14SAndroid Build Coastguard Worker    ret = libc.if_nametoindex(ifname.encode('ascii'))
54*cfb92d14SAndroid Build Coastguard Worker    if not ret:
55*cfb92d14SAndroid Build Coastguard Worker        raise RuntimeError('Invalid interface name')
56*cfb92d14SAndroid Build Coastguard Worker    return ret
57*cfb92d14SAndroid Build Coastguard Worker
58*cfb92d14SAndroid Build Coastguard Worker
59*cfb92d14SAndroid Build Coastguard Workerdef get_ipaddr(ifname: str) -> str:
60*cfb92d14SAndroid Build Coastguard Worker    for line in os.popen(f'ip addr list dev {ifname} | grep inet | grep global'):
61*cfb92d14SAndroid Build Coastguard Worker        addr = line.strip().split()[1]
62*cfb92d14SAndroid Build Coastguard Worker        return addr.split('/')[0]
63*cfb92d14SAndroid Build Coastguard Worker    raise RuntimeError(f'No IP address on dev {ifname}')
64*cfb92d14SAndroid Build Coastguard Worker
65*cfb92d14SAndroid Build Coastguard Worker
66*cfb92d14SAndroid Build Coastguard Workerdef init_socket(ifname: str, group: str, port: int) -> socket.socket:
67*cfb92d14SAndroid Build Coastguard Worker    # Look up multicast group address in name server and find out IP version
68*cfb92d14SAndroid Build Coastguard Worker    addrinfo = socket.getaddrinfo(group, None)[0]
69*cfb92d14SAndroid Build Coastguard Worker    assert addrinfo[0] == socket.AF_INET6
70*cfb92d14SAndroid Build Coastguard Worker
71*cfb92d14SAndroid Build Coastguard Worker    # Create a socket
72*cfb92d14SAndroid Build Coastguard Worker    s = socket.socket(addrinfo[0], socket.SOCK_DGRAM)
73*cfb92d14SAndroid Build Coastguard Worker    s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, (ifname + '\0').encode('ascii'))
74*cfb92d14SAndroid Build Coastguard Worker
75*cfb92d14SAndroid Build Coastguard Worker    # Bind it to the port
76*cfb92d14SAndroid Build Coastguard Worker    s.bind((group, port))
77*cfb92d14SAndroid Build Coastguard Worker
78*cfb92d14SAndroid Build Coastguard Worker    group_bin = socket.inet_pton(addrinfo[0], addrinfo[4][0])
79*cfb92d14SAndroid Build Coastguard Worker    # Join group
80*cfb92d14SAndroid Build Coastguard Worker    interface_index = if_nametoindex(ifname)
81*cfb92d14SAndroid Build Coastguard Worker    mreq = group_bin + struct.pack('@I', interface_index)
82*cfb92d14SAndroid Build Coastguard Worker    s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
83*cfb92d14SAndroid Build Coastguard Worker
84*cfb92d14SAndroid Build Coastguard Worker    return s
85*cfb92d14SAndroid Build Coastguard Worker
86*cfb92d14SAndroid Build Coastguard Worker
87*cfb92d14SAndroid Build Coastguard Workerdef _advertise(s: socket.socket, dst, info):
88*cfb92d14SAndroid Build Coastguard Worker    logging.info('Advertise: %r', info)
89*cfb92d14SAndroid Build Coastguard Worker    s.sendto(json.dumps(info).encode('utf-8'), dst)
90*cfb92d14SAndroid Build Coastguard Worker
91*cfb92d14SAndroid Build Coastguard Worker
92*cfb92d14SAndroid Build Coastguard Workerdef advertise_devices(s: socket.socket, dst, ven: str, add: str, nodeids: Iterable[int], tag: str):
93*cfb92d14SAndroid Build Coastguard Worker    for nodeid in nodeids:
94*cfb92d14SAndroid Build Coastguard Worker        info = {
95*cfb92d14SAndroid Build Coastguard Worker            'ven': ven,
96*cfb92d14SAndroid Build Coastguard Worker            'mod': 'OpenThread',
97*cfb92d14SAndroid Build Coastguard Worker            'ver': '4',
98*cfb92d14SAndroid Build Coastguard Worker            'add': f'{tag}_{nodeid}@{add}',
99*cfb92d14SAndroid Build Coastguard Worker            'por': 22,
100*cfb92d14SAndroid Build Coastguard Worker        }
101*cfb92d14SAndroid Build Coastguard Worker        _advertise(s, dst, info)
102*cfb92d14SAndroid Build Coastguard Worker
103*cfb92d14SAndroid Build Coastguard Worker
104*cfb92d14SAndroid Build Coastguard Workerdef advertise_sniffers(s: socket.socket, dst, add: str, ports: Iterable[int]):
105*cfb92d14SAndroid Build Coastguard Worker    for port in ports:
106*cfb92d14SAndroid Build Coastguard Worker        info = {
107*cfb92d14SAndroid Build Coastguard Worker            'add': add,
108*cfb92d14SAndroid Build Coastguard Worker            'por': port,
109*cfb92d14SAndroid Build Coastguard Worker        }
110*cfb92d14SAndroid Build Coastguard Worker        _advertise(s, dst, info)
111*cfb92d14SAndroid Build Coastguard Worker
112*cfb92d14SAndroid Build Coastguard Worker
113*cfb92d14SAndroid Build Coastguard Workerdef start_sniffer(addr: str, port: int, ot_path: str, max_nodes_num: int) -> subprocess.Popen:
114*cfb92d14SAndroid Build Coastguard Worker    if isinstance(ipaddress.ip_address(addr), ipaddress.IPv6Address):
115*cfb92d14SAndroid Build Coastguard Worker        server = f'[{addr}]:{port}'
116*cfb92d14SAndroid Build Coastguard Worker    else:
117*cfb92d14SAndroid Build Coastguard Worker        server = f'{addr}:{port}'
118*cfb92d14SAndroid Build Coastguard Worker
119*cfb92d14SAndroid Build Coastguard Worker    cmd = [
120*cfb92d14SAndroid Build Coastguard Worker        'python3',
121*cfb92d14SAndroid Build Coastguard Worker        os.path.join(ot_path, 'tools/harness-simulation/posix/sniffer_sim/sniffer.py'),
122*cfb92d14SAndroid Build Coastguard Worker        '--grpc-server',
123*cfb92d14SAndroid Build Coastguard Worker        server,
124*cfb92d14SAndroid Build Coastguard Worker        '--max-nodes-num',
125*cfb92d14SAndroid Build Coastguard Worker        str(max_nodes_num),
126*cfb92d14SAndroid Build Coastguard Worker    ]
127*cfb92d14SAndroid Build Coastguard Worker    logging.info('Executing command:  %s', ' '.join(cmd))
128*cfb92d14SAndroid Build Coastguard Worker    return subprocess.Popen(cmd)
129*cfb92d14SAndroid Build Coastguard Worker
130*cfb92d14SAndroid Build Coastguard Worker
131*cfb92d14SAndroid Build Coastguard Workerdef main():
132*cfb92d14SAndroid Build Coastguard Worker    logging.basicConfig(level=logging.INFO)
133*cfb92d14SAndroid Build Coastguard Worker
134*cfb92d14SAndroid Build Coastguard Worker    # Parse arguments
135*cfb92d14SAndroid Build Coastguard Worker    parser = argparse.ArgumentParser()
136*cfb92d14SAndroid Build Coastguard Worker    parser.add_argument('-c',
137*cfb92d14SAndroid Build Coastguard Worker                        '--config',
138*cfb92d14SAndroid Build Coastguard Worker                        dest='config',
139*cfb92d14SAndroid Build Coastguard Worker                        type=str,
140*cfb92d14SAndroid Build Coastguard Worker                        required=True,
141*cfb92d14SAndroid Build Coastguard Worker                        help='the path of the configuration JSON file')
142*cfb92d14SAndroid Build Coastguard Worker    args = parser.parse_args()
143*cfb92d14SAndroid Build Coastguard Worker    with open(args.config, 'rt') as f:
144*cfb92d14SAndroid Build Coastguard Worker        config = yaml.safe_load(f)
145*cfb92d14SAndroid Build Coastguard Worker
146*cfb92d14SAndroid Build Coastguard Worker    ot_path = config['ot_path']
147*cfb92d14SAndroid Build Coastguard Worker    ot_build = config['ot_build']
148*cfb92d14SAndroid Build Coastguard Worker    max_nodes_num = ot_build['max_number']
149*cfb92d14SAndroid Build Coastguard Worker    # No test case requires more than 2 sniffers
150*cfb92d14SAndroid Build Coastguard Worker    MAX_SNIFFER_NUM = 2
151*cfb92d14SAndroid Build Coastguard Worker
152*cfb92d14SAndroid Build Coastguard Worker    ot_devices = [(item['tag'], item['number']) for item in ot_build['ot']]
153*cfb92d14SAndroid Build Coastguard Worker    otbr_devices = [(item['tag'], item['number']) for item in ot_build['otbr']]
154*cfb92d14SAndroid Build Coastguard Worker    ot_nodes_num = sum(x[1] for x in ot_devices)
155*cfb92d14SAndroid Build Coastguard Worker    otbr_nodes_num = sum(x[1] for x in otbr_devices)
156*cfb92d14SAndroid Build Coastguard Worker    nodes_num = ot_nodes_num + otbr_nodes_num
157*cfb92d14SAndroid Build Coastguard Worker    sniffer_num = config['sniffer']['number']
158*cfb92d14SAndroid Build Coastguard Worker
159*cfb92d14SAndroid Build Coastguard Worker    # Check validation of numbers
160*cfb92d14SAndroid Build Coastguard Worker    if not all(0 <= x[1] <= max_nodes_num for x in ot_devices):
161*cfb92d14SAndroid Build Coastguard Worker        raise ValueError(f'The number of devices of each OT version should be between 0 and {max_nodes_num}')
162*cfb92d14SAndroid Build Coastguard Worker
163*cfb92d14SAndroid Build Coastguard Worker    if not all(0 <= x[1] <= max_nodes_num for x in otbr_devices):
164*cfb92d14SAndroid Build Coastguard Worker        raise ValueError(f'The number of devices of each OTBR version should be between 0 and {max_nodes_num}')
165*cfb92d14SAndroid Build Coastguard Worker
166*cfb92d14SAndroid Build Coastguard Worker    if not 1 <= nodes_num <= max_nodes_num:
167*cfb92d14SAndroid Build Coastguard Worker        raise ValueError(f'The number of devices should be between 1 and {max_nodes_num}')
168*cfb92d14SAndroid Build Coastguard Worker
169*cfb92d14SAndroid Build Coastguard Worker    if not 1 <= sniffer_num <= MAX_SNIFFER_NUM:
170*cfb92d14SAndroid Build Coastguard Worker        raise ValueError(f'The number of sniffers should be between 1 and {MAX_SNIFFER_NUM}')
171*cfb92d14SAndroid Build Coastguard Worker
172*cfb92d14SAndroid Build Coastguard Worker    # Get the local IP address on the specified interface
173*cfb92d14SAndroid Build Coastguard Worker    ifname = config['discovery_ifname']
174*cfb92d14SAndroid Build Coastguard Worker    addr = get_ipaddr(ifname)
175*cfb92d14SAndroid Build Coastguard Worker
176*cfb92d14SAndroid Build Coastguard Worker    # Start the sniffer
177*cfb92d14SAndroid Build Coastguard Worker    sniffer_server_port_base = config['sniffer']['server_port_base']
178*cfb92d14SAndroid Build Coastguard Worker    sniffer_procs = []
179*cfb92d14SAndroid Build Coastguard Worker    for i in range(sniffer_num):
180*cfb92d14SAndroid Build Coastguard Worker        sniffer_procs.append(start_sniffer(addr, i + sniffer_server_port_base, ot_path, max_nodes_num))
181*cfb92d14SAndroid Build Coastguard Worker
182*cfb92d14SAndroid Build Coastguard Worker    # OTBR firewall scripts create rules inside the Docker container
183*cfb92d14SAndroid Build Coastguard Worker    # Run modprobe to load the kernel modules for iptables
184*cfb92d14SAndroid Build Coastguard Worker    subprocess.run(['sudo', 'modprobe', 'ip6table_filter'])
185*cfb92d14SAndroid Build Coastguard Worker    # Start the BRs
186*cfb92d14SAndroid Build Coastguard Worker    otbr_dockers = []
187*cfb92d14SAndroid Build Coastguard Worker    nodeid = ot_nodes_num
188*cfb92d14SAndroid Build Coastguard Worker    for item in ot_build['otbr']:
189*cfb92d14SAndroid Build Coastguard Worker        tag = item['tag']
190*cfb92d14SAndroid Build Coastguard Worker        ot_rcp_path = os.path.join(ot_path, item['rcp_subpath'], 'examples/apps/ncp/ot-rcp')
191*cfb92d14SAndroid Build Coastguard Worker        docker_image = item['docker_image']
192*cfb92d14SAndroid Build Coastguard Worker        for _ in range(item['number']):
193*cfb92d14SAndroid Build Coastguard Worker            nodeid += 1
194*cfb92d14SAndroid Build Coastguard Worker            otbr_dockers.append(
195*cfb92d14SAndroid Build Coastguard Worker                otbr_docker.OtbrDocker(nodeid=nodeid,
196*cfb92d14SAndroid Build Coastguard Worker                                       ot_path=ot_path,
197*cfb92d14SAndroid Build Coastguard Worker                                       ot_rcp_path=ot_rcp_path,
198*cfb92d14SAndroid Build Coastguard Worker                                       docker_image=docker_image,
199*cfb92d14SAndroid Build Coastguard Worker                                       docker_name=f'{tag}_{nodeid}'))
200*cfb92d14SAndroid Build Coastguard Worker
201*cfb92d14SAndroid Build Coastguard Worker    s = init_socket(ifname, GROUP, PORT)
202*cfb92d14SAndroid Build Coastguard Worker
203*cfb92d14SAndroid Build Coastguard Worker    logging.info('Advertising on interface %s group %s ...', ifname, GROUP)
204*cfb92d14SAndroid Build Coastguard Worker
205*cfb92d14SAndroid Build Coastguard Worker    # Terminate all sniffer simulation server processes and then exit
206*cfb92d14SAndroid Build Coastguard Worker    def exit_handler(signum, context):
207*cfb92d14SAndroid Build Coastguard Worker        # Return code is non-zero if any return code of the processes is non-zero
208*cfb92d14SAndroid Build Coastguard Worker        ret = 0
209*cfb92d14SAndroid Build Coastguard Worker        for sniffer_proc in sniffer_procs:
210*cfb92d14SAndroid Build Coastguard Worker            sniffer_proc.terminate()
211*cfb92d14SAndroid Build Coastguard Worker            ret = max(ret, sniffer_proc.wait())
212*cfb92d14SAndroid Build Coastguard Worker
213*cfb92d14SAndroid Build Coastguard Worker        for otbr in otbr_dockers:
214*cfb92d14SAndroid Build Coastguard Worker            otbr.close()
215*cfb92d14SAndroid Build Coastguard Worker
216*cfb92d14SAndroid Build Coastguard Worker        sys.exit(ret)
217*cfb92d14SAndroid Build Coastguard Worker
218*cfb92d14SAndroid Build Coastguard Worker    signal.signal(signal.SIGINT, exit_handler)
219*cfb92d14SAndroid Build Coastguard Worker    signal.signal(signal.SIGTERM, exit_handler)
220*cfb92d14SAndroid Build Coastguard Worker
221*cfb92d14SAndroid Build Coastguard Worker    # Loop, printing any data we receive
222*cfb92d14SAndroid Build Coastguard Worker    while True:
223*cfb92d14SAndroid Build Coastguard Worker        data, src = s.recvfrom(64)
224*cfb92d14SAndroid Build Coastguard Worker
225*cfb92d14SAndroid Build Coastguard Worker        if data == b'BBR':
226*cfb92d14SAndroid Build Coastguard Worker            logging.info('Received OpenThread simulation query, advertising')
227*cfb92d14SAndroid Build Coastguard Worker
228*cfb92d14SAndroid Build Coastguard Worker            nodeid = 1
229*cfb92d14SAndroid Build Coastguard Worker            for ven, devices in [('OpenThread_Sim', ot_devices), ('OpenThread_BR_Sim', otbr_devices)]:
230*cfb92d14SAndroid Build Coastguard Worker                for tag, number in devices:
231*cfb92d14SAndroid Build Coastguard Worker                    advertise_devices(s, src, ven=ven, add=addr, nodeids=range(nodeid, nodeid + number), tag=tag)
232*cfb92d14SAndroid Build Coastguard Worker                    nodeid += number
233*cfb92d14SAndroid Build Coastguard Worker
234*cfb92d14SAndroid Build Coastguard Worker        elif data == b'Sniffer':
235*cfb92d14SAndroid Build Coastguard Worker            logging.info('Received sniffer simulation query, advertising')
236*cfb92d14SAndroid Build Coastguard Worker            advertise_sniffers(s,
237*cfb92d14SAndroid Build Coastguard Worker                               src,
238*cfb92d14SAndroid Build Coastguard Worker                               add=addr,
239*cfb92d14SAndroid Build Coastguard Worker                               ports=range(sniffer_server_port_base, sniffer_server_port_base + sniffer_num))
240*cfb92d14SAndroid Build Coastguard Worker
241*cfb92d14SAndroid Build Coastguard Worker        else:
242*cfb92d14SAndroid Build Coastguard Worker            logging.warning('Received %r, but ignored', data)
243*cfb92d14SAndroid Build Coastguard Worker
244*cfb92d14SAndroid Build Coastguard Worker
245*cfb92d14SAndroid Build Coastguard Workerif __name__ == '__main__':
246*cfb92d14SAndroid Build Coastguard Worker    main()
247