xref: /aosp_15_r20/external/autotest/server/hosts/gsc_devboard_host.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2022 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 Li# Expects to be run in an environment with sudo and no interactive password
7*9c5db199SXin Li# prompt, such as within the Chromium OS development chroot.
8*9c5db199SXin Li"""Host class for GSC devboard connected host."""
9*9c5db199SXin Li
10*9c5db199SXin Liimport contextlib
11*9c5db199SXin Liimport logging
12*9c5db199SXin Litry:
13*9c5db199SXin Li    import docker
14*9c5db199SXin Liexcept ImportError:
15*9c5db199SXin Li    logging.info("Docker API is not installed in this environment")
16*9c5db199SXin Li
17*9c5db199SXin LiDOCKER_IMAGE = "gcr.io/satlab-images/gsc_dev_board:release"
18*9c5db199SXin Li
19*9c5db199SXin LiSATLAB_DOCKER_HOST = 'tcp://192.168.231.1:2375'
20*9c5db199SXin LiLOCAL_DOCKER_HOST = 'tcp://127.0.0.1:2375'
21*9c5db199SXin LiDEFAULT_DOCKER_HOST = 'unix:///var/run/docker.sock'
22*9c5db199SXin Li
23*9c5db199SXin LiDEFAULT_SERVICE_PORT = 39999
24*9c5db199SXin Li
25*9c5db199SXin LiULTRADEBUG = '18d1:0304'
26*9c5db199SXin Li
27*9c5db199SXin Li
28*9c5db199SXin Liclass GSCDevboardHost(object):
29*9c5db199SXin Li    """
30*9c5db199SXin Li    A host that is physically connected to a GSC devboard.
31*9c5db199SXin Li
32*9c5db199SXin Li    It could either be a SDK workstation (chroot) or a SatLab box.
33*9c5db199SXin Li    """
34*9c5db199SXin Li
35*9c5db199SXin Li    def _initialize(self,
36*9c5db199SXin Li                    hostname,
37*9c5db199SXin Li                    service_debugger_serial=None,
38*9c5db199SXin Li                    service_ip=None,
39*9c5db199SXin Li                    service_port=DEFAULT_SERVICE_PORT,
40*9c5db199SXin Li                    *args,
41*9c5db199SXin Li                    **dargs):
42*9c5db199SXin Li        """Construct a GSCDevboardHost object.
43*9c5db199SXin Li
44*9c5db199SXin Li        @hostname: Name of the devboard host, will be used in future to look up
45*9c5db199SXin Li                   the debugger serial, not currently used.
46*9c5db199SXin Li        @service_debugger_serial: debugger connected to devboard, defaults to
47*9c5db199SXin Li                                  the first one found on the container.
48*9c5db199SXin Li        @service_ip: devboard service ip, default is to start a new container.
49*9c5db199SXin Li        @service_port: devboard service port, defaults to 39999.
50*9c5db199SXin Li        """
51*9c5db199SXin Li
52*9c5db199SXin Li        # Use docker host from environment or by probing a list of candidates.
53*9c5db199SXin Li        self._client = None
54*9c5db199SXin Li        try:
55*9c5db199SXin Li            self._client = docker.from_env()
56*9c5db199SXin Li            logging.info("Created docker host from env")
57*9c5db199SXin Li        except NameError:
58*9c5db199SXin Li            raise NameError('Please install docker using '
59*9c5db199SXin Li                            '"autotest/files/utils/install_docker_chroot.sh"')
60*9c5db199SXin Li        except docker.errors.DockerException:
61*9c5db199SXin Li            docker_host = None
62*9c5db199SXin Li            candidate_hosts = [
63*9c5db199SXin Li                    SATLAB_DOCKER_HOST, DEFAULT_DOCKER_HOST, LOCAL_DOCKER_HOST
64*9c5db199SXin Li            ]
65*9c5db199SXin Li            for h in candidate_hosts:
66*9c5db199SXin Li                try:
67*9c5db199SXin Li                    c = docker.DockerClient(base_url=h, timeout=2)
68*9c5db199SXin Li                    c.close()
69*9c5db199SXin Li                    docker_host = h
70*9c5db199SXin Li                    break
71*9c5db199SXin Li                except docker.errors.DockerException:
72*9c5db199SXin Li                    pass
73*9c5db199SXin Li            if docker_host is not None:
74*9c5db199SXin Li                self._client = docker.DockerClient(base_url=docker_host,
75*9c5db199SXin Li                                                   timeout=300)
76*9c5db199SXin Li            else:
77*9c5db199SXin Li                raise ValueError('Invalid DOCKER_HOST, ensure dockerd is'
78*9c5db199SXin Li                                 ' running.')
79*9c5db199SXin Li            logging.info("Using docker host at %s", docker_host)
80*9c5db199SXin Li
81*9c5db199SXin Li        self._satlab = False
82*9c5db199SXin Li        # GSCDevboardHost should only be created on Satlab or localhost, so
83*9c5db199SXin Li        # assume Satlab if a drone container is running.
84*9c5db199SXin Li        if len(self._client.containers.list(filters={'name': 'drone'})) > 0:
85*9c5db199SXin Li            logging.info("In Satlab")
86*9c5db199SXin Li            self._satlab = True
87*9c5db199SXin Li
88*9c5db199SXin Li        self._service_debugger_serial = service_debugger_serial
89*9c5db199SXin Li        self._service_ip = service_ip
90*9c5db199SXin Li        self._service_port = service_port
91*9c5db199SXin Li        logging.info("Using service port %s", self._service_port)
92*9c5db199SXin Li
93*9c5db199SXin Li        self._docker_network = 'default_satlab' if self._satlab else 'host'
94*9c5db199SXin Li        self._docker_container = None
95*9c5db199SXin Li
96*9c5db199SXin Li        serials = self._list_debugger_serials()
97*9c5db199SXin Li        if len(serials) == 0:
98*9c5db199SXin Li            raise ValueError('No debuggers found')
99*9c5db199SXin Li        logging.info("Available debuggers: [%s]", ', '.join(serials))
100*9c5db199SXin Li
101*9c5db199SXin Li        if self._service_debugger_serial is None:
102*9c5db199SXin Li            self._service_debugger_serial = serials[0]
103*9c5db199SXin Li        else:
104*9c5db199SXin Li            if self._service_debugger_serial not in serials:
105*9c5db199SXin Li                raise ValueError(
106*9c5db199SXin Li                        '%s debugger not found in [%s]' %
107*9c5db199SXin Li                        (self._service_debugger_serial, ', '.join(serials)))
108*9c5db199SXin Li        logging.info("Using debugger %s", self._service_debugger_serial)
109*9c5db199SXin Li        self._docker_container_name = "gsc_dev_board_{}".format(
110*9c5db199SXin Li                self._service_debugger_serial)
111*9c5db199SXin Li
112*9c5db199SXin Li    def _list_debugger_serials(self):
113*9c5db199SXin Li        """List all attached debuggers."""
114*9c5db199SXin Li
115*9c5db199SXin Li        c = self._client.containers.run(DOCKER_IMAGE,
116*9c5db199SXin Li                                        remove=True,
117*9c5db199SXin Li                                        privileged=True,
118*9c5db199SXin Li                                        name='list_debugger_serial',
119*9c5db199SXin Li                                        hostname='list_debugger_serial',
120*9c5db199SXin Li                                        detach=True,
121*9c5db199SXin Li                                        volumes=["/dev:/hostdev"],
122*9c5db199SXin Li                                        command=['sleep', '5'])
123*9c5db199SXin Li
124*9c5db199SXin Li        res, output = c.exec_run(['lsusb', '-v', '-d', ULTRADEBUG],
125*9c5db199SXin Li                                 stderr=False,
126*9c5db199SXin Li                                 privileged=True)
127*9c5db199SXin Li        c.kill()
128*9c5db199SXin Li        if res != 0:
129*9c5db199SXin Li            return []
130*9c5db199SXin Li        output = output.decode("utf-8").split('\n')
131*9c5db199SXin Li        serials = [
132*9c5db199SXin Li                l.strip().split(' ')[-1] for l in output
133*9c5db199SXin Li                if l.strip()[:7] == 'iSerial'
134*9c5db199SXin Li        ]
135*9c5db199SXin Li        return serials
136*9c5db199SXin Li
137*9c5db199SXin Li    @contextlib.contextmanager
138*9c5db199SXin Li    def service_context(self):
139*9c5db199SXin Li        """Service context manager that provides the service endpoint."""
140*9c5db199SXin Li        self.start_service()
141*9c5db199SXin Li        try:
142*9c5db199SXin Li            yield "{}:{}".format(self.service_ip, self.service_port)
143*9c5db199SXin Li        finally:
144*9c5db199SXin Li            self.stop_service()
145*9c5db199SXin Li
146*9c5db199SXin Li    def start_service(self):
147*9c5db199SXin Li        """Starts service if needed."""
148*9c5db199SXin Li        if self._docker_container is not None:
149*9c5db199SXin Li            return
150*9c5db199SXin Li
151*9c5db199SXin Li        if self._service_ip:
152*9c5db199SXin Li            # Assume container was manually started if service_ip was set
153*9c5db199SXin Li            logging.info("Skip start_service due to set service_ip")
154*9c5db199SXin Li            return
155*9c5db199SXin Li
156*9c5db199SXin Li        #TODO(b/215767105): Pull image onto Satlab box if not present.
157*9c5db199SXin Li
158*9c5db199SXin Li        environment = {
159*9c5db199SXin Li                'DEVBOARDSVC_PORT': self._service_port,
160*9c5db199SXin Li                'DEBUGGER_SERIAL': self._service_debugger_serial
161*9c5db199SXin Li        }
162*9c5db199SXin Li        start_cmd = ['/opt/gscdevboard/start_devboardsvc.sh']
163*9c5db199SXin Li
164*9c5db199SXin Li        # Stop any leftover containers
165*9c5db199SXin Li        try:
166*9c5db199SXin Li            c = self._client.containers.get(self._docker_container_name)
167*9c5db199SXin Li            c.kill()
168*9c5db199SXin Li        except docker.errors.NotFound:
169*9c5db199SXin Li            pass
170*9c5db199SXin Li
171*9c5db199SXin Li        self._client.containers.run(DOCKER_IMAGE,
172*9c5db199SXin Li                                    remove=True,
173*9c5db199SXin Li                                    privileged=True,
174*9c5db199SXin Li                                    name=self._docker_container_name,
175*9c5db199SXin Li                                    hostname=self._docker_container_name,
176*9c5db199SXin Li                                    network=self._docker_network,
177*9c5db199SXin Li                                    cap_add=["NET_ADMIN"],
178*9c5db199SXin Li                                    detach=True,
179*9c5db199SXin Li                                    volumes=["/dev:/hostdev"],
180*9c5db199SXin Li                                    environment=environment,
181*9c5db199SXin Li                                    command=start_cmd)
182*9c5db199SXin Li
183*9c5db199SXin Li        # A separate containers.get call is needed to capture network attributes
184*9c5db199SXin Li        self._docker_container = self._client.containers.get(
185*9c5db199SXin Li                self._docker_container_name)
186*9c5db199SXin Li
187*9c5db199SXin Li    def stop_service(self):
188*9c5db199SXin Li        """Stops service by killing the container."""
189*9c5db199SXin Li        if self._docker_container is None:
190*9c5db199SXin Li            return
191*9c5db199SXin Li        self._docker_container.kill()
192*9c5db199SXin Li        self._docker_container = None
193*9c5db199SXin Li
194*9c5db199SXin Li    @property
195*9c5db199SXin Li    def service_port(self):
196*9c5db199SXin Li        """Return service port (local to the container host)."""
197*9c5db199SXin Li        return self._service_port
198*9c5db199SXin Li
199*9c5db199SXin Li    @property
200*9c5db199SXin Li    def service_ip(self):
201*9c5db199SXin Li        """Return service ip (local to the container host)."""
202*9c5db199SXin Li        if self._service_ip is not None:
203*9c5db199SXin Li            return self._service_ip
204*9c5db199SXin Li
205*9c5db199SXin Li        if self._docker_network == 'host':
206*9c5db199SXin Li            return '127.0.0.1'
207*9c5db199SXin Li        else:
208*9c5db199SXin Li            if self._docker_container is None:
209*9c5db199SXin Li                return ''
210*9c5db199SXin Li            else:
211*9c5db199SXin Li                settings = self._docker_container.attrs['NetworkSettings']
212*9c5db199SXin Li                return settings['Networks'][self._docker_network]['IPAddress']
213