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