1*8975f5c5SAndroid Build Coastguard Worker# Copyright 2022 The Chromium Authors 2*8975f5c5SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be 3*8975f5c5SAndroid Build Coastguard Worker# found in the LICENSE file. 4*8975f5c5SAndroid Build Coastguard Worker"""Test server set up.""" 5*8975f5c5SAndroid Build Coastguard Worker 6*8975f5c5SAndroid Build Coastguard Workerimport logging 7*8975f5c5SAndroid Build Coastguard Workerimport os 8*8975f5c5SAndroid Build Coastguard Workerimport sys 9*8975f5c5SAndroid Build Coastguard Workerimport subprocess 10*8975f5c5SAndroid Build Coastguard Worker 11*8975f5c5SAndroid Build Coastguard Workerfrom typing import List, Optional, Tuple 12*8975f5c5SAndroid Build Coastguard Worker 13*8975f5c5SAndroid Build Coastguard Workerfrom common import DIR_SRC_ROOT, get_free_local_port, get_ssh_address 14*8975f5c5SAndroid Build Coastguard Workerfrom compatible_utils import get_ssh_prefix 15*8975f5c5SAndroid Build Coastguard Worker 16*8975f5c5SAndroid Build Coastguard Workersys.path.append(os.path.join(DIR_SRC_ROOT, 'build', 'util', 'lib', 'common')) 17*8975f5c5SAndroid Build Coastguard Worker# pylint: disable=import-error,wrong-import-position 18*8975f5c5SAndroid Build Coastguard Workerimport chrome_test_server_spawner 19*8975f5c5SAndroid Build Coastguard Worker# pylint: enable=import-error,wrong-import-position 20*8975f5c5SAndroid Build Coastguard Worker 21*8975f5c5SAndroid Build Coastguard Worker 22*8975f5c5SAndroid Build Coastguard Workerdef _run_ssh_tunnel(target_addr: str, command: str, 23*8975f5c5SAndroid Build Coastguard Worker port_maps: List[str]) -> subprocess.CompletedProcess: 24*8975f5c5SAndroid Build Coastguard Worker assert port_maps 25*8975f5c5SAndroid Build Coastguard Worker 26*8975f5c5SAndroid Build Coastguard Worker ssh_prefix = get_ssh_prefix(target_addr) 27*8975f5c5SAndroid Build Coastguard Worker 28*8975f5c5SAndroid Build Coastguard Worker # Allow a tunnel / control path to be established for the first time. 29*8975f5c5SAndroid Build Coastguard Worker # The sshconfig https://crsrc.org/c/build/fuchsia/test/sshconfig used here 30*8975f5c5SAndroid Build Coastguard Worker # persists the connection. 31*8975f5c5SAndroid Build Coastguard Worker subprocess.run(ssh_prefix + ['echo', 'true'], check=True) 32*8975f5c5SAndroid Build Coastguard Worker 33*8975f5c5SAndroid Build Coastguard Worker forward_proc = subprocess.run( 34*8975f5c5SAndroid Build Coastguard Worker ssh_prefix + [ 35*8975f5c5SAndroid Build Coastguard Worker '-O', 36*8975f5c5SAndroid Build Coastguard Worker command, # Send SSH mux control signal. 37*8975f5c5SAndroid Build Coastguard Worker '-NT' # Don't execute command; don't allocate terminal. 38*8975f5c5SAndroid Build Coastguard Worker ] + port_maps, 39*8975f5c5SAndroid Build Coastguard Worker capture_output=True, 40*8975f5c5SAndroid Build Coastguard Worker check=True, 41*8975f5c5SAndroid Build Coastguard Worker text=True) 42*8975f5c5SAndroid Build Coastguard Worker return forward_proc 43*8975f5c5SAndroid Build Coastguard Worker 44*8975f5c5SAndroid Build Coastguard Worker 45*8975f5c5SAndroid Build Coastguard Workerdef _forward_command(fuchsia_port: int, host_port: int, 46*8975f5c5SAndroid Build Coastguard Worker port_forwarding: bool) -> List[str]: 47*8975f5c5SAndroid Build Coastguard Worker max_port = 65535 48*8975f5c5SAndroid Build Coastguard Worker assert fuchsia_port is not None and 0 <= fuchsia_port <= max_port 49*8975f5c5SAndroid Build Coastguard Worker assert host_port is not None and 0 < host_port <= max_port 50*8975f5c5SAndroid Build Coastguard Worker if port_forwarding: 51*8975f5c5SAndroid Build Coastguard Worker return ['-R', f'{fuchsia_port}:localhost:{host_port}'] 52*8975f5c5SAndroid Build Coastguard Worker assert fuchsia_port != 0 53*8975f5c5SAndroid Build Coastguard Worker return ['-L', f'{host_port}:localhost:{fuchsia_port}'] 54*8975f5c5SAndroid Build Coastguard Worker 55*8975f5c5SAndroid Build Coastguard Worker 56*8975f5c5SAndroid Build Coastguard Workerdef _forward_commands(ports: List[Tuple[int, int]], 57*8975f5c5SAndroid Build Coastguard Worker port_forwarding: bool) -> List[str]: 58*8975f5c5SAndroid Build Coastguard Worker assert ports 59*8975f5c5SAndroid Build Coastguard Worker forward_cmd = [] 60*8975f5c5SAndroid Build Coastguard Worker for port in ports: 61*8975f5c5SAndroid Build Coastguard Worker assert port is not None 62*8975f5c5SAndroid Build Coastguard Worker forward_cmd.extend(_forward_command(port[0], port[1], port_forwarding)) 63*8975f5c5SAndroid Build Coastguard Worker return forward_cmd 64*8975f5c5SAndroid Build Coastguard Worker 65*8975f5c5SAndroid Build Coastguard Worker 66*8975f5c5SAndroid Build Coastguard Workerdef ports_forward(target_addr: str, 67*8975f5c5SAndroid Build Coastguard Worker ports: List[Tuple[int, int]]) -> subprocess.CompletedProcess: 68*8975f5c5SAndroid Build Coastguard Worker """Establishes a port forwarding SSH task to forward ports from the host to 69*8975f5c5SAndroid Build Coastguard Worker the fuchsia endpoints specified by tuples of port numbers in format of 70*8975f5c5SAndroid Build Coastguard Worker [fuchsia-port, host-port]. Setting fuchsia-port to 0 would allow the fuchsia 71*8975f5c5SAndroid Build Coastguard Worker selecting a free port; host-port shouldn't be 0. 72*8975f5c5SAndroid Build Coastguard Worker 73*8975f5c5SAndroid Build Coastguard Worker Blocks until port forwarding is established. 74*8975f5c5SAndroid Build Coastguard Worker 75*8975f5c5SAndroid Build Coastguard Worker Returns the CompletedProcess of the SSH task.""" 76*8975f5c5SAndroid Build Coastguard Worker return _run_ssh_tunnel(target_addr, 'forward', 77*8975f5c5SAndroid Build Coastguard Worker _forward_commands(ports, True)) 78*8975f5c5SAndroid Build Coastguard Worker 79*8975f5c5SAndroid Build Coastguard Worker 80*8975f5c5SAndroid Build Coastguard Workerdef ports_backward( 81*8975f5c5SAndroid Build Coastguard Worker target_addr: str, 82*8975f5c5SAndroid Build Coastguard Worker ports: List[Tuple[int, int]]) -> subprocess.CompletedProcess: 83*8975f5c5SAndroid Build Coastguard Worker """Establishes a reverse port forwarding SSH task to forward ports from the 84*8975f5c5SAndroid Build Coastguard Worker fuchsia to the host endpoints specified by tuples of port numbers in format 85*8975f5c5SAndroid Build Coastguard Worker of [fuchsia-port, host-port]. Both host-port and fuchsia-port shouldn't be 86*8975f5c5SAndroid Build Coastguard Worker 0. 87*8975f5c5SAndroid Build Coastguard Worker 88*8975f5c5SAndroid Build Coastguard Worker Blocks until port forwarding is established. 89*8975f5c5SAndroid Build Coastguard Worker 90*8975f5c5SAndroid Build Coastguard Worker Returns the CompletedProcess of the SSH task.""" 91*8975f5c5SAndroid Build Coastguard Worker return _run_ssh_tunnel(target_addr, 'forward', 92*8975f5c5SAndroid Build Coastguard Worker _forward_commands(ports, False)) 93*8975f5c5SAndroid Build Coastguard Worker 94*8975f5c5SAndroid Build Coastguard Worker 95*8975f5c5SAndroid Build Coastguard Workerdef port_forward(target_addr: str, host_port: int) -> int: 96*8975f5c5SAndroid Build Coastguard Worker """Establishes a port forwarding SSH task to a host TCP endpoint at port 97*8975f5c5SAndroid Build Coastguard Worker |host_port|. Blocks until port forwarding is established. 98*8975f5c5SAndroid Build Coastguard Worker 99*8975f5c5SAndroid Build Coastguard Worker Returns the fuchsia port number.""" 100*8975f5c5SAndroid Build Coastguard Worker 101*8975f5c5SAndroid Build Coastguard Worker forward_proc = ports_forward(target_addr, [(0, host_port)]) 102*8975f5c5SAndroid Build Coastguard Worker parsed_port = int(forward_proc.stdout.splitlines()[0].strip()) 103*8975f5c5SAndroid Build Coastguard Worker logging.debug('Port forwarding established (local=%d, device=%d)', 104*8975f5c5SAndroid Build Coastguard Worker host_port, parsed_port) 105*8975f5c5SAndroid Build Coastguard Worker return parsed_port 106*8975f5c5SAndroid Build Coastguard Worker 107*8975f5c5SAndroid Build Coastguard Worker 108*8975f5c5SAndroid Build Coastguard Workerdef port_backward(target_addr: str, 109*8975f5c5SAndroid Build Coastguard Worker fuchsia_port: int, 110*8975f5c5SAndroid Build Coastguard Worker host_port: int = 0) -> int: 111*8975f5c5SAndroid Build Coastguard Worker """Establishes a reverse port forwarding SSH task to a fuchsia TCP endpoint 112*8975f5c5SAndroid Build Coastguard Worker at port |fuchsia_port| from the host at port |host_port|. If |host_port| is 113*8975f5c5SAndroid Build Coastguard Worker None or 0, a local free port will be selected. 114*8975f5c5SAndroid Build Coastguard Worker Blocks until reverse port forwarding is established. 115*8975f5c5SAndroid Build Coastguard Worker 116*8975f5c5SAndroid Build Coastguard Worker Returns the local port number.""" 117*8975f5c5SAndroid Build Coastguard Worker 118*8975f5c5SAndroid Build Coastguard Worker if not host_port: 119*8975f5c5SAndroid Build Coastguard Worker host_port = get_free_local_port() 120*8975f5c5SAndroid Build Coastguard Worker ports_backward(target_addr, [(fuchsia_port, host_port)]) 121*8975f5c5SAndroid Build Coastguard Worker logging.debug('Reverse port forwarding established (local=%d, device=%d)', 122*8975f5c5SAndroid Build Coastguard Worker host_port, fuchsia_port) 123*8975f5c5SAndroid Build Coastguard Worker return host_port 124*8975f5c5SAndroid Build Coastguard Worker 125*8975f5c5SAndroid Build Coastguard Worker 126*8975f5c5SAndroid Build Coastguard Workerdef cancel_port_forwarding(target_addr: str, fuchsia_port: int, host_port: int, 127*8975f5c5SAndroid Build Coastguard Worker port_forwarding: bool) -> None: 128*8975f5c5SAndroid Build Coastguard Worker """Cancels an existing port forwarding, if port_forwarding is false, it will 129*8975f5c5SAndroid Build Coastguard Worker be treated as reverse port forwarding. 130*8975f5c5SAndroid Build Coastguard Worker Note, the ports passing in here need to exactly match the ports used to 131*8975f5c5SAndroid Build Coastguard Worker setup the port forwarding, i.e. if ports_forward([0, 8080]) was issued, even 132*8975f5c5SAndroid Build Coastguard Worker it returned an allocated port, cancel_port_forwarding(..., 0, 8080, ...) 133*8975f5c5SAndroid Build Coastguard Worker should still be used to cancel the port forwarding.""" 134*8975f5c5SAndroid Build Coastguard Worker _run_ssh_tunnel(target_addr, 'cancel', 135*8975f5c5SAndroid Build Coastguard Worker _forward_command(fuchsia_port, host_port, port_forwarding)) 136*8975f5c5SAndroid Build Coastguard Worker 137*8975f5c5SAndroid Build Coastguard Worker 138*8975f5c5SAndroid Build Coastguard Worker# Disable pylint errors since the subclass is not from this directory. 139*8975f5c5SAndroid Build Coastguard Worker# pylint: disable=invalid-name,missing-function-docstring 140*8975f5c5SAndroid Build Coastguard Workerclass SSHPortForwarder(chrome_test_server_spawner.PortForwarder): 141*8975f5c5SAndroid Build Coastguard Worker """Implementation of chrome_test_server_spawner.PortForwarder that uses 142*8975f5c5SAndroid Build Coastguard Worker SSH's remote port forwarding feature to forward ports.""" 143*8975f5c5SAndroid Build Coastguard Worker 144*8975f5c5SAndroid Build Coastguard Worker def __init__(self, target_addr: str) -> None: 145*8975f5c5SAndroid Build Coastguard Worker self._target_addr = target_addr 146*8975f5c5SAndroid Build Coastguard Worker 147*8975f5c5SAndroid Build Coastguard Worker # Maps the host (server) port to the device port number. 148*8975f5c5SAndroid Build Coastguard Worker self._port_mapping = {} 149*8975f5c5SAndroid Build Coastguard Worker 150*8975f5c5SAndroid Build Coastguard Worker def Map(self, port_pairs: List[Tuple[int, int]]) -> None: 151*8975f5c5SAndroid Build Coastguard Worker for p in port_pairs: 152*8975f5c5SAndroid Build Coastguard Worker fuchsia_port, host_port = p 153*8975f5c5SAndroid Build Coastguard Worker assert fuchsia_port == 0, \ 154*8975f5c5SAndroid Build Coastguard Worker 'Port forwarding with a fixed fuchsia-port is unsupported yet.' 155*8975f5c5SAndroid Build Coastguard Worker self._port_mapping[host_port] = \ 156*8975f5c5SAndroid Build Coastguard Worker port_forward(self._target_addr, host_port) 157*8975f5c5SAndroid Build Coastguard Worker 158*8975f5c5SAndroid Build Coastguard Worker def GetDevicePortForHostPort(self, host_port: int) -> int: 159*8975f5c5SAndroid Build Coastguard Worker return self._port_mapping[host_port] 160*8975f5c5SAndroid Build Coastguard Worker 161*8975f5c5SAndroid Build Coastguard Worker def Unmap(self, device_port: int) -> None: 162*8975f5c5SAndroid Build Coastguard Worker for host_port, fuchsia_port in self._port_mapping.items(): 163*8975f5c5SAndroid Build Coastguard Worker if fuchsia_port == device_port: 164*8975f5c5SAndroid Build Coastguard Worker cancel_port_forwarding(self._target_addr, 0, host_port, True) 165*8975f5c5SAndroid Build Coastguard Worker del self._port_mapping[host_port] 166*8975f5c5SAndroid Build Coastguard Worker return 167*8975f5c5SAndroid Build Coastguard Worker 168*8975f5c5SAndroid Build Coastguard Worker raise Exception('Unmap called for unknown port: %d' % device_port) 169*8975f5c5SAndroid Build Coastguard Worker 170*8975f5c5SAndroid Build Coastguard Worker 171*8975f5c5SAndroid Build Coastguard Worker# pylint: enable=invalid-name,missing-function-docstring 172*8975f5c5SAndroid Build Coastguard Worker 173*8975f5c5SAndroid Build Coastguard Worker 174*8975f5c5SAndroid Build Coastguard Workerdef setup_test_server(target_id: Optional[str], test_concurrency: int)\ 175*8975f5c5SAndroid Build Coastguard Worker -> Tuple[chrome_test_server_spawner.SpawningServer, str]: 176*8975f5c5SAndroid Build Coastguard Worker """Provisions a test server and configures |target_id| to use it. 177*8975f5c5SAndroid Build Coastguard Worker 178*8975f5c5SAndroid Build Coastguard Worker Args: 179*8975f5c5SAndroid Build Coastguard Worker target_id: The target to which port forwarding to the test server will 180*8975f5c5SAndroid Build Coastguard Worker be established. 181*8975f5c5SAndroid Build Coastguard Worker test_concurrency: The number of parallel test jobs that will be run. 182*8975f5c5SAndroid Build Coastguard Worker 183*8975f5c5SAndroid Build Coastguard Worker Returns a tuple of a SpawningServer object and the local url to use on 184*8975f5c5SAndroid Build Coastguard Worker |target_id| to reach the test server.""" 185*8975f5c5SAndroid Build Coastguard Worker 186*8975f5c5SAndroid Build Coastguard Worker logging.debug('Starting test server.') 187*8975f5c5SAndroid Build Coastguard Worker 188*8975f5c5SAndroid Build Coastguard Worker target_addr = get_ssh_address(target_id) 189*8975f5c5SAndroid Build Coastguard Worker 190*8975f5c5SAndroid Build Coastguard Worker # The TestLauncher can launch more jobs than the limit specified with 191*8975f5c5SAndroid Build Coastguard Worker # --test-launcher-jobs so the max number of spawned test servers is set to 192*8975f5c5SAndroid Build Coastguard Worker # twice that limit here. See https://crbug.com/913156#c19. 193*8975f5c5SAndroid Build Coastguard Worker spawning_server = chrome_test_server_spawner.SpawningServer( 194*8975f5c5SAndroid Build Coastguard Worker 0, SSHPortForwarder(target_addr), test_concurrency * 2) 195*8975f5c5SAndroid Build Coastguard Worker 196*8975f5c5SAndroid Build Coastguard Worker forwarded_port = port_forward(target_addr, spawning_server.server_port) 197*8975f5c5SAndroid Build Coastguard Worker spawning_server.Start() 198*8975f5c5SAndroid Build Coastguard Worker 199*8975f5c5SAndroid Build Coastguard Worker logging.debug('Test server listening for connections (port=%d)', 200*8975f5c5SAndroid Build Coastguard Worker spawning_server.server_port) 201*8975f5c5SAndroid Build Coastguard Worker logging.debug('Forwarded port is %d', forwarded_port) 202*8975f5c5SAndroid Build Coastguard Worker 203*8975f5c5SAndroid Build Coastguard Worker return (spawning_server, 'http://localhost:%d' % forwarded_port) 204