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