• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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