• 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 datetime
30import logging
31import re
32import subprocess
33
34# pylint: disable=import-error
35import dateutil.parser
36import dateutil.tz
37
38from acloud.internal import constants
39from acloud.internal.lib import utils
40from acloud.internal.lib.adb_tools import AdbTools
41
42logger = logging.getLogger(__name__)
43
44_MSG_UNABLE_TO_CALCULATE = "Unable to calculate"
45_RE_GROUP_ADB = "local_adb_port"
46_RE_GROUP_VNC = "local_vnc_port"
47_RE_SSH_TUNNEL_PATTERN = (r"((.*\s*-L\s)(?P<%s>\d+):127.0.0.1:%s)"
48                          r"((.*\s*-L\s)(?P<%s>\d+):127.0.0.1:%s)"
49                          r"(.+%s)")
50_RE_TIMEZONE = re.compile(r"^(?P<time>[0-9\-\.:T]*)(?P<timezone>[+-]\d+:\d+)$")
51
52_COMMAND_PS_LAUNCH_CVD = ["ps", "-wweo", "lstart,cmd"]
53_RE_LAUNCH_CVD = re.compile(r"(?P<date_str>^[^/]+)(.*launch_cvd --daemon )+"
54                            r"((.*\s*-cpus\s)(?P<cpu>\d+))?"
55                            r"((.*\s*-x_res\s)(?P<x_res>\d+))?"
56                            r"((.*\s*-y_res\s)(?P<y_res>\d+))?"
57                            r"((.*\s*-dpi\s)(?P<dpi>\d+))?"
58                            r"((.*\s*-memory_mb\s)(?P<memory>\d+))?"
59                            r"((.*\s*-blank_data_image_mb\s)(?P<disk>\d+))?")
60_FULL_NAME_STRING = ("device serial: %(device_serial)s (%(instance_name)s) "
61                     "elapsed time: %(elapsed_time)s")
62
63
64def _GetElapsedTime(start_time):
65    """Calculate the elapsed time from start_time till now.
66
67    Args:
68        start_time: String of instance created time.
69
70    Returns:
71        datetime.timedelta of elapsed time, _MSG_UNABLE_TO_CALCULATE for
72        datetime can't parse cases.
73    """
74    match = _RE_TIMEZONE.match(start_time)
75    try:
76        # Check start_time has timezone or not. If timezone can't be found,
77        # use local timezone to get elapsed time.
78        if match:
79            return datetime.datetime.now(
80                dateutil.tz.tzlocal()) - dateutil.parser.parse(start_time)
81
82        return datetime.datetime.now(
83            dateutil.tz.tzlocal()) - dateutil.parser.parse(
84                start_time).replace(tzinfo=dateutil.tz.tzlocal())
85    except ValueError:
86        logger.debug(("Can't parse datetime string(%s)."), start_time)
87        return _MSG_UNABLE_TO_CALCULATE
88
89
90class Instance(object):
91    """Class to store data of instance."""
92
93    def __init__(self):
94        self._name = None
95        self._fullname = None
96        self._status = None
97        self._display = None  # Resolution and dpi
98        self._ip = None
99        self._adb_port = None  # adb port which is forwarding to remote
100        self._vnc_port = None  # vnc port which is forwarding to remote
101        self._ssh_tunnel_is_connected = None  # True if ssh tunnel is still connected
102        self._createtime = None
103        self._elapsed_time = None
104        self._avd_type = None
105        self._avd_flavor = None
106        self._is_local = None  # True if this is a local instance
107
108    def __repr__(self):
109        """Return full name property for print."""
110        return self._fullname
111
112    def Summary(self):
113        """Let's make it easy to see what this class is holding."""
114        indent = " " * 3
115        representation = []
116        representation.append(" name: %s" % self._name)
117        representation.append("%s IP: %s" % (indent, self._ip))
118        representation.append("%s create time: %s" % (indent, self._createtime))
119        representation.append("%s elapse time: %s" % (indent, self._elapsed_time))
120        representation.append("%s status: %s" % (indent, self._status))
121        representation.append("%s avd type: %s" % (indent, self._avd_type))
122        representation.append("%s display: %s" % (indent, self._display))
123        representation.append("%s vnc: 127.0.0.1:%s" % (indent, self._vnc_port))
124
125        if self._adb_port:
126            representation.append("%s adb serial: 127.0.0.1:%s" %
127                                  (indent, self._adb_port))
128        else:
129            representation.append("%s adb serial: disconnected" % indent)
130
131        return "\n".join(representation)
132
133    @property
134    def name(self):
135        """Return the instance name."""
136        return self._name
137
138    @property
139    def fullname(self):
140        """Return the instance full name."""
141        return self._fullname
142
143    @property
144    def ip(self):
145        """Return the ip."""
146        return self._ip
147
148    @property
149    def status(self):
150        """Return status."""
151        return self._status
152
153    @property
154    def display(self):
155        """Return display."""
156        return self._display
157
158    @property
159    def forwarding_adb_port(self):
160        """Return the adb port."""
161        return self._adb_port
162
163    @property
164    def forwarding_vnc_port(self):
165        """Return the vnc port."""
166        return self._vnc_port
167
168    @property
169    def ssh_tunnel_is_connected(self):
170        """Return the connect status."""
171        return self._ssh_tunnel_is_connected
172
173    @property
174    def createtime(self):
175        """Return create time."""
176        return self._createtime
177
178    @property
179    def avd_type(self):
180        """Return avd_type."""
181        return self._avd_type
182
183    @property
184    def avd_flavor(self):
185        """Return avd_flavor."""
186        return self._avd_flavor
187
188    @property
189    def islocal(self):
190        """Return if it is a local instance."""
191        return self._is_local
192
193
194class LocalInstance(Instance):
195    """Class to store data of local instance."""
196
197    # pylint: disable=protected-access
198    def __new__(cls):
199        """Initialize a localInstance object.
200
201        Gather local instance information from launch_cvd process.
202
203        returns:
204            Instance object if launch_cvd process is found otherwise return None.
205        """
206        # Running instances on local is not supported on all OS.
207        if not utils.IsSupportedPlatform():
208            return None
209
210        process_output = subprocess.check_output(_COMMAND_PS_LAUNCH_CVD)
211        for line in process_output.splitlines():
212            match = _RE_LAUNCH_CVD.match(line)
213            if match:
214                local_instance = Instance()
215                x_res = match.group("x_res")
216                y_res = match.group("y_res")
217                dpi = match.group("dpi")
218                date_str = match.group("date_str").strip()
219                local_instance._name = constants.LOCAL_INS_NAME
220                local_instance._createtime = date_str
221                local_instance._elapsed_time = _GetElapsedTime(date_str)
222                local_instance._fullname = (_FULL_NAME_STRING %
223                                            {"device_serial": "127.0.0.1:%d" %
224                                                              constants.CF_ADB_PORT,
225                                             "instance_name": local_instance._name,
226                                             "elapsed_time": local_instance._elapsed_time})
227                local_instance._avd_type = constants.TYPE_CF
228                local_instance._ip = "127.0.0.1"
229                local_instance._status = constants.INS_STATUS_RUNNING
230                local_instance._adb_port = constants.CF_ADB_PORT
231                local_instance._vnc_port = constants.CF_VNC_PORT
232                local_instance._display = ("%sx%s (%s)" % (x_res, y_res, dpi))
233                local_instance._is_local = True
234                local_instance._ssh_tunnel_is_connected = True
235                return local_instance
236        return None
237
238
239class RemoteInstance(Instance):
240    """Class to store data of remote instance."""
241
242    def __init__(self, gce_instance):
243        """Process the args into class vars.
244
245        RemoteInstace initialized by gce dict object.
246        Reference:
247        https://cloud.google.com/compute/docs/reference/rest/v1/instances/get
248
249        Args:
250            gce_instance: dict object queried from gce.
251        """
252        super(RemoteInstance, self).__init__()
253        self._ProcessGceInstance(gce_instance)
254        self._is_local = False
255
256    def _ProcessGceInstance(self, gce_instance):
257        """Parse the required data from gce_instance to local variables.
258
259        We also gather more details on client side including the forwarding adb
260        port and vnc port which will be used to determine the status of ssh
261        tunnel connection.
262
263        The status of gce instance will be displayed in _fullname property:
264        - Connected: If gce instance and ssh tunnel and adb connection are all
265         active.
266        - No connected: If ssh tunnel or adb connection is not found.
267        - Terminated: If we can't retrieve the public ip from gce instance.
268
269        Args:
270           gce_instance: dict object queried from gce.
271        """
272        self._name = gce_instance.get(constants.INS_KEY_NAME)
273
274        self._createtime = gce_instance.get(constants.INS_KEY_CREATETIME)
275        self._elapsed_time = _GetElapsedTime(self._createtime)
276        self._status = gce_instance.get(constants.INS_KEY_STATUS)
277
278        ip = None
279        for network_interface in gce_instance.get("networkInterfaces"):
280            for access_config in network_interface.get("accessConfigs"):
281                ip = access_config.get("natIP")
282
283        # Get metadata
284        for metadata in gce_instance.get("metadata", {}).get("items", []):
285            key = metadata["key"]
286            value = metadata["value"]
287            if key == constants.INS_KEY_DISPLAY:
288                self._display = value
289            elif key == constants.INS_KEY_AVD_TYPE:
290                self._avd_type = value
291            elif key == constants.INS_KEY_AVD_FLAVOR:
292                self._avd_flavor = value
293
294        # Find ssl tunnel info.
295        if ip:
296            forwarded_ports = self.GetAdbVncPortFromSSHTunnel(ip,
297                                                              self._avd_type)
298            self._ip = ip
299            self._adb_port = forwarded_ports.adb_port
300            self._vnc_port = forwarded_ports.vnc_port
301            self._ssh_tunnel_is_connected = self._adb_port is not None
302
303            adb_device = AdbTools(self._adb_port)
304            if adb_device.IsAdbConnected():
305                self._fullname = (_FULL_NAME_STRING %
306                                  {"device_serial": "127.0.0.1:%d" % self._adb_port,
307                                   "instance_name": self._name,
308                                   "elapsed_time": self._elapsed_time})
309            else:
310                self._fullname = (_FULL_NAME_STRING %
311                                  {"device_serial": "not connected",
312                                   "instance_name": self._name,
313                                   "elapsed_time": self._elapsed_time})
314        # If instance is terminated, its ip is None.
315        else:
316            self._ssh_tunnel_is_connected = False
317            self._fullname = (_FULL_NAME_STRING %
318                              {"device_serial": "terminated",
319                               "instance_name": self._name,
320                               "elapsed_time": self._elapsed_time})
321
322    @staticmethod
323    def GetAdbVncPortFromSSHTunnel(ip, avd_type):
324        """Get forwarding adb and vnc port from ssh tunnel.
325
326        Args:
327            ip: String, ip address.
328            avd_type: String, the AVD type.
329
330        Returns:
331            NamedTuple ForwardedPorts(vnc_port, adb_port) holding the ports
332            used in the ssh forwarded call. Both fields are integers.
333        """
334        process_output = subprocess.check_output(constants.COMMAND_PS)
335        default_vnc_port = utils.AVD_PORT_DICT[avd_type].vnc_port
336        default_adb_port = utils.AVD_PORT_DICT[avd_type].adb_port
337        re_pattern = re.compile(_RE_SSH_TUNNEL_PATTERN %
338                                (_RE_GROUP_VNC, default_vnc_port,
339                                 _RE_GROUP_ADB, default_adb_port, ip))
340
341        adb_port = None
342        vnc_port = None
343        for line in process_output.splitlines():
344            match = re_pattern.match(line)
345            if match:
346                adb_port = int(match.group(_RE_GROUP_ADB))
347                vnc_port = int(match.group(_RE_GROUP_VNC))
348                break
349
350        logger.debug(("grathering detail for ssh tunnel. "
351                      "IP:%s, forwarding (adb:%d, vnc:%d)"), ip, adb_port,
352                     vnc_port)
353
354        return utils.ForwardedPorts(vnc_port=vnc_port, adb_port=adb_port)
355