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