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.internal import constants 42from acloud.internal.lib import cvd_runtime_config 43from acloud.internal.lib import utils 44from acloud.internal.lib.adb_tools import AdbTools 45 46 47logger = logging.getLogger(__name__) 48 49_ACLOUD_CVD_TEMP = os.path.join(tempfile.gettempdir(), "acloud_cvd_temp") 50_CVD_RUNTIME_FOLDER_NAME = "cuttlefish_runtime" 51_CVD_STATUS_BIN = "cvd_status" 52_MSG_UNABLE_TO_CALCULATE = "Unable to calculate" 53_RE_GROUP_ADB = "local_adb_port" 54_RE_GROUP_VNC = "local_vnc_port" 55_RE_SSH_TUNNEL_PATTERN = (r"((.*\s*-L\s)(?P<%s>\d+):127.0.0.1:%s)" 56 r"((.*\s*-L\s)(?P<%s>\d+):127.0.0.1:%s)" 57 r"(.+%s)") 58_RE_TIMEZONE = re.compile(r"^(?P<time>[0-9\-\.:T]*)(?P<timezone>[+-]\d+:\d+)$") 59 60_COMMAND_PS_LAUNCH_CVD = ["ps", "-wweo", "lstart,cmd"] 61_RE_RUN_CVD = re.compile(r"(?P<date_str>^[^/]+)(.*run_cvd)") 62_DISPLAY_STRING = "%(x_res)sx%(y_res)s (%(dpi)s)" 63_RE_ZONE = re.compile(r".+/zones/(?P<zone>.+)$") 64_LOCAL_ZONE = "local" 65_FULL_NAME_STRING = ("device serial: %(device_serial)s (%(instance_name)s) " 66 "elapsed time: %(elapsed_time)s") 67_INDENT = " " * 3 68LocalPorts = collections.namedtuple("LocalPorts", [constants.VNC_PORT, 69 constants.ADB_PORT]) 70 71 72def GetDefaultCuttlefishConfig(): 73 """Get the path of default cuttlefish instance config. 74 75 Return: 76 String, path of cf runtime config. 77 """ 78 return os.path.join(os.path.expanduser("~"), _CVD_RUNTIME_FOLDER_NAME, 79 constants.CUTTLEFISH_CONFIG_FILE) 80 81 82def GetLocalInstanceName(local_instance_id): 83 """Get local cuttlefish instance name by instance id. 84 85 Args: 86 local_instance_id: Integer of instance id. 87 88 Return: 89 String, the instance name. 90 """ 91 return "%s-%d" % (constants.LOCAL_INS_NAME, local_instance_id) 92 93 94def GetLocalInstanceConfig(local_instance_id): 95 """Get the path of instance config. 96 97 Args: 98 local_instance_id: Integer of instance id. 99 100 Return: 101 String, path of cf runtime config. 102 """ 103 cfg_path = os.path.join(GetLocalInstanceRuntimeDir(local_instance_id), 104 constants.CUTTLEFISH_CONFIG_FILE) 105 if os.path.isfile(cfg_path): 106 return cfg_path 107 return None 108 109 110def GetAllLocalInstanceConfigs(): 111 """Get the list of instance config. 112 113 Return: 114 List of instance config path. 115 """ 116 cfg_list = [] 117 # Check if any instance config is under home folder. 118 cfg_path = GetDefaultCuttlefishConfig() 119 if os.path.isfile(cfg_path): 120 cfg_list.append(cfg_path) 121 122 # Check if any instance config is under acloud cvd temp folder. 123 if os.path.exists(_ACLOUD_CVD_TEMP): 124 for ins_name in os.listdir(_ACLOUD_CVD_TEMP): 125 cfg_path = os.path.join(_ACLOUD_CVD_TEMP, 126 ins_name, 127 _CVD_RUNTIME_FOLDER_NAME, 128 constants.CUTTLEFISH_CONFIG_FILE) 129 if os.path.isfile(cfg_path): 130 cfg_list.append(cfg_path) 131 return cfg_list 132 133 134def GetLocalInstanceHomeDir(local_instance_id): 135 """Get local instance home dir according to instance id. 136 137 Args: 138 local_instance_id: Integer of instance id. 139 140 Return: 141 String, path of instance home dir. 142 """ 143 return os.path.join(_ACLOUD_CVD_TEMP, 144 GetLocalInstanceName(local_instance_id)) 145 146 147def GetLocalInstanceRuntimeDir(local_instance_id): 148 """Get instance runtime dir 149 150 Args: 151 local_instance_id: Integer of instance id. 152 153 Return: 154 String, path of instance runtime dir. 155 """ 156 return os.path.join(GetLocalInstanceHomeDir(local_instance_id), 157 _CVD_RUNTIME_FOLDER_NAME) 158 159 160def _GetCurrentLocalTime(): 161 """Return a datetime object for current time in local time zone.""" 162 return datetime.datetime.now(dateutil.tz.tzlocal()) 163 164 165def _GetElapsedTime(start_time): 166 """Calculate the elapsed time from start_time till now. 167 168 Args: 169 start_time: String of instance created time. 170 171 Returns: 172 datetime.timedelta of elapsed time, _MSG_UNABLE_TO_CALCULATE for 173 datetime can't parse cases. 174 """ 175 match = _RE_TIMEZONE.match(start_time) 176 try: 177 # Check start_time has timezone or not. If timezone can't be found, 178 # use local timezone to get elapsed time. 179 if match: 180 return _GetCurrentLocalTime() - dateutil.parser.parse(start_time) 181 182 return _GetCurrentLocalTime() - dateutil.parser.parse( 183 start_time).replace(tzinfo=dateutil.tz.tzlocal()) 184 except ValueError: 185 logger.debug(("Can't parse datetime string(%s)."), start_time) 186 return _MSG_UNABLE_TO_CALCULATE 187 188 189class Instance(object): 190 """Class to store data of instance.""" 191 192 # pylint: disable=too-many-locals 193 def __init__(self, name, fullname, display, ip, status=None, adb_port=None, 194 vnc_port=None, ssh_tunnel_is_connected=None, createtime=None, 195 elapsed_time=None, avd_type=None, avd_flavor=None, 196 is_local=False, device_information=None, zone=None): 197 self._name = name 198 self._fullname = fullname 199 self._status = status 200 self._display = display # Resolution and dpi 201 self._ip = ip 202 self._adb_port = adb_port # adb port which is forwarding to remote 203 self._vnc_port = vnc_port # vnc port which is forwarding to remote 204 # True if ssh tunnel is still connected 205 self._ssh_tunnel_is_connected = ssh_tunnel_is_connected 206 self._createtime = createtime 207 self._elapsed_time = elapsed_time 208 self._avd_type = avd_type 209 self._avd_flavor = avd_flavor 210 self._is_local = is_local # True if this is a local instance 211 self._device_information = device_information 212 self._zone = zone 213 214 def __repr__(self): 215 """Return full name property for print.""" 216 return self._fullname 217 218 def Summary(self): 219 """Let's make it easy to see what this class is holding.""" 220 representation = [] 221 representation.append(" name: %s" % self._name) 222 representation.append("%s IP: %s" % (_INDENT, self._ip)) 223 representation.append("%s create time: %s" % (_INDENT, self._createtime)) 224 representation.append("%s elapse time: %s" % (_INDENT, self._elapsed_time)) 225 representation.append("%s status: %s" % (_INDENT, self._status)) 226 representation.append("%s avd type: %s" % (_INDENT, self._avd_type)) 227 representation.append("%s display: %s" % (_INDENT, self._display)) 228 representation.append("%s vnc: 127.0.0.1:%s" % (_INDENT, self._vnc_port)) 229 representation.append("%s zone: %s" % (_INDENT, self._zone)) 230 231 if self._adb_port and self._device_information: 232 representation.append("%s adb serial: 127.0.0.1:%s" % 233 (_INDENT, self._adb_port)) 234 representation.append("%s product: %s" % ( 235 _INDENT, self._device_information["product"])) 236 representation.append("%s model: %s" % ( 237 _INDENT, self._device_information["model"])) 238 representation.append("%s device: %s" % ( 239 _INDENT, self._device_information["device"])) 240 representation.append("%s transport_id: %s" % ( 241 _INDENT, self._device_information["transport_id"])) 242 else: 243 representation.append("%s adb serial: disconnected" % _INDENT) 244 245 return "\n".join(representation) 246 247 @property 248 def name(self): 249 """Return the instance name.""" 250 return self._name 251 252 @property 253 def fullname(self): 254 """Return the instance full name.""" 255 return self._fullname 256 257 @property 258 def ip(self): 259 """Return the ip.""" 260 return self._ip 261 262 @property 263 def status(self): 264 """Return status.""" 265 return self._status 266 267 @property 268 def display(self): 269 """Return display.""" 270 return self._display 271 272 @property 273 def ssh_tunnel_is_connected(self): 274 """Return the connect status.""" 275 return self._ssh_tunnel_is_connected 276 277 @property 278 def createtime(self): 279 """Return create time.""" 280 return self._createtime 281 282 @property 283 def avd_type(self): 284 """Return avd_type.""" 285 return self._avd_type 286 287 @property 288 def avd_flavor(self): 289 """Return avd_flavor.""" 290 return self._avd_flavor 291 292 @property 293 def islocal(self): 294 """Return if it is a local instance.""" 295 return self._is_local 296 297 @property 298 def adb_port(self): 299 """Return adb_port.""" 300 return self._adb_port 301 302 @property 303 def vnc_port(self): 304 """Return vnc_port.""" 305 return self._vnc_port 306 307 @property 308 def zone(self): 309 """Return zone.""" 310 return self._zone 311 312 313class LocalInstance(Instance): 314 """Class to store data of local cuttlefish instance.""" 315 def __init__(self, cf_config_path): 316 """Initialize a localInstance object. 317 318 Args: 319 cf_config_path: String, path to the cf runtime config. 320 """ 321 self._cf_runtime_cfg = cvd_runtime_config.CvdRuntimeConfig(cf_config_path) 322 self._instance_dir = self._cf_runtime_cfg.instance_dir 323 self._virtual_disk_paths = self._cf_runtime_cfg.virtual_disk_paths 324 self._local_instance_id = int(self._cf_runtime_cfg.instance_id) 325 326 display = _DISPLAY_STRING % {"x_res": self._cf_runtime_cfg.x_res, 327 "y_res": self._cf_runtime_cfg.y_res, 328 "dpi": self._cf_runtime_cfg.dpi} 329 # TODO(143063678), there's no createtime info in 330 # cuttlefish_config.json so far. 331 name = GetLocalInstanceName(self._local_instance_id) 332 fullname = (_FULL_NAME_STRING % 333 {"device_serial": "127.0.0.1:%s" % self._cf_runtime_cfg.adb_port, 334 "instance_name": name, 335 "elapsed_time": None}) 336 adb_device = AdbTools(self._cf_runtime_cfg.adb_port) 337 device_information = None 338 if adb_device.IsAdbConnected(): 339 device_information = adb_device.device_information 340 341 super(LocalInstance, self).__init__( 342 name=name, fullname=fullname, display=display, ip="127.0.0.1", 343 status=constants.INS_STATUS_RUNNING, 344 adb_port=self._cf_runtime_cfg.adb_port, 345 vnc_port=self._cf_runtime_cfg.vnc_port, 346 createtime=None, elapsed_time=None, avd_type=constants.TYPE_CF, 347 is_local=True, device_information=device_information, 348 zone=_LOCAL_ZONE) 349 350 def Summary(self): 351 """Return the string that this class is holding.""" 352 instance_home = "%s instance home: %s" % (_INDENT, self._instance_dir) 353 return "%s\n%s" % (super(LocalInstance, self).Summary(), instance_home) 354 355 def CvdStatus(self): 356 """check if local instance is active. 357 358 Execute cvd_status cmd to check if it exit without error. 359 360 Returns 361 True if instance is active. 362 """ 363 cvd_env = os.environ.copy() 364 cvd_env[constants.ENV_CUTTLEFISH_CONFIG_FILE] = self._cf_runtime_cfg.config_path 365 cvd_env[constants.ENV_CVD_HOME] = GetLocalInstanceHomeDir(self._local_instance_id) 366 cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(self._local_instance_id) 367 try: 368 cvd_status_cmd = os.path.join(self._cf_runtime_cfg.cvd_tools_path, 369 _CVD_STATUS_BIN) 370 logger.debug("Running cmd[%s] to check cvd status.", cvd_status_cmd) 371 process = subprocess.Popen(cvd_status_cmd, 372 stdin=None, 373 stdout=subprocess.PIPE, 374 stderr=subprocess.STDOUT, 375 env=cvd_env) 376 stdout, _ = process.communicate() 377 if process.returncode != 0: 378 if stdout: 379 logger.debug("Local instance[%s] is not active: %s", 380 self.name, stdout.strip()) 381 return False 382 return True 383 except subprocess.CalledProcessError as cpe: 384 logger.error("Failed to run cvd_status: %s", cpe.output) 385 return False 386 387 def Delete(self): 388 """Execute stop_cvd to stop local cuttlefish instance. 389 390 - We should get the same host tool used to launch cvd to delete instance 391 , So get stop_cvd bin from the cvd runtime config. 392 - Add CUTTLEFISH_CONFIG_FILE env variable to tell stop_cvd which cvd 393 need to be deleted. 394 - Stop adb since local instance use the fixed adb port and could be 395 reused again soon. 396 """ 397 stop_cvd_cmd = os.path.join(self.cf_runtime_cfg.cvd_tools_path, 398 constants.CMD_STOP_CVD) 399 logger.debug("Running cmd[%s] to delete local cvd", stop_cvd_cmd) 400 with open(os.devnull, "w") as dev_null: 401 cvd_env = os.environ.copy() 402 if self.instance_dir: 403 cvd_env[constants.ENV_CUTTLEFISH_CONFIG_FILE] = self._cf_runtime_cfg.config_path 404 cvd_env[constants.ENV_CVD_HOME] = GetLocalInstanceHomeDir( 405 self._local_instance_id) 406 cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(self._local_instance_id) 407 else: 408 logger.error("instance_dir is null!! instance[%d] might not be" 409 " deleted", self._local_instance_id) 410 subprocess.check_call( 411 utils.AddUserGroupsToCmd(stop_cvd_cmd, 412 constants.LIST_CF_USER_GROUPS), 413 stderr=dev_null, stdout=dev_null, shell=True, env=cvd_env) 414 415 adb_cmd = AdbTools(self.adb_port) 416 # When relaunch a local instance, we need to pass in retry=True to make 417 # sure adb device is completely gone since it will use the same adb port 418 adb_cmd.DisconnectAdb(retry=True) 419 420 @property 421 def instance_dir(self): 422 """Return _instance_dir.""" 423 return self._instance_dir 424 425 @property 426 def instance_id(self): 427 """Return _local_instance_id.""" 428 return self._local_instance_id 429 430 @property 431 def virtual_disk_paths(self): 432 """Return virtual_disk_paths""" 433 return self._virtual_disk_paths 434 435 @property 436 def cf_runtime_cfg(self): 437 """Return _cf_runtime_cfg""" 438 return self._cf_runtime_cfg 439 440 441class LocalGoldfishInstance(Instance): 442 """Class to store data of local goldfish instance.""" 443 444 _INSTANCE_NAME_PATTERN = re.compile( 445 r"^local-goldfish-instance-(?P<id>\d+)$") 446 _CREATION_TIMESTAMP_FILE_NAME = "creation_timestamp.txt" 447 _INSTANCE_NAME_FORMAT = "local-goldfish-instance-%(id)s" 448 _EMULATOR_DEFAULT_CONSOLE_PORT = 5554 449 _GF_ADB_DEVICE_SERIAL = "emulator-%(console_port)s" 450 451 def __init__(self, local_instance_id, avd_flavor=None, create_time=None, 452 x_res=None, y_res=None, dpi=None): 453 """Initialize a LocalGoldfishInstance object. 454 455 Args: 456 local_instance_id: Integer of instance id. 457 avd_flavor: String, the flavor of the virtual device. 458 create_time: String, the creation date and time. 459 x_res: Integer of x dimension. 460 y_res: Integer of y dimension. 461 dpi: Integer of dpi. 462 """ 463 self._id = local_instance_id 464 # By convention, adb port is console port + 1. 465 adb_port = self.console_port + 1 466 467 name = self._INSTANCE_NAME_FORMAT % {"id": local_instance_id} 468 469 elapsed_time = _GetElapsedTime(create_time) if create_time else None 470 471 fullname = _FULL_NAME_STRING % {"device_serial": self.device_serial, 472 "instance_name": name, 473 "elapsed_time": elapsed_time} 474 475 if x_res and y_res and dpi: 476 display = _DISPLAY_STRING % {"x_res": x_res, "y_res": y_res, 477 "dpi": dpi} 478 else: 479 display = "unknown" 480 481 adb = AdbTools(adb_port) 482 device_information = (adb.device_information if 483 adb.device_information else None) 484 485 super(LocalGoldfishInstance, self).__init__( 486 name=name, fullname=fullname, display=display, ip="127.0.0.1", 487 status=None, adb_port=adb_port, avd_type=constants.TYPE_GF, 488 createtime=create_time, elapsed_time=elapsed_time, 489 avd_flavor=avd_flavor, is_local=True, 490 device_information=device_information) 491 492 @staticmethod 493 def _GetInstanceDirRoot(): 494 """Return the root directory of all instance directories.""" 495 return os.path.join(tempfile.gettempdir(), "acloud_gf_temp") 496 497 @property 498 def console_port(self): 499 """Return the console port as an integer""" 500 # Emulator requires the console port to be an even number. 501 return self._EMULATOR_DEFAULT_CONSOLE_PORT + (self._id - 1) * 2 502 503 @property 504 def device_serial(self): 505 """Return the serial number that contains the console port.""" 506 return self._GF_ADB_DEVICE_SERIAL % {"console_port": self.console_port} 507 508 @property 509 def instance_dir(self): 510 """Return the path to instance directory.""" 511 return os.path.join(self._GetInstanceDirRoot(), 512 self._INSTANCE_NAME_FORMAT % {"id": self._id}) 513 514 @property 515 def creation_timestamp_path(self): 516 """Return the file path containing the creation timestamp.""" 517 return os.path.join(self.instance_dir, 518 self._CREATION_TIMESTAMP_FILE_NAME) 519 520 def WriteCreationTimestamp(self): 521 """Write creation timestamp to file.""" 522 with open(self.creation_timestamp_path, "w") as timestamp_file: 523 timestamp_file.write(str(_GetCurrentLocalTime())) 524 525 def DeleteCreationTimestamp(self, ignore_errors): 526 """Delete the creation timestamp file. 527 528 Args: 529 ignore_errors: Boolean, whether to ignore the errors. 530 531 Raises: 532 OSError if fails to delete the file. 533 """ 534 try: 535 os.remove(self.creation_timestamp_path) 536 except OSError as e: 537 if not ignore_errors: 538 raise 539 logger.warning("Can't delete creation timestamp: %s", e) 540 541 @classmethod 542 def GetExistingInstances(cls): 543 """Get a list of instances that have creation timestamp files.""" 544 instance_root = cls._GetInstanceDirRoot() 545 if not os.path.isdir(instance_root): 546 return [] 547 548 instances = [] 549 for name in os.listdir(instance_root): 550 match = cls._INSTANCE_NAME_PATTERN.match(name) 551 timestamp_path = os.path.join(instance_root, name, 552 cls._CREATION_TIMESTAMP_FILE_NAME) 553 if match and os.path.isfile(timestamp_path): 554 instance_id = int(match.group("id")) 555 with open(timestamp_path, "r") as timestamp_file: 556 timestamp = timestamp_file.read().strip() 557 instances.append(LocalGoldfishInstance(instance_id, 558 create_time=timestamp)) 559 return instances 560 561 562class RemoteInstance(Instance): 563 """Class to store data of remote instance.""" 564 565 # pylint: disable=too-many-locals 566 def __init__(self, gce_instance): 567 """Process the args into class vars. 568 569 RemoteInstace initialized by gce dict object. We parse the required data 570 from gce_instance to local variables. 571 Reference: 572 https://cloud.google.com/compute/docs/reference/rest/v1/instances/get 573 574 We also gather more details on client side including the forwarding adb 575 port and vnc port which will be used to determine the status of ssh 576 tunnel connection. 577 578 The status of gce instance will be displayed in _fullname property: 579 - Connected: If gce instance and ssh tunnel and adb connection are all 580 active. 581 - No connected: If ssh tunnel or adb connection is not found. 582 - Terminated: If we can't retrieve the public ip from gce instance. 583 584 Args: 585 gce_instance: dict object queried from gce. 586 """ 587 name = gce_instance.get(constants.INS_KEY_NAME) 588 589 create_time = gce_instance.get(constants.INS_KEY_CREATETIME) 590 elapsed_time = _GetElapsedTime(create_time) 591 status = gce_instance.get(constants.INS_KEY_STATUS) 592 zone = self._GetZoneName(gce_instance.get(constants.INS_KEY_ZONE)) 593 594 ip = None 595 for network_interface in gce_instance.get("networkInterfaces"): 596 for access_config in network_interface.get("accessConfigs"): 597 ip = access_config.get("natIP") 598 599 # Get metadata 600 display = None 601 avd_type = None 602 avd_flavor = None 603 for metadata in gce_instance.get("metadata", {}).get("items", []): 604 key = metadata["key"] 605 value = metadata["value"] 606 if key == constants.INS_KEY_DISPLAY: 607 display = value 608 elif key == constants.INS_KEY_AVD_TYPE: 609 avd_type = value 610 elif key == constants.INS_KEY_AVD_FLAVOR: 611 avd_flavor = value 612 613 # Find ssl tunnel info. 614 adb_port = None 615 vnc_port = None 616 device_information = None 617 if ip: 618 forwarded_ports = self.GetAdbVncPortFromSSHTunnel(ip, avd_type) 619 adb_port = forwarded_ports.adb_port 620 vnc_port = forwarded_ports.vnc_port 621 ssh_tunnel_is_connected = adb_port is not None 622 623 adb_device = AdbTools(adb_port) 624 if adb_device.IsAdbConnected(): 625 device_information = adb_device.device_information 626 fullname = (_FULL_NAME_STRING % 627 {"device_serial": "127.0.0.1:%d" % adb_port, 628 "instance_name": name, 629 "elapsed_time": elapsed_time}) 630 else: 631 fullname = (_FULL_NAME_STRING % 632 {"device_serial": "not connected", 633 "instance_name": name, 634 "elapsed_time": elapsed_time}) 635 # If instance is terminated, its ip is None. 636 else: 637 ssh_tunnel_is_connected = False 638 fullname = (_FULL_NAME_STRING % 639 {"device_serial": "terminated", 640 "instance_name": name, 641 "elapsed_time": elapsed_time}) 642 643 super(RemoteInstance, self).__init__( 644 name=name, fullname=fullname, display=display, ip=ip, status=status, 645 adb_port=adb_port, vnc_port=vnc_port, 646 ssh_tunnel_is_connected=ssh_tunnel_is_connected, 647 createtime=create_time, elapsed_time=elapsed_time, avd_type=avd_type, 648 avd_flavor=avd_flavor, is_local=False, 649 device_information=device_information, 650 zone=zone) 651 652 @staticmethod 653 def _GetZoneName(zone_info): 654 """Get the zone name from the zone information of gce instance. 655 656 Zone information is like: 657 "https://www.googleapis.com/compute/v1/projects/project/zones/us-central1-c" 658 We want to get "us-central1-c" as zone name. 659 660 Args: 661 zone_info: String, zone information of gce instance. 662 663 Returns: 664 Zone name of gce instance. None if zone name can't find. 665 """ 666 zone_match = _RE_ZONE.match(zone_info) 667 if zone_match: 668 return zone_match.group("zone") 669 670 logger.debug("Can't get zone name from %s.", zone_info) 671 return None 672 673 @staticmethod 674 def GetAdbVncPortFromSSHTunnel(ip, avd_type): 675 """Get forwarding adb and vnc port from ssh tunnel. 676 677 Args: 678 ip: String, ip address. 679 avd_type: String, the AVD type. 680 681 Returns: 682 NamedTuple ForwardedPorts(vnc_port, adb_port) holding the ports 683 used in the ssh forwarded call. Both fields are integers. 684 """ 685 if avd_type not in utils.AVD_PORT_DICT: 686 return utils.ForwardedPorts(vnc_port=None, adb_port=None) 687 688 default_vnc_port = utils.AVD_PORT_DICT[avd_type].vnc_port 689 default_adb_port = utils.AVD_PORT_DICT[avd_type].adb_port 690 re_pattern = re.compile(_RE_SSH_TUNNEL_PATTERN % 691 (_RE_GROUP_VNC, default_vnc_port, 692 _RE_GROUP_ADB, default_adb_port, ip)) 693 adb_port = None 694 vnc_port = None 695 process_output = subprocess.check_output(constants.COMMAND_PS) 696 for line in process_output.splitlines(): 697 match = re_pattern.match(line) 698 if match: 699 adb_port = int(match.group(_RE_GROUP_ADB)) 700 vnc_port = int(match.group(_RE_GROUP_VNC)) 701 break 702 703 logger.debug(("grathering detail for ssh tunnel. " 704 "IP:%s, forwarding (adb:%d, vnc:%d)"), ip, adb_port, 705 vnc_port) 706 707 return utils.ForwardedPorts(vnc_port=vnc_port, adb_port=adb_port) 708