xref: /aosp_15_r20/external/autotest/server/hosts/rpc_server_tracker.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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