1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright (c) 2015 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Liimport six.moves.http_client 7*9c5db199SXin Liimport logging 8*9c5db199SXin Liimport socket 9*9c5db199SXin Liimport tempfile 10*9c5db199SXin Liimport time 11*9c5db199SXin Liimport six.moves.xmlrpc_client 12*9c5db199SXin Li 13*9c5db199SXin Liimport common 14*9c5db199SXin Lifrom autotest_lib.client.bin import utils 15*9c5db199SXin Lifrom autotest_lib.client.common_lib import error 16*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import retry 17*9c5db199SXin Li 18*9c5db199SXin Li 19*9c5db199SXin Liclass RpcServerTracker(object): 20*9c5db199SXin Li """ 21*9c5db199SXin Li This class keeps track of all the RPC server connections started on a 22*9c5db199SXin Li remote host. The caller can use either |xmlrpc_connect| to start the 23*9c5db199SXin Li required type of rpc server on the remote host. 24*9c5db199SXin Li The host will cleanup all the open RPC server connections on disconnect. 25*9c5db199SXin Li """ 26*9c5db199SXin Li 27*9c5db199SXin Li _RPC_PROXY_URL_FORMAT = 'http://localhost:%d' 28*9c5db199SXin Li _RPC_HOST_ADDRESS_FORMAT = 'localhost:%d' 29*9c5db199SXin Li _RPC_SHUTDOWN_POLLING_PERIOD_SECONDS = 2 30*9c5db199SXin Li _RPC_SHUTDOWN_TIMEOUT_SECONDS = 10 31*9c5db199SXin Li 32*9c5db199SXin Li def __init__(self, host): 33*9c5db199SXin Li """ 34*9c5db199SXin Li @param port: The host object associated with this instance of 35*9c5db199SXin Li RpcServerTracker. 36*9c5db199SXin Li """ 37*9c5db199SXin Li self._host = host 38*9c5db199SXin Li self._rpc_proxy_map = {} 39*9c5db199SXin Li 40*9c5db199SXin Li 41*9c5db199SXin Li def _setup_port(self, port, command_name, remote_pid=None): 42*9c5db199SXin Li """Sets up a tunnel process and register it to rpc_server_tracker. 43*9c5db199SXin Li 44*9c5db199SXin Li ChromeOS on the target closes down most external ports for security. 45*9c5db199SXin Li We could open the port, but doing that would conflict with security 46*9c5db199SXin Li tests that check that only expected ports are open. So, to get to 47*9c5db199SXin Li the port on the target we use an ssh tunnel. 48*9c5db199SXin Li 49*9c5db199SXin Li This method assumes that xmlrpc and jsonrpc never conflict, since 50*9c5db199SXin Li we can only either have an xmlrpc or a jsonrpc server listening on 51*9c5db199SXin Li a remote port. As such, it enforces a single proxy->remote port 52*9c5db199SXin Li policy, i.e if one starts a jsonrpc proxy/server from port A->B, 53*9c5db199SXin Li and then tries to start an xmlrpc proxy forwarded to the same port, 54*9c5db199SXin Li the xmlrpc proxy will override the jsonrpc tunnel process, however: 55*9c5db199SXin Li 56*9c5db199SXin Li 1. None of the methods on the xmlrpc proxy will work because 57*9c5db199SXin Li the server listening on B is jsonrpc. 58*9c5db199SXin Li 59*9c5db199SXin Li 2. The xmlrpc client cannot initiate a termination of the JsonRPC 60*9c5db199SXin Li server, as the only use case currently is goofy, which is tied to 61*9c5db199SXin Li the factory image. It is much easier to handle a failed xmlrpc 62*9c5db199SXin Li call on the client than it is to terminate goofy in this scenario, 63*9c5db199SXin Li as doing the latter might leave the DUT in a hard to recover state. 64*9c5db199SXin Li 65*9c5db199SXin Li With the current implementation newer rpc proxy connections will 66*9c5db199SXin Li terminate the tunnel processes of older rpc connections tunneling 67*9c5db199SXin Li to the same remote port. If methods are invoked on the client 68*9c5db199SXin Li after this has happened they will fail with connection closed errors. 69*9c5db199SXin Li 70*9c5db199SXin Li @param port: The remote forwarding port. 71*9c5db199SXin Li @param command_name: The name of the remote process, to terminate 72*9c5db199SXin Li using pkill. 73*9c5db199SXin Li @param remote_pid: The PID of the remote background process 74*9c5db199SXin Li as a string. 75*9c5db199SXin Li 76*9c5db199SXin Li @return the local port which is used for port forwarding on the ssh 77*9c5db199SXin Li client. 78*9c5db199SXin Li """ 79*9c5db199SXin Li self.disconnect(port) 80*9c5db199SXin Li local_port = utils.get_unused_port() 81*9c5db199SXin Li tunnel_proc = self._host.create_ssh_tunnel(port, local_port) 82*9c5db199SXin Li self._rpc_proxy_map[port] = (command_name, tunnel_proc, remote_pid) 83*9c5db199SXin Li return local_port 84*9c5db199SXin Li 85*9c5db199SXin Li 86*9c5db199SXin Li def _setup_rpc(self, port, command_name, remote_pid=None): 87*9c5db199SXin Li """Construct a URL for an rpc connection using ssh tunnel. 88*9c5db199SXin Li 89*9c5db199SXin Li @param port: The remote forwarding port. 90*9c5db199SXin Li @param command_name: The name of the remote process, to terminate 91*9c5db199SXin Li using pkill. 92*9c5db199SXin Li @param remote_pid: The PID of the remote background process 93*9c5db199SXin Li as a string. 94*9c5db199SXin Li 95*9c5db199SXin Li @return a url that we can use to initiate the rpc connection. 96*9c5db199SXin Li """ 97*9c5db199SXin Li return self._RPC_PROXY_URL_FORMAT % self._setup_port( 98*9c5db199SXin Li port, command_name, remote_pid=remote_pid) 99*9c5db199SXin Li 100*9c5db199SXin Li 101*9c5db199SXin Li def tunnel_connect(self, port): 102*9c5db199SXin Li """Construct a host address using ssh tunnel. 103*9c5db199SXin Li 104*9c5db199SXin Li @param port: The remote forwarding port. 105*9c5db199SXin Li 106*9c5db199SXin Li @return a host address using ssh tunnel. 107*9c5db199SXin Li """ 108*9c5db199SXin Li return self._RPC_HOST_ADDRESS_FORMAT % self._setup_port(port, None) 109*9c5db199SXin Li 110*9c5db199SXin Li 111*9c5db199SXin Li def xmlrpc_connect(self, command, port, command_name=None, 112*9c5db199SXin Li ready_test_name=None, timeout_seconds=10, 113*9c5db199SXin Li logfile=None, request_timeout_seconds=None, 114*9c5db199SXin Li server_desc=None): 115*9c5db199SXin Li """Connect to an XMLRPC server on the host. 116*9c5db199SXin Li 117*9c5db199SXin Li The `command` argument should be a simple shell command that 118*9c5db199SXin Li starts an XMLRPC server on the given `port`. The command 119*9c5db199SXin Li must not daemonize, and must terminate cleanly on SIGTERM. 120*9c5db199SXin Li The command is started in the background on the host, and a 121*9c5db199SXin Li local XMLRPC client for the server is created and returned 122*9c5db199SXin Li to the caller. 123*9c5db199SXin Li 124*9c5db199SXin Li Note that the process of creating an XMLRPC client makes no 125*9c5db199SXin Li attempt to connect to the remote server; the caller is 126*9c5db199SXin Li responsible for determining whether the server is running 127*9c5db199SXin Li correctly, and is ready to serve requests. 128*9c5db199SXin Li 129*9c5db199SXin Li Optionally, the caller can pass ready_test_name, a string 130*9c5db199SXin Li containing the name of a method to call on the proxy. This 131*9c5db199SXin Li method should take no parameters and return successfully only 132*9c5db199SXin Li when the server is ready to process client requests. When 133*9c5db199SXin Li ready_test_name is set, xmlrpc_connect will block until the 134*9c5db199SXin Li proxy is ready, and throw a TestError if the server isn't 135*9c5db199SXin Li ready by timeout_seconds. 136*9c5db199SXin Li 137*9c5db199SXin Li If a server is already running on the remote port, this 138*9c5db199SXin Li method will kill it and disconnect the tunnel process 139*9c5db199SXin Li associated with the connection before establishing a new one, 140*9c5db199SXin Li by consulting the rpc_proxy_map in disconnect. 141*9c5db199SXin Li 142*9c5db199SXin Li @param command Shell command to start the server. 143*9c5db199SXin Li @param port Port number on which the server is expected to 144*9c5db199SXin Li be serving. 145*9c5db199SXin Li @param command_name String to use as input to `pkill` to 146*9c5db199SXin Li terminate the XMLRPC server on the host. 147*9c5db199SXin Li @param ready_test_name String containing the name of a 148*9c5db199SXin Li method defined on the XMLRPC server. 149*9c5db199SXin Li @param timeout_seconds Number of seconds to wait 150*9c5db199SXin Li for the server to become 'ready.' Will throw a 151*9c5db199SXin Li TestFail error if server is not ready in time. 152*9c5db199SXin Li @param logfile Logfile to send output when running 153*9c5db199SXin Li 'command' argument. 154*9c5db199SXin Li @param request_timeout_seconds Timeout in seconds for an XMLRPC request. 155*9c5db199SXin Li @param server_desc: Extra text to report in socket.error descriptions. 156*9c5db199SXin Li 157*9c5db199SXin Li """ 158*9c5db199SXin Li # Clean up any existing state. If the caller is willing 159*9c5db199SXin Li # to believe their server is down, we ought to clean up 160*9c5db199SXin Li # any tunnels we might have sitting around. 161*9c5db199SXin Li self.disconnect(port) 162*9c5db199SXin Li remote_pid = None 163*9c5db199SXin Li if command is not None: 164*9c5db199SXin Li if logfile: 165*9c5db199SXin Li remote_cmd = '%s > %s 2>&1' % (command, logfile) 166*9c5db199SXin Li else: 167*9c5db199SXin Li remote_cmd = command 168*9c5db199SXin Li remote_pid = self._host.run_background(remote_cmd) 169*9c5db199SXin Li logging.debug('Started XMLRPC server on host %s, pid = %s', 170*9c5db199SXin Li self._host.hostname, remote_pid) 171*9c5db199SXin Li 172*9c5db199SXin Li # Tunnel through SSH to be able to reach that remote port. 173*9c5db199SXin Li rpc_url = self._setup_rpc(port, command_name, remote_pid=remote_pid) 174*9c5db199SXin Li if not server_desc: 175*9c5db199SXin Li server_desc = "<%s '%s:%s'>" % (command_name or 'XMLRPC', 176*9c5db199SXin Li self._host.hostname, port) 177*9c5db199SXin Li server_desc = '%s (%s)' % (server_desc, rpc_url.replace('http://', '')) 178*9c5db199SXin Li if request_timeout_seconds is not None: 179*9c5db199SXin Li proxy = TimeoutXMLRPCServerProxy( 180*9c5db199SXin Li rpc_url, timeout=request_timeout_seconds, allow_none=True) 181*9c5db199SXin Li else: 182*9c5db199SXin Li proxy = six.moves.xmlrpc_client.ServerProxy(rpc_url, allow_none=True) 183*9c5db199SXin Li 184*9c5db199SXin Li if ready_test_name is not None: 185*9c5db199SXin Li # retry.retry logs each attempt; calculate delay_sec to 186*9c5db199SXin Li # keep log spam to a dull roar. 187*9c5db199SXin Li @retry.retry((socket.error, 188*9c5db199SXin Li six.moves.xmlrpc_client.ProtocolError, 189*9c5db199SXin Li six.moves.http_client.BadStatusLine), 190*9c5db199SXin Li timeout_min=timeout_seconds / 60.0, 191*9c5db199SXin Li delay_sec=min(max(timeout_seconds / 20.0, 0.1), 1)) 192*9c5db199SXin Li def ready_test(): 193*9c5db199SXin Li """ Call proxy.ready_test_name(). """ 194*9c5db199SXin Li try: 195*9c5db199SXin Li getattr(proxy, ready_test_name)() 196*9c5db199SXin Li except socket.error as e: 197*9c5db199SXin Li e.filename = server_desc 198*9c5db199SXin Li raise 199*9c5db199SXin Li 200*9c5db199SXin Li try: 201*9c5db199SXin Li logging.info('Waiting %d seconds for XMLRPC server ' 202*9c5db199SXin Li 'to start.', timeout_seconds) 203*9c5db199SXin Li ready_test() 204*9c5db199SXin Li except Exception as exc: 205*9c5db199SXin Li log_lines = [] 206*9c5db199SXin Li if logfile: 207*9c5db199SXin Li logging.warning('Failed to start XMLRPC server; getting log.') 208*9c5db199SXin Li with tempfile.NamedTemporaryFile() as temp: 209*9c5db199SXin Li self._host.get_file(logfile, temp.name) 210*9c5db199SXin Li with open(temp.name) as f: 211*9c5db199SXin Li log_lines = f.read().rstrip().splitlines() 212*9c5db199SXin Li else: 213*9c5db199SXin Li logging.warning('Failed to start XMLRPC server; no log.') 214*9c5db199SXin Li 215*9c5db199SXin Li logging.error( 216*9c5db199SXin Li 'Failed to start XMLRPC server: %s.%s: %s.', 217*9c5db199SXin Li type(exc).__module__, type(exc).__name__, 218*9c5db199SXin Li str(exc).rstrip('.')) 219*9c5db199SXin Li 220*9c5db199SXin Li if isinstance(exc, six.moves.http_client.BadStatusLine): 221*9c5db199SXin Li # BadStatusLine: inject the last log line into the message, 222*9c5db199SXin Li # using the 'line' and 'args' attributes. 223*9c5db199SXin Li if log_lines: 224*9c5db199SXin Li if exc.line: 225*9c5db199SXin Li exc.line = '%s -- Log tail: %r' % ( 226*9c5db199SXin Li exc.line, log_lines[-1]) 227*9c5db199SXin Li else: 228*9c5db199SXin Li exc.line = 'Log tail: %r' % ( 229*9c5db199SXin Li log_lines[-1]) 230*9c5db199SXin Li exc.args = (exc.line,) 231*9c5db199SXin Li elif isinstance(exc, socket.error): 232*9c5db199SXin Li # socket.error: inject the last log line into the message, 233*9c5db199SXin Li # using the 'filename' attribute. 234*9c5db199SXin Li if log_lines: 235*9c5db199SXin Li if exc.filename: 236*9c5db199SXin Li exc.filename = '%s -- Log tail: %r' % ( 237*9c5db199SXin Li exc.filename, log_lines[-1]) 238*9c5db199SXin Li else: 239*9c5db199SXin Li exc.filename = 'Log tail: %r' % log_lines[-1] 240*9c5db199SXin Li elif log_lines: 241*9c5db199SXin Li # Unusual failure: can't inject the last log line, 242*9c5db199SXin Li # so report it via logging. 243*9c5db199SXin Li logging.error('Log tail: %r', log_lines[-1]) 244*9c5db199SXin Li 245*9c5db199SXin Li if len(log_lines) > 1: 246*9c5db199SXin Li # The failure messages include only the last line, 247*9c5db199SXin Li # so report the whole thing if it had more lines. 248*9c5db199SXin Li logging.error('Full XMLRPC server log:\n%s', 249*9c5db199SXin Li '\n'.join(log_lines)) 250*9c5db199SXin Li 251*9c5db199SXin Li self.disconnect(port) 252*9c5db199SXin Li raise 253*9c5db199SXin Li logging.info('XMLRPC server started successfully.') 254*9c5db199SXin Li return proxy 255*9c5db199SXin Li 256*9c5db199SXin Li def disconnect(self, port, pkill=True): 257*9c5db199SXin Li """Disconnect from an RPC server on the host. 258*9c5db199SXin Li 259*9c5db199SXin Li Terminates the remote RPC server previously started for 260*9c5db199SXin Li the given `port`. Also closes the local ssh tunnel created 261*9c5db199SXin Li for the connection to the host. This function does not 262*9c5db199SXin Li directly alter the state of a previously returned RPC 263*9c5db199SXin Li client object; however disconnection will cause all 264*9c5db199SXin Li subsequent calls to methods on the object to fail. 265*9c5db199SXin Li 266*9c5db199SXin Li This function does nothing if requested to disconnect a port 267*9c5db199SXin Li that was not previously connected via _setup_rpc. 268*9c5db199SXin Li 269*9c5db199SXin Li @param port Port number passed to a previous call to `_setup_rpc()`. 270*9c5db199SXin Li @param pkill: if True, ssh in to the server and pkill the process. 271*9c5db199SXin Li """ 272*9c5db199SXin Li if port not in self._rpc_proxy_map: 273*9c5db199SXin Li return 274*9c5db199SXin Li remote_name, tunnel_proc, remote_pid = self._rpc_proxy_map[port] 275*9c5db199SXin Li if pkill and remote_name: 276*9c5db199SXin Li # We use 'pkill' to find our target process rather than 277*9c5db199SXin Li # a PID, because the host may have rebooted since 278*9c5db199SXin Li # connecting, and we don't want to kill an innocent 279*9c5db199SXin Li # process with the same PID. 280*9c5db199SXin Li # 281*9c5db199SXin Li # 'pkill' helpfully exits with status 1 if no target 282*9c5db199SXin Li # process is found, for which run() will throw an 283*9c5db199SXin Li # exception. We don't want that, so we the ignore 284*9c5db199SXin Li # status. 285*9c5db199SXin Li self._host.run("pkill -f '%s'" % remote_name, ignore_status=True) 286*9c5db199SXin Li if remote_pid: 287*9c5db199SXin Li logging.info('Waiting for RPC server "%s" shutdown (%s)', 288*9c5db199SXin Li remote_name, remote_pid) 289*9c5db199SXin Li start_time = time.time() 290*9c5db199SXin Li while (time.time() - start_time < 291*9c5db199SXin Li self._RPC_SHUTDOWN_TIMEOUT_SECONDS): 292*9c5db199SXin Li running_processes = self._host.run( 293*9c5db199SXin Li "pgrep -f '%s'" % remote_name, 294*9c5db199SXin Li ignore_status=True).stdout.split() 295*9c5db199SXin Li if not remote_pid in running_processes: 296*9c5db199SXin Li logging.info('Shut down RPC server %s.', remote_pid) 297*9c5db199SXin Li break 298*9c5db199SXin Li time.sleep(self._RPC_SHUTDOWN_POLLING_PERIOD_SECONDS) 299*9c5db199SXin Li self._host.run("pkill -9 -f '%s'" % remote_name, 300*9c5db199SXin Li ignore_status=True) 301*9c5db199SXin Li else: 302*9c5db199SXin Li raise error.TestError('Failed to shutdown RPC server %s' % 303*9c5db199SXin Li remote_name) 304*9c5db199SXin Li 305*9c5db199SXin Li self._host.disconnect_ssh_tunnel(tunnel_proc) 306*9c5db199SXin Li del self._rpc_proxy_map[port] 307*9c5db199SXin Li 308*9c5db199SXin Li 309*9c5db199SXin Li def disconnect_all(self): 310*9c5db199SXin Li """Disconnect all known RPC proxy ports.""" 311*9c5db199SXin Li for port in list(self._rpc_proxy_map.keys()): 312*9c5db199SXin Li self.disconnect(port) 313*9c5db199SXin Li 314*9c5db199SXin Li 315*9c5db199SXin Liclass TimeoutXMLRPCServerProxy(six.moves.xmlrpc_client.ServerProxy): 316*9c5db199SXin Li """XMLRPC ServerProxy supporting timeout.""" 317*9c5db199SXin Li def __init__(self, uri, timeout=20, *args, **kwargs): 318*9c5db199SXin Li """Initializes a TimeoutXMLRPCServerProxy. 319*9c5db199SXin Li 320*9c5db199SXin Li @param uri: URI to a XMLRPC server. 321*9c5db199SXin Li @param timeout: Timeout in seconds for a XMLRPC request. 322*9c5db199SXin Li @param *args: args to xmlrpclib.ServerProxy. 323*9c5db199SXin Li @param **kwargs: kwargs to xmlrpclib.ServerProxy. 324*9c5db199SXin Li 325*9c5db199SXin Li """ 326*9c5db199SXin Li if timeout: 327*9c5db199SXin Li kwargs['transport'] = TimeoutXMLRPCTransport(timeout=timeout) 328*9c5db199SXin Li six.moves.xmlrpc_client.ServerProxy.__init__(self, uri, *args, **kwargs) 329*9c5db199SXin Li 330*9c5db199SXin Li 331*9c5db199SXin Liclass TimeoutXMLRPCTransport(six.moves.xmlrpc_client.Transport): 332*9c5db199SXin Li """A Transport subclass supporting timeout.""" 333*9c5db199SXin Li def __init__(self, timeout=20, *args, **kwargs): 334*9c5db199SXin Li """Initializes a TimeoutXMLRPCTransport. 335*9c5db199SXin Li 336*9c5db199SXin Li @param timeout: Timeout in seconds for a HTTP request through this transport layer. 337*9c5db199SXin Li @param *args: args to xmlrpclib.Transport. 338*9c5db199SXin Li @param **kwargs: kwargs to xmlrpclib.Transport. 339*9c5db199SXin Li 340*9c5db199SXin Li """ 341*9c5db199SXin Li six.moves.xmlrpc_client.Transport.__init__(self, *args, **kwargs) 342*9c5db199SXin Li self.timeout = timeout 343*9c5db199SXin Li 344*9c5db199SXin Li 345*9c5db199SXin Li def make_connection(self, host): 346*9c5db199SXin Li """Overwrites make_connection in xmlrpclib.Transport with timeout. 347*9c5db199SXin Li 348*9c5db199SXin Li @param host: Host address to connect. 349*9c5db199SXin Li 350*9c5db199SXin Li @return: A httplib.HTTPConnection connecting to host with timeout. 351*9c5db199SXin Li 352*9c5db199SXin Li """ 353*9c5db199SXin Li conn = six.moves.http_client.HTTPConnection(host, timeout=self.timeout) 354*9c5db199SXin Li return conn 355