1# Copyright 2018 - The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14r"""Instance class. 15 16Define the instance class used to hold details about an AVD instance. 17 18The instance class will hold details about AVD instances (remote/local) used to 19enable users to understand what instances they've created. This will be leveraged 20for the list, delete, and reconnect commands. 21 22The details include: 23- instance name (for remote instances) 24- creation date/instance duration 25- instance image details (branch/target/build id) 26- and more! 27""" 28 29import collections 30import datetime 31import logging 32import os 33import re 34import subprocess 35import tempfile 36 37# pylint: disable=import-error 38import dateutil.parser 39import dateutil.tz 40 41from acloud.create import local_image_local_instance 42from acloud.internal import constants 43from acloud.internal.lib import cvd_runtime_config 44from acloud.internal.lib import utils 45from acloud.internal.lib.adb_tools import AdbTools 46from acloud.internal.lib.local_instance_lock import LocalInstanceLock 47 48 49logger = logging.getLogger(__name__) 50 51_ACLOUD_CVD_TEMP = os.path.join(tempfile.gettempdir(), "acloud_cvd_temp") 52_CVD_RUNTIME_FOLDER_NAME = "cuttlefish_runtime" 53_CVD_STATUS_BIN = "cvd_status" 54_LOCAL_INSTANCE_NAME_FORMAT = "local-instance-%(id)d" 55_LOCAL_INSTANCE_NAME_PATTERN = re.compile(r"^local-instance-(?P<id>\d+)$") 56_ACLOUDWEB_INSTANCE_START_STRING = "cf-" 57_MSG_UNABLE_TO_CALCULATE = "Unable to calculate" 58_NO_ANDROID_ENV = "android source not available" 59_RE_GROUP_ADB = "local_adb_port" 60_RE_GROUP_VNC = "local_vnc_port" 61_RE_SSH_TUNNEL_PATTERN = (r"((.*\s*-L\s)(?P<%s>\d+):127.0.0.1:%s)" 62 r"((.*\s*-L\s)(?P<%s>\d+):127.0.0.1:%s)" 63 r"(.+%s)") 64_RE_TIMEZONE = re.compile(r"^(?P<time>[0-9\-\.:T]*)(?P<timezone>[+-]\d+:\d+)$") 65 66_COMMAND_PS_LAUNCH_CVD = ["ps", "-wweo", "lstart,cmd"] 67_RE_RUN_CVD = re.compile(r"(?P<date_str>^[^/]+)(.*run_cvd)") 68_DISPLAY_STRING = "%(x_res)sx%(y_res)s (%(dpi)s)" 69_RE_ZONE = re.compile(r".+/zones/(?P<zone>.+)$") 70_LOCAL_ZONE = "local" 71_FULL_NAME_STRING = ("device serial: %(device_serial)s (%(instance_name)s) " 72 "elapsed time: %(elapsed_time)s") 73_INDENT = " " * 3 74LocalPorts = collections.namedtuple("LocalPorts", [constants.VNC_PORT, 75 constants.ADB_PORT]) 76 77 78def GetDefaultCuttlefishConfig(): 79 """Get the path of default cuttlefish instance config. 80 81 Return: 82 String, path of cf runtime config. 83 """ 84 cfg_path = os.path.join(os.path.expanduser("~"), _CVD_RUNTIME_FOLDER_NAME, 85 constants.CUTTLEFISH_CONFIG_FILE) 86 if os.path.isfile(cfg_path): 87 return cfg_path 88 return None 89 90 91def GetLocalInstanceName(local_instance_id): 92 """Get local cuttlefish instance name by instance id. 93 94 Args: 95 local_instance_id: Integer of instance id. 96 97 Return: 98 String, the instance name. 99 """ 100 return _LOCAL_INSTANCE_NAME_FORMAT % {"id": local_instance_id} 101 102 103def GetLocalInstanceIdByName(name): 104 """Get local cuttlefish instance id by name. 105 106 Args: 107 name: String of instance name. 108 109 Return: 110 The instance id as an integer if the name is in valid format. 111 None if the name does not represent a local cuttlefish instance. 112 """ 113 match = _LOCAL_INSTANCE_NAME_PATTERN.match(name) 114 if match: 115 return int(match.group("id")) 116 return None 117 118 119def GetLocalInstanceConfig(local_instance_id): 120 """Get the path of instance config. 121 122 Args: 123 local_instance_id: Integer of instance id. 124 125 Return: 126 String, path of cf runtime config. 127 """ 128 cfg_path = os.path.join(GetLocalInstanceRuntimeDir(local_instance_id), 129 constants.CUTTLEFISH_CONFIG_FILE) 130 if os.path.isfile(cfg_path): 131 return cfg_path 132 return None 133 134 135def GetAllLocalInstanceConfigs(): 136 """Get all cuttlefish runtime configs from the known locations. 137 138 Return: 139 List of tuples. Each tuple consists of an instance id and a config 140 path. 141 """ 142 id_cfg_pairs = [] 143 # Check if any instance config is under home folder. 144 cfg_path = GetDefaultCuttlefishConfig() 145 if cfg_path: 146 id_cfg_pairs.append((1, cfg_path)) 147 148 # Check if any instance config is under acloud cvd temp folder. 149 if os.path.exists(_ACLOUD_CVD_TEMP): 150 for ins_name in os.listdir(_ACLOUD_CVD_TEMP): 151 ins_id = GetLocalInstanceIdByName(ins_name) 152 if ins_id is not None: 153 cfg_path = GetLocalInstanceConfig(ins_id) 154 if cfg_path: 155 id_cfg_pairs.append((ins_id, cfg_path)) 156 return id_cfg_pairs 157 158 159def GetLocalInstanceHomeDir(local_instance_id): 160 """Get local instance home dir according to instance id. 161 162 Args: 163 local_instance_id: Integer of instance id. 164 165 Return: 166 String, path of instance home dir. 167 """ 168 return os.path.join(_ACLOUD_CVD_TEMP, 169 GetLocalInstanceName(local_instance_id)) 170 171 172def GetLocalInstanceLock(local_instance_id): 173 """Get local instance lock. 174 175 Args: 176 local_instance_id: Integer of instance id. 177 178 Returns: 179 LocalInstanceLock object. 180 """ 181 file_path = os.path.join(_ACLOUD_CVD_TEMP, 182 GetLocalInstanceName(local_instance_id) + ".lock") 183 return LocalInstanceLock(file_path) 184 185 186def GetLocalInstanceRuntimeDir(local_instance_id): 187 """Get instance runtime dir 188 189 Args: 190 local_instance_id: Integer of instance id. 191 192 Return: 193 String, path of instance runtime dir. 194 """ 195 return os.path.join(GetLocalInstanceHomeDir(local_instance_id), 196 _CVD_RUNTIME_FOLDER_NAME) 197 198 199def _GetCurrentLocalTime(): 200 """Return a datetime object for current time in local time zone.""" 201 return datetime.datetime.now(dateutil.tz.tzlocal()) 202 203 204def _GetElapsedTime(start_time): 205 """Calculate the elapsed time from start_time till now. 206 207 Args: 208 start_time: String of instance created time. 209 210 Returns: 211 datetime.timedelta of elapsed time, _MSG_UNABLE_TO_CALCULATE for 212 datetime can't parse cases. 213 """ 214 match = _RE_TIMEZONE.match(start_time) 215 try: 216 # Check start_time has timezone or not. If timezone can't be found, 217 # use local timezone to get elapsed time. 218 if match: 219 return _GetCurrentLocalTime() - dateutil.parser.parse(start_time) 220 221 return _GetCurrentLocalTime() - dateutil.parser.parse( 222 start_time).replace(tzinfo=dateutil.tz.tzlocal()) 223 except ValueError: 224 logger.debug(("Can't parse datetime string(%s)."), start_time) 225 return _MSG_UNABLE_TO_CALCULATE 226 227 228# pylint: disable=useless-object-inheritance 229class Instance(object): 230 """Class to store data of instance.""" 231 232 # pylint: disable=too-many-locals 233 def __init__(self, name, fullname, display, ip, status=None, adb_port=None, 234 vnc_port=None, ssh_tunnel_is_connected=None, createtime=None, 235 elapsed_time=None, avd_type=None, avd_flavor=None, 236 is_local=False, device_information=None, zone=None, 237 webrtc_port=None): 238 self._name = name 239 self._fullname = fullname 240 self._status = status 241 self._display = display # Resolution and dpi 242 self._ip = ip 243 self._adb_port = adb_port # adb port which is forwarding to remote 244 self._vnc_port = vnc_port # vnc port which is forwarding to remote 245 self._webrtc_port = webrtc_port 246 # True if ssh tunnel is still connected 247 self._ssh_tunnel_is_connected = ssh_tunnel_is_connected 248 self._createtime = createtime 249 self._elapsed_time = elapsed_time 250 self._avd_type = avd_type 251 self._avd_flavor = avd_flavor 252 self._is_local = is_local # True if this is a local instance 253 self._device_information = device_information 254 self._zone = zone 255 256 def __repr__(self): 257 """Return full name property for print.""" 258 return self._fullname 259 260 def Summary(self): 261 """Let's make it easy to see what this class is holding.""" 262 representation = [] 263 representation.append(" name: %s" % self._name) 264 representation.append("%s IP: %s" % (_INDENT, self._ip)) 265 representation.append("%s create time: %s" % (_INDENT, self._createtime)) 266 representation.append("%s elapse time: %s" % (_INDENT, self._elapsed_time)) 267 representation.append("%s status: %s" % (_INDENT, self._status)) 268 representation.append("%s avd type: %s" % (_INDENT, self._avd_type)) 269 representation.append("%s display: %s" % (_INDENT, self._display)) 270 representation.append("%s vnc: 127.0.0.1:%s" % (_INDENT, self._vnc_port)) 271 representation.append("%s zone: %s" % (_INDENT, self._zone)) 272 representation.append("%s webrtc port: %s" % (_INDENT, self._webrtc_port)) 273 274 if self._adb_port and self._device_information: 275 representation.append("%s adb serial: 127.0.0.1:%s" % 276 (_INDENT, self._adb_port)) 277 representation.append("%s product: %s" % ( 278 _INDENT, self._device_information["product"])) 279 representation.append("%s model: %s" % ( 280 _INDENT, self._device_information["model"])) 281 representation.append("%s device: %s" % ( 282 _INDENT, self._device_information["device"])) 283 representation.append("%s transport_id: %s" % ( 284 _INDENT, self._device_information["transport_id"])) 285 else: 286 representation.append("%s adb serial: disconnected" % _INDENT) 287 288 return "\n".join(representation) 289 290 def AdbConnected(self): 291 """Check AVD adb connected. 292 293 Returns: 294 Boolean, True when adb status of AVD is connected. 295 """ 296 if self._adb_port and self._device_information: 297 return True 298 return False 299 300 @property 301 def name(self): 302 """Return the instance name.""" 303 return self._name 304 305 @property 306 def fullname(self): 307 """Return the instance full name.""" 308 return self._fullname 309 310 @property 311 def ip(self): 312 """Return the ip.""" 313 return self._ip 314 315 @property 316 def status(self): 317 """Return status.""" 318 return self._status 319 320 @property 321 def display(self): 322 """Return display.""" 323 return self._display 324 325 @property 326 def ssh_tunnel_is_connected(self): 327 """Return the connect status.""" 328 return self._ssh_tunnel_is_connected 329 330 @property 331 def createtime(self): 332 """Return create time.""" 333 return self._createtime 334 335 @property 336 def avd_type(self): 337 """Return avd_type.""" 338 return self._avd_type 339 340 @property 341 def avd_flavor(self): 342 """Return avd_flavor.""" 343 return self._avd_flavor 344 345 @property 346 def islocal(self): 347 """Return if it is a local instance.""" 348 return self._is_local 349 350 @property 351 def adb_port(self): 352 """Return adb_port.""" 353 return self._adb_port 354 355 @property 356 def vnc_port(self): 357 """Return vnc_port.""" 358 return self._vnc_port 359 360 @property 361 def webrtc_port(self): 362 """Return webrtc_port.""" 363 return self._webrtc_port 364 365 @property 366 def zone(self): 367 """Return zone.""" 368 return self._zone 369 370 371class LocalInstance(Instance): 372 """Class to store data of local cuttlefish instance.""" 373 def __init__(self, cf_config_path): 374 """Initialize a localInstance object. 375 376 Args: 377 cf_config_path: String, path to the cf runtime config. 378 """ 379 self._cf_runtime_cfg = cvd_runtime_config.CvdRuntimeConfig(cf_config_path) 380 self._instance_dir = self._cf_runtime_cfg.instance_dir 381 self._virtual_disk_paths = self._cf_runtime_cfg.virtual_disk_paths 382 self._local_instance_id = int(self._cf_runtime_cfg.instance_id) 383 384 display = _DISPLAY_STRING % {"x_res": self._cf_runtime_cfg.x_res, 385 "y_res": self._cf_runtime_cfg.y_res, 386 "dpi": self._cf_runtime_cfg.dpi} 387 # TODO(143063678), there's no createtime info in 388 # cuttlefish_config.json so far. 389 name = GetLocalInstanceName(self._local_instance_id) 390 fullname = (_FULL_NAME_STRING % 391 {"device_serial": "0.0.0.0:%s" % self._cf_runtime_cfg.adb_port, 392 "instance_name": name, 393 "elapsed_time": None}) 394 adb_device = AdbTools(self._cf_runtime_cfg.adb_port) 395 webrtc_port = local_image_local_instance.LocalImageLocalInstance.GetWebrtcSigServerPort( 396 self._local_instance_id) 397 device_information = None 398 if adb_device.IsAdbConnected(): 399 device_information = adb_device.device_information 400 401 super().__init__( 402 name=name, fullname=fullname, display=display, ip="0.0.0.0", 403 status=constants.INS_STATUS_RUNNING, 404 adb_port=self._cf_runtime_cfg.adb_port, 405 vnc_port=self._cf_runtime_cfg.vnc_port, 406 createtime=None, elapsed_time=None, avd_type=constants.TYPE_CF, 407 is_local=True, device_information=device_information, 408 zone=_LOCAL_ZONE, webrtc_port=webrtc_port) 409 410 def Summary(self): 411 """Return the string that this class is holding.""" 412 instance_home = "%s instance home: %s" % (_INDENT, self._instance_dir) 413 return "%s\n%s" % (super().Summary(), instance_home) 414 415 def CvdStatus(self): 416 """check if local instance is active. 417 418 Execute cvd_status cmd to check if it exit without error. 419 420 Returns 421 True if instance is active. 422 """ 423 if not self._cf_runtime_cfg.cvd_tools_path: 424 logger.debug("No cvd tools path found from config:%s", 425 self._cf_runtime_cfg.config_path) 426 return False 427 cvd_env = os.environ.copy() 428 cvd_env[constants.ENV_CUTTLEFISH_CONFIG_FILE] = self._cf_runtime_cfg.config_path 429 cvd_env[constants.ENV_CVD_HOME] = GetLocalInstanceHomeDir(self._local_instance_id) 430 cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(self._local_instance_id) 431 try: 432 cvd_status_cmd = os.path.join(self._cf_runtime_cfg.cvd_tools_path, 433 _CVD_STATUS_BIN) 434 # TODO(b/150575261): Change the cvd home and cvd artifact path to 435 # another place instead of /tmp to prevent from the file not 436 # found exception. 437 if not os.path.exists(cvd_status_cmd): 438 logger.warning("Cvd tools path doesn't exist:%s", cvd_status_cmd) 439 for env_host_out in [constants.ENV_ANDROID_SOONG_HOST_OUT, 440 constants.ENV_ANDROID_HOST_OUT]: 441 if os.environ.get(env_host_out, _NO_ANDROID_ENV) in cvd_status_cmd: 442 logger.warning( 443 "Can't find the cvd_status tool (Try lunching a " 444 "cuttlefish target like aosp_cf_x86_phone-userdebug " 445 "and running 'make hosttar' before list/delete local " 446 "instances)") 447 return False 448 logger.debug("Running cmd[%s] to check cvd status.", cvd_status_cmd) 449 process = subprocess.Popen(cvd_status_cmd, 450 stdin=None, 451 stdout=subprocess.PIPE, 452 stderr=subprocess.STDOUT, 453 env=cvd_env) 454 stdout, _ = process.communicate() 455 if process.returncode != 0: 456 if stdout: 457 logger.debug("Local instance[%s] is not active: %s", 458 self.name, stdout.strip()) 459 return False 460 return True 461 except subprocess.CalledProcessError as cpe: 462 logger.error("Failed to run cvd_status: %s", cpe.output) 463 return False 464 465 def Delete(self): 466 """Execute stop_cvd to stop local cuttlefish instance. 467 468 - We should get the same host tool used to launch cvd to delete instance 469 , So get stop_cvd bin from the cvd runtime config. 470 - Add CUTTLEFISH_CONFIG_FILE env variable to tell stop_cvd which cvd 471 need to be deleted. 472 - Stop adb since local instance use the fixed adb port and could be 473 reused again soon. 474 """ 475 stop_cvd_cmd = os.path.join(self.cf_runtime_cfg.cvd_tools_path, 476 constants.CMD_STOP_CVD) 477 logger.debug("Running cmd[%s] to delete local cvd", stop_cvd_cmd) 478 cvd_env = os.environ.copy() 479 if self.instance_dir: 480 cvd_env[constants.ENV_CUTTLEFISH_CONFIG_FILE] = self._cf_runtime_cfg.config_path 481 cvd_env[constants.ENV_CVD_HOME] = GetLocalInstanceHomeDir( 482 self._local_instance_id) 483 cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(self._local_instance_id) 484 else: 485 logger.error("instance_dir is null!! instance[%d] might not be" 486 " deleted", self._local_instance_id) 487 subprocess.check_call( 488 utils.AddUserGroupsToCmd(stop_cvd_cmd, 489 constants.LIST_CF_USER_GROUPS), 490 stderr=subprocess.STDOUT, shell=True, env=cvd_env) 491 492 adb_cmd = AdbTools(self.adb_port) 493 # When relaunch a local instance, we need to pass in retry=True to make 494 # sure adb device is completely gone since it will use the same adb port 495 adb_cmd.DisconnectAdb(retry=True) 496 497 def GetLock(self): 498 """Return the LocalInstanceLock for this object.""" 499 return GetLocalInstanceLock(self._local_instance_id) 500 501 @property 502 def instance_dir(self): 503 """Return _instance_dir.""" 504 return self._instance_dir 505 506 @property 507 def instance_id(self): 508 """Return _local_instance_id.""" 509 return self._local_instance_id 510 511 @property 512 def virtual_disk_paths(self): 513 """Return virtual_disk_paths""" 514 return self._virtual_disk_paths 515 516 @property 517 def cf_runtime_cfg(self): 518 """Return _cf_runtime_cfg""" 519 return self._cf_runtime_cfg 520 521 522class LocalGoldfishInstance(Instance): 523 """Class to store data of local goldfish instance. 524 525 A goldfish instance binds to a console port and an adb port. The console 526 port is for `adb emu` to send emulator-specific commands. The adb port is 527 for `adb connect` to start a TCP connection. By convention, the console 528 port is an even number, and the adb port is the console port + 1. The first 529 instance uses port 5554 and 5555, the second instance uses 5556 and 5557, 530 and so on. 531 """ 532 533 _INSTANCE_NAME_PATTERN = re.compile( 534 r"^local-goldfish-instance-(?P<id>\d+)$") 535 _INSTANCE_NAME_FORMAT = "local-goldfish-instance-%(id)s" 536 _EMULATOR_DEFAULT_CONSOLE_PORT = 5554 537 _DEFAULT_ADB_LOCAL_TRANSPORT_MAX_PORT = 5585 538 _DEVICE_SERIAL_FORMAT = "emulator-%(console_port)s" 539 _DEVICE_SERIAL_PATTERN = re.compile(r"^emulator-(?P<console_port>\d+)$") 540 541 def __init__(self, local_instance_id, avd_flavor=None, create_time=None, 542 x_res=None, y_res=None, dpi=None): 543 """Initialize a LocalGoldfishInstance object. 544 545 Args: 546 local_instance_id: Integer of instance id. 547 avd_flavor: String, the flavor of the virtual device. 548 create_time: String, the creation date and time. 549 x_res: Integer of x dimension. 550 y_res: Integer of y dimension. 551 dpi: Integer of dpi. 552 """ 553 self._id = local_instance_id 554 adb_port = self.console_port + 1 555 self._adb = AdbTools(adb_port=adb_port, 556 device_serial=self.device_serial) 557 558 name = self._INSTANCE_NAME_FORMAT % {"id": local_instance_id} 559 560 elapsed_time = _GetElapsedTime(create_time) if create_time else None 561 562 fullname = _FULL_NAME_STRING % {"device_serial": self.device_serial, 563 "instance_name": name, 564 "elapsed_time": elapsed_time} 565 566 if x_res and y_res and dpi: 567 display = _DISPLAY_STRING % {"x_res": x_res, "y_res": y_res, 568 "dpi": dpi} 569 else: 570 display = "unknown" 571 572 device_information = (self._adb.device_information if 573 self._adb.device_information else None) 574 575 super().__init__( 576 name=name, fullname=fullname, display=display, ip="127.0.0.1", 577 status=None, adb_port=adb_port, avd_type=constants.TYPE_GF, 578 createtime=create_time, elapsed_time=elapsed_time, 579 avd_flavor=avd_flavor, is_local=True, 580 device_information=device_information) 581 582 @staticmethod 583 def _GetInstanceDirRoot(): 584 """Return the root directory of all instance directories.""" 585 return os.path.join(tempfile.gettempdir(), "acloud_gf_temp") 586 587 @property 588 def adb(self): 589 """Return the AdbTools to send emulator commands to this instance.""" 590 return self._adb 591 592 @property 593 def console_port(self): 594 """Return the console port as an integer.""" 595 # Emulator requires the console port to be an even number. 596 return self._EMULATOR_DEFAULT_CONSOLE_PORT + (self._id - 1) * 2 597 598 @property 599 def device_serial(self): 600 """Return the serial number that contains the console port.""" 601 return self._DEVICE_SERIAL_FORMAT % {"console_port": self.console_port} 602 603 @property 604 def instance_dir(self): 605 """Return the path to instance directory.""" 606 return os.path.join(self._GetInstanceDirRoot(), 607 self._INSTANCE_NAME_FORMAT % {"id": self._id}) 608 609 @classmethod 610 def GetIdByName(cls, name): 611 """Get id by name. 612 613 Args: 614 name: String of instance name. 615 616 Return: 617 The instance id as an integer if the name is in valid format. 618 None if the name does not represent a local goldfish instance. 619 """ 620 match = cls._INSTANCE_NAME_PATTERN.match(name) 621 if match: 622 return int(match.group("id")) 623 return None 624 625 @classmethod 626 def GetLockById(cls, instance_id): 627 """Get LocalInstanceLock by id.""" 628 lock_path = os.path.join( 629 cls._GetInstanceDirRoot(), 630 (cls._INSTANCE_NAME_FORMAT % {"id": instance_id}) + ".lock") 631 return LocalInstanceLock(lock_path) 632 633 def GetLock(self): 634 """Return the LocalInstanceLock for this object.""" 635 return self.GetLockById(self._id) 636 637 @classmethod 638 def GetExistingInstances(cls): 639 """Get the list of instances that adb can send emu commands to.""" 640 instances = [] 641 for serial in AdbTools.GetDeviceSerials(): 642 match = cls._DEVICE_SERIAL_PATTERN.match(serial) 643 if not match: 644 continue 645 port = int(match.group("console_port")) 646 instance_id = (port - cls._EMULATOR_DEFAULT_CONSOLE_PORT) // 2 + 1 647 instances.append(LocalGoldfishInstance(instance_id)) 648 return instances 649 650 @classmethod 651 def GetMaxNumberOfInstances(cls): 652 """Get number of emulators that adb can detect.""" 653 max_port = os.environ.get("ADB_LOCAL_TRANSPORT_MAX_PORT", 654 cls._DEFAULT_ADB_LOCAL_TRANSPORT_MAX_PORT) 655 try: 656 max_port = int(max_port) 657 except ValueError: 658 max_port = cls._DEFAULT_ADB_LOCAL_TRANSPORT_MAX_PORT 659 if (max_port < cls._EMULATOR_DEFAULT_CONSOLE_PORT or 660 max_port > constants.MAX_PORT): 661 max_port = cls._DEFAULT_ADB_LOCAL_TRANSPORT_MAX_PORT 662 return (max_port + 1 - cls._EMULATOR_DEFAULT_CONSOLE_PORT) // 2 663 664 665class RemoteInstance(Instance): 666 """Class to store data of remote instance.""" 667 668 # pylint: disable=too-many-locals 669 def __init__(self, gce_instance): 670 """Process the args into class vars. 671 672 RemoteInstace initialized by gce dict object. We parse the required data 673 from gce_instance to local variables. 674 Reference: 675 https://cloud.google.com/compute/docs/reference/rest/v1/instances/get 676 677 We also gather more details on client side including the forwarding adb 678 port and vnc port which will be used to determine the status of ssh 679 tunnel connection. 680 681 The status of gce instance will be displayed in _fullname property: 682 - Connected: If gce instance and ssh tunnel and adb connection are all 683 active. 684 - No connected: If ssh tunnel or adb connection is not found. 685 - Terminated: If we can't retrieve the public ip from gce instance. 686 687 Args: 688 gce_instance: dict object queried from gce. 689 """ 690 name = gce_instance.get(constants.INS_KEY_NAME) 691 692 create_time = gce_instance.get(constants.INS_KEY_CREATETIME) 693 elapsed_time = _GetElapsedTime(create_time) 694 status = gce_instance.get(constants.INS_KEY_STATUS) 695 zone = self._GetZoneName(gce_instance.get(constants.INS_KEY_ZONE)) 696 697 ip = None 698 for network_interface in gce_instance.get("networkInterfaces"): 699 for access_config in network_interface.get("accessConfigs"): 700 ip = access_config.get("natIP") 701 702 # Get metadata 703 display = None 704 avd_type = None 705 avd_flavor = None 706 for metadata in gce_instance.get("metadata", {}).get("items", []): 707 key = metadata["key"] 708 value = metadata["value"] 709 if key == constants.INS_KEY_DISPLAY: 710 display = value 711 elif key == constants.INS_KEY_AVD_TYPE: 712 avd_type = value 713 elif key == constants.INS_KEY_AVD_FLAVOR: 714 avd_flavor = value 715 # TODO(176884236): Insert avd information into metadata of instance. 716 if not avd_type and name.startswith(_ACLOUDWEB_INSTANCE_START_STRING): 717 avd_type = constants.TYPE_CF 718 719 # Find ssl tunnel info. 720 adb_port = None 721 vnc_port = None 722 device_information = None 723 if ip: 724 forwarded_ports = self.GetAdbVncPortFromSSHTunnel(ip, avd_type) 725 adb_port = forwarded_ports.adb_port 726 vnc_port = forwarded_ports.vnc_port 727 ssh_tunnel_is_connected = adb_port is not None 728 729 adb_device = AdbTools(adb_port) 730 if adb_device.IsAdbConnected(): 731 device_information = adb_device.device_information 732 fullname = (_FULL_NAME_STRING % 733 {"device_serial": "127.0.0.1:%d" % adb_port, 734 "instance_name": name, 735 "elapsed_time": elapsed_time}) 736 else: 737 fullname = (_FULL_NAME_STRING % 738 {"device_serial": "not connected", 739 "instance_name": name, 740 "elapsed_time": elapsed_time}) 741 # If instance is terminated, its ip is None. 742 else: 743 ssh_tunnel_is_connected = False 744 fullname = (_FULL_NAME_STRING % 745 {"device_serial": "terminated", 746 "instance_name": name, 747 "elapsed_time": elapsed_time}) 748 749 super().__init__( 750 name=name, fullname=fullname, display=display, ip=ip, status=status, 751 adb_port=adb_port, vnc_port=vnc_port, 752 ssh_tunnel_is_connected=ssh_tunnel_is_connected, 753 createtime=create_time, elapsed_time=elapsed_time, avd_type=avd_type, 754 avd_flavor=avd_flavor, is_local=False, 755 device_information=device_information, 756 zone=zone, 757 webrtc_port=constants.WEBRTC_LOCAL_PORT) 758 759 @staticmethod 760 def _GetZoneName(zone_info): 761 """Get the zone name from the zone information of gce instance. 762 763 Zone information is like: 764 "https://www.googleapis.com/compute/v1/projects/project/zones/us-central1-c" 765 We want to get "us-central1-c" as zone name. 766 767 Args: 768 zone_info: String, zone information of gce instance. 769 770 Returns: 771 Zone name of gce instance. None if zone name can't find. 772 """ 773 zone_match = _RE_ZONE.match(zone_info) 774 if zone_match: 775 return zone_match.group("zone") 776 777 logger.debug("Can't get zone name from %s.", zone_info) 778 return None 779 780 @staticmethod 781 def GetAdbVncPortFromSSHTunnel(ip, avd_type): 782 """Get forwarding adb and vnc port from ssh tunnel. 783 784 Args: 785 ip: String, ip address. 786 avd_type: String, the AVD type. 787 788 Returns: 789 NamedTuple ForwardedPorts(vnc_port, adb_port) holding the ports 790 used in the ssh forwarded call. Both fields are integers. 791 """ 792 if avd_type not in utils.AVD_PORT_DICT: 793 return utils.ForwardedPorts(vnc_port=None, adb_port=None) 794 795 default_vnc_port = utils.AVD_PORT_DICT[avd_type].vnc_port 796 default_adb_port = utils.AVD_PORT_DICT[avd_type].adb_port 797 # TODO(165888525): Align the SSH tunnel for the order of adb port and 798 # vnc port. 799 re_pattern = re.compile(_RE_SSH_TUNNEL_PATTERN % 800 (_RE_GROUP_ADB, default_adb_port, 801 _RE_GROUP_VNC, default_vnc_port, ip)) 802 adb_port = None 803 vnc_port = None 804 process_output = utils.CheckOutput(constants.COMMAND_PS) 805 for line in process_output.splitlines(): 806 match = re_pattern.match(line) 807 if match: 808 adb_port = int(match.group(_RE_GROUP_ADB)) 809 vnc_port = int(match.group(_RE_GROUP_VNC)) 810 break 811 812 logger.debug(("grathering detail for ssh tunnel. " 813 "IP:%s, forwarding (adb:%d, vnc:%d)"), ip, adb_port, 814 vnc_port) 815 816 return utils.ForwardedPorts(vnc_port=vnc_port, adb_port=adb_port) 817