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