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