blob: 682a17798f43d72da3a2cc73538f38b9dbb00285 [file] [log] [blame]
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001# Copyright 2016 - 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.
Fang Deng26e4dc12018-03-04 19:01:59 -080014"""Common Utilities."""
Sam Chiu99dfee32018-11-20 10:19:17 +080015# pylint: disable=too-many-lines
Sam Chiu81bdc652018-06-29 18:45:08 +080016from __future__ import print_function
cylan66713722018-10-06 01:38:26 +080017
18from distutils.spawn import find_executable
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070019import base64
20import binascii
cylan66713722018-10-06 01:38:26 +080021import collections
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070022import errno
Fang Deng69498c32017-03-02 14:29:30 -080023import getpass
herbertxue07293a32018-11-05 20:40:11 +080024import grp
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070025import logging
26import os
Sam Chiu6c738d62018-12-04 10:29:02 +080027import platform
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070028import shutil
29import struct
cylan66713722018-10-06 01:38:26 +080030import socket
Fang Deng69498c32017-03-02 14:29:30 -080031import subprocess
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070032import sys
33import tarfile
34import tempfile
35import time
36import uuid
chojoycecd004bc2018-09-13 10:39:00 +080037import zipfile
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070038
chojoycecd004bc2018-09-13 10:39:00 +080039from acloud import errors as root_errors
herbertxue34776bb2018-07-03 21:57:48 +080040from acloud.internal import constants
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070041from acloud.public import errors
42
43logger = logging.getLogger(__name__)
44
Fang Deng69498c32017-03-02 14:29:30 -080045SSH_KEYGEN_CMD = ["ssh-keygen", "-t", "rsa", "-b", "4096"]
cylan4f73c1f2018-07-19 16:40:31 +080046SSH_KEYGEN_PUB_CMD = ["ssh-keygen", "-y"]
Kevin Chengce6cfb02018-12-04 13:21:31 -080047SSH_ARGS = ["-o", "UserKnownHostsFile=/dev/null",
48 "-o", "StrictHostKeyChecking=no"]
49SSH_CMD = ["ssh"] + SSH_ARGS
50SCP_CMD = ["scp"] + SSH_ARGS
Kevin Chengd25feee2018-05-24 10:15:20 -070051DEFAULT_RETRY_BACKOFF_FACTOR = 1
52DEFAULT_SLEEP_MULTIPLIER = 0
Fang Deng69498c32017-03-02 14:29:30 -080053
cylan66713722018-10-06 01:38:26 +080054_SSH_TUNNEL_ARGS = ("-i %(rsa_key_file)s -o UserKnownHostsFile=/dev/null "
55 "-o StrictHostKeyChecking=no "
56 "-L %(vnc_port)d:127.0.0.1:%(target_vnc_port)d "
57 "-L %(adb_port)d:127.0.0.1:%(target_adb_port)d "
58 "-N -f -l %(ssh_user)s %(ip_addr)s")
cylan66713722018-10-06 01:38:26 +080059_ADB_CONNECT_ARGS = "connect 127.0.0.1:%(adb_port)d"
60# Store the ports that vnc/adb are forwarded to, both are integers.
61ForwardedPorts = collections.namedtuple("ForwardedPorts", [constants.VNC_PORT,
62 constants.ADB_PORT])
Kevin Chengae7a49d2018-10-18 14:11:22 -070063_VNC_BIN = "ssvnc"
herbertxue07293a32018-11-05 20:40:11 +080064_CMD_PGREP = "pgrep"
65_CMD_SG = "sg "
cylan4569dca2018-11-02 12:12:53 +080066_CMD_START_VNC = "%(bin)s vnc://127.0.0.1:%(port)d"
Kevin Chengae7a49d2018-10-18 14:11:22 -070067_CMD_INSTALL_SSVNC = "sudo apt-get --assume-yes install ssvnc"
68_ENV_DISPLAY = "DISPLAY"
Sam Chiu7a477f52018-10-22 11:20:36 +080069_SSVNC_ENV_VARS = {"SSVNC_NO_ENC_WARN": "1", "SSVNC_SCALE": "auto"}
Kevin Cheng53aa5a52018-12-03 01:33:55 -080070_DEFAULT_DISPLAY_SCALE = 1.0
Kevin Chengae7a49d2018-10-18 14:11:22 -070071
72_CONFIRM_CONTINUE = ("In order to display the screen to the AVD, we'll need to "
73 "install a vnc client (ssnvc). \nWould you like acloud to "
74 "install it for you? (%s) \nPress 'y' to continue or "
75 "anything else to abort it:[y] ") % _CMD_INSTALL_SSVNC
Sam Chiu99dfee32018-11-20 10:19:17 +080076_EvaluatedResult = collections.namedtuple("EvaluatedResult",
77 ["is_result_ok", "result_message"])
Sam Chiu6c738d62018-12-04 10:29:02 +080078# dict of supported system and their distributions.
79_SUPPORTED_SYSTEMS_AND_DISTS = {"Linux": ["Ubuntu", "Debian"]}
Fang Deng69498c32017-03-02 14:29:30 -080080
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070081class TempDir(object):
Fang Deng26e4dc12018-03-04 19:01:59 -080082 """A context manager that ceates a temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070083
Fang Deng26e4dc12018-03-04 19:01:59 -080084 Attributes:
Sam Chiu81bdc652018-06-29 18:45:08 +080085 path: The path of the temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070086 """
87
Fang Deng26e4dc12018-03-04 19:01:59 -080088 def __init__(self):
89 self.path = tempfile.mkdtemp()
90 os.chmod(self.path, 0o700)
91 logger.debug("Created temporary dir %s", self.path)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070092
93 def __enter__(self):
Fang Deng26e4dc12018-03-04 19:01:59 -080094 """Enter."""
95 return self.path
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070096
Fang Deng26e4dc12018-03-04 19:01:59 -080097 def __exit__(self, exc_type, exc_value, traceback):
98 """Exit.
99
100 Args:
101 exc_type: Exception type raised within the context manager.
102 None if no execption is raised.
103 exc_value: Exception instance raised within the context manager.
104 None if no execption is raised.
105 traceback: Traceback for exeception that is raised within
106 the context manager.
107 None if no execption is raised.
108 Raises:
109 EnvironmentError or OSError when failed to delete temp directory.
110 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700111 try:
Fang Deng26e4dc12018-03-04 19:01:59 -0800112 if self.path:
113 shutil.rmtree(self.path)
114 logger.debug("Deleted temporary dir %s", self.path)
115 except EnvironmentError as e:
116 # Ignore error if there is no exception raised
117 # within the with-clause and the EnvironementError is
118 # about problem that directory or file does not exist.
119 if not exc_type and e.errno != errno.ENOENT:
120 raise
121 except Exception as e: # pylint: disable=W0703
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700122 if exc_type:
Fang Deng26e4dc12018-03-04 19:01:59 -0800123 logger.error(
cylan0d77ae12018-05-18 08:36:48 +0000124 "Encountered error while deleting %s: %s",
125 self.path,
126 str(e),
127 exc_info=True)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700128 else:
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700129 raise
130
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700131
cylan0d77ae12018-05-18 08:36:48 +0000132def RetryOnException(retry_checker,
133 max_retries,
134 sleep_multiplier=0,
Fang Dengf24be082018-02-10 10:09:55 -0800135 retry_backoff_factor=1):
cylan0d77ae12018-05-18 08:36:48 +0000136 """Decorater which retries the function call if |retry_checker| returns true.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700137
cylan0d77ae12018-05-18 08:36:48 +0000138 Args:
139 retry_checker: A callback function which should take an exception instance
140 and return True if functor(*args, **kwargs) should be retried
141 when such exception is raised, and return False if it should
142 not be retried.
143 max_retries: Maximum number of retries allowed.
144 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
145 retry_backoff_factor is 1. Will sleep
146 sleep_multiplier * (
147 retry_backoff_factor ** (attempt_count - 1))
148 if retry_backoff_factor != 1.
149 retry_backoff_factor: See explanation of sleep_multiplier.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700150
cylan0d77ae12018-05-18 08:36:48 +0000151 Returns:
152 The function wrapper.
153 """
154
155 def _Wrapper(func):
156 def _FunctionWrapper(*args, **kwargs):
157 return Retry(retry_checker, max_retries, func, sleep_multiplier,
158 retry_backoff_factor, *args, **kwargs)
159
160 return _FunctionWrapper
161
162 return _Wrapper
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700163
164
cylan4f73c1f2018-07-19 16:40:31 +0800165def Retry(retry_checker, max_retries, functor, sleep_multiplier,
166 retry_backoff_factor, *args, **kwargs):
cylan0d77ae12018-05-18 08:36:48 +0000167 """Conditionally retry a function.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700168
cylan0d77ae12018-05-18 08:36:48 +0000169 Args:
170 retry_checker: A callback function which should take an exception instance
171 and return True if functor(*args, **kwargs) should be retried
172 when such exception is raised, and return False if it should
173 not be retried.
174 max_retries: Maximum number of retries allowed.
175 functor: The function to call, will call functor(*args, **kwargs).
176 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
177 retry_backoff_factor is 1. Will sleep
178 sleep_multiplier * (
179 retry_backoff_factor ** (attempt_count - 1))
180 if retry_backoff_factor != 1.
181 retry_backoff_factor: See explanation of sleep_multiplier.
182 *args: Arguments to pass to the functor.
183 **kwargs: Key-val based arguments to pass to the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700184
cylan0d77ae12018-05-18 08:36:48 +0000185 Returns:
186 The return value of the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700187
cylan0d77ae12018-05-18 08:36:48 +0000188 Raises:
189 Exception: The exception that functor(*args, **kwargs) throws.
190 """
191 attempt_count = 0
192 while attempt_count <= max_retries:
193 try:
194 attempt_count += 1
195 return_value = functor(*args, **kwargs)
196 return return_value
197 except Exception as e: # pylint: disable=W0703
198 if retry_checker(e) and attempt_count <= max_retries:
199 if retry_backoff_factor != 1:
200 sleep = sleep_multiplier * (retry_backoff_factor**
201 (attempt_count - 1))
202 else:
203 sleep = sleep_multiplier * attempt_count
Kevin Chengd25feee2018-05-24 10:15:20 -0700204 time.sleep(sleep)
cylan0d77ae12018-05-18 08:36:48 +0000205 else:
206 raise
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700207
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700208
Fang Dengf24be082018-02-10 10:09:55 -0800209def RetryExceptionType(exception_types, max_retries, functor, *args, **kwargs):
cylan0d77ae12018-05-18 08:36:48 +0000210 """Retry exception if it is one of the given types.
Fang Dengf24be082018-02-10 10:09:55 -0800211
cylan0d77ae12018-05-18 08:36:48 +0000212 Args:
213 exception_types: A tuple of exception types, e.g. (ValueError, KeyError)
214 max_retries: Max number of retries allowed.
215 functor: The function to call. Will be retried if exception is raised and
216 the exception is one of the exception_types.
217 *args: Arguments to pass to Retry function.
218 **kwargs: Key-val based arguments to pass to Retry functions.
Fang Dengf24be082018-02-10 10:09:55 -0800219
cylan0d77ae12018-05-18 08:36:48 +0000220 Returns:
221 The value returned by calling functor.
222 """
223 return Retry(lambda e: isinstance(e, exception_types), max_retries,
224 functor, *args, **kwargs)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700225
226
227def PollAndWait(func, expected_return, timeout_exception, timeout_secs,
228 sleep_interval_secs, *args, **kwargs):
229 """Call a function until the function returns expected value or times out.
230
231 Args:
232 func: Function to call.
233 expected_return: The expected return value.
234 timeout_exception: Exception to raise when it hits timeout.
235 timeout_secs: Timeout seconds.
236 If 0 or less than zero, the function will run once and
237 we will not wait on it.
238 sleep_interval_secs: Time to sleep between two attemps.
239 *args: list of args to pass to func.
240 **kwargs: dictionary of keyword based args to pass to func.
241
242 Raises:
243 timeout_exception: if the run of function times out.
244 """
245 # TODO(fdeng): Currently this method does not kill
246 # |func|, if |func| takes longer than |timeout_secs|.
247 # We can use a more robust version from chromite.
248 start = time.time()
249 while True:
250 return_value = func(*args, **kwargs)
251 if return_value == expected_return:
252 return
253 elif time.time() - start > timeout_secs:
254 raise timeout_exception
255 else:
256 if sleep_interval_secs > 0:
257 time.sleep(sleep_interval_secs)
258
259
260def GenerateUniqueName(prefix=None, suffix=None):
Sam Chiu81bdc652018-06-29 18:45:08 +0800261 """Generate a random unique name using uuid4.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700262
263 Args:
264 prefix: String, desired prefix to prepend to the generated name.
265 suffix: String, desired suffix to append to the generated name.
266
267 Returns:
268 String, a random name.
269 """
270 name = uuid.uuid4().hex
271 if prefix:
272 name = "-".join([prefix, name])
273 if suffix:
274 name = "-".join([name, suffix])
275 return name
276
277
278def MakeTarFile(src_dict, dest):
279 """Archive files in tar.gz format to a file named as |dest|.
280
281 Args:
282 src_dict: A dictionary that maps a path to be archived
283 to the corresponding name that appears in the archive.
284 dest: String, path to output file, e.g. /tmp/myfile.tar.gz
285 """
286 logger.info("Compressing %s into %s.", src_dict.keys(), dest)
287 with tarfile.open(dest, "w:gz") as tar:
288 for src, arcname in src_dict.iteritems():
289 tar.add(src, arcname=arcname)
290
291
Kevin Chengce6cfb02018-12-04 13:21:31 -0800292def ScpPullFile(src_file, dst_file, host_name, user_name=None,
293 rsa_key_file=None):
294 """Scp pull file from remote.
295
296 Args:
297 src_file: The source file path to be pulled.
298 dst_file: The destiation file path the file is pulled to.
299 host_name: The device host_name or ip to pull file from.
300 user_name: The user_name for scp session.
301 rsa_key_file: The rsa key file.
302 Raises:
303 errors.DeviceConnectionError if scp failed.
304 """
305 scp_cmd_list = SCP_CMD[:]
306 if rsa_key_file:
307 scp_cmd_list.extend(["-i", rsa_key_file])
308 else:
309 logger.warning(
310 "Rsa key file is not specified. "
311 "Will use default rsa key set in user environment")
312 if user_name:
313 scp_cmd_list.append("%s@%s:%s" % (user_name, host_name, src_file))
314 else:
315 scp_cmd_list.append("%s:%s" % (host_name, src_file))
316 scp_cmd_list.append(dst_file)
317 try:
318 subprocess.check_call(scp_cmd_list)
319 except subprocess.CalledProcessError as e:
320 raise errors.DeviceConnectionError(
321 "Failed to pull file %s from %s with '%s': %s" % (
322 src_file, host_name, " ".join(scp_cmd_list), e))
323
324
Fang Deng69498c32017-03-02 14:29:30 -0800325def CreateSshKeyPairIfNotExist(private_key_path, public_key_path):
326 """Create the ssh key pair if they don't exist.
327
cylan4f73c1f2018-07-19 16:40:31 +0800328 Case1. If the private key doesn't exist, we will create both the public key
329 and the private key.
330 Case2. If the private key exists but public key doesn't, we will create the
331 public key by using the private key.
332 Case3. If the public key exists but the private key doesn't, we will create
333 a new private key and overwrite the public key.
Fang Deng69498c32017-03-02 14:29:30 -0800334
335 Args:
336 private_key_path: Path to the private key file.
337 e.g. ~/.ssh/acloud_rsa
338 public_key_path: Path to the public key file.
339 e.g. ~/.ssh/acloud_rsa.pub
cylan4f73c1f2018-07-19 16:40:31 +0800340
Fang Deng69498c32017-03-02 14:29:30 -0800341 Raises:
342 error.DriverError: If failed to create the key pair.
343 """
344 public_key_path = os.path.expanduser(public_key_path)
345 private_key_path = os.path.expanduser(private_key_path)
cylan4f73c1f2018-07-19 16:40:31 +0800346 public_key_exist = os.path.exists(public_key_path)
347 private_key_exist = os.path.exists(private_key_path)
348 if public_key_exist and private_key_exist:
cylan0d77ae12018-05-18 08:36:48 +0000349 logger.debug(
cylan4f73c1f2018-07-19 16:40:31 +0800350 "The ssh private key (%s) and public key (%s) already exist,"
cylan0d77ae12018-05-18 08:36:48 +0000351 "will not automatically create the key pairs.", private_key_path,
352 public_key_path)
Fang Deng69498c32017-03-02 14:29:30 -0800353 return
cylan4f73c1f2018-07-19 16:40:31 +0800354 key_folder = os.path.dirname(private_key_path)
355 if not os.path.exists(key_folder):
356 os.makedirs(key_folder)
Fang Deng69498c32017-03-02 14:29:30 -0800357 try:
cylan4f73c1f2018-07-19 16:40:31 +0800358 if private_key_exist:
359 cmd = SSH_KEYGEN_PUB_CMD + ["-f", private_key_path]
360 with open(public_key_path, 'w') as outfile:
361 stream_content = subprocess.check_output(cmd)
362 outfile.write(
363 stream_content.rstrip('\n') + " " + getpass.getuser())
364 logger.info(
365 "The ssh public key (%s) do not exist, "
366 "automatically creating public key, calling: %s",
367 public_key_path, " ".join(cmd))
368 else:
369 cmd = SSH_KEYGEN_CMD + [
370 "-C", getpass.getuser(), "-f", private_key_path
371 ]
372 logger.info(
373 "Creating public key from private key (%s) via cmd: %s",
374 private_key_path, " ".join(cmd))
375 subprocess.check_call(cmd, stdout=sys.stderr, stderr=sys.stdout)
Fang Deng69498c32017-03-02 14:29:30 -0800376 except subprocess.CalledProcessError as e:
cylan0d77ae12018-05-18 08:36:48 +0000377 raise errors.DriverError("Failed to create ssh key pair: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800378 except OSError as e:
379 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000380 "Failed to create ssh key pair, please make sure "
381 "'ssh-keygen' is installed: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800382
383 # By default ssh-keygen will create a public key file
384 # by append .pub to the private key file name. Rename it
385 # to what's requested by public_key_path.
386 default_pub_key_path = "%s.pub" % private_key_path
387 try:
388 if default_pub_key_path != public_key_path:
389 os.rename(default_pub_key_path, public_key_path)
390 except OSError as e:
391 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000392 "Failed to rename %s to %s: %s" % (default_pub_key_path,
393 public_key_path, str(e)))
Fang Deng69498c32017-03-02 14:29:30 -0800394
395 logger.info("Created ssh private key (%s) and public key (%s)",
396 private_key_path, public_key_path)
397
398
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700399def VerifyRsaPubKey(rsa):
400 """Verify the format of rsa public key.
401
402 Args:
403 rsa: content of rsa public key. It should follow the format of
404 ssh-rsa AAAAB3NzaC1yc2EA.... test@test.com
405
406 Raises:
407 DriverError if the format is not correct.
408 """
409 if not rsa or not all(ord(c) < 128 for c in rsa):
410 raise errors.DriverError(
411 "rsa key is empty or contains non-ascii character: %s" % rsa)
412
413 elements = rsa.split()
414 if len(elements) != 3:
415 raise errors.DriverError("rsa key is invalid, wrong format: %s" % rsa)
416
417 key_type, data, _ = elements
418 try:
419 binary_data = base64.decodestring(data)
420 # number of bytes of int type
421 int_length = 4
422 # binary_data is like "7ssh-key..." in a binary format.
423 # The first 4 bytes should represent 7, which should be
424 # the length of the following string "ssh-key".
425 # And the next 7 bytes should be string "ssh-key".
426 # We will verify that the rsa conforms to this format.
427 # ">I" in the following line means "big-endian unsigned integer".
428 type_length = struct.unpack(">I", binary_data[:int_length])[0]
429 if binary_data[int_length:int_length + type_length] != key_type:
430 raise errors.DriverError("rsa key is invalid: %s" % rsa)
431 except (struct.error, binascii.Error) as e:
cylan0d77ae12018-05-18 08:36:48 +0000432 raise errors.DriverError(
433 "rsa key is invalid: %s, error: %s" % (rsa, str(e)))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700434
chojoycecd004bc2018-09-13 10:39:00 +0800435def Decompress(sourcefile, dest=None):
436 """Decompress .zip or .tar.gz.
437
438 Args:
439 sourcefile: A string, a source file path to decompress.
440 dest: A string, a folder path as decompress destination.
441
442 Raises:
443 errors.UnsupportedCompressionFileType: Not supported extension.
444 """
445 logger.info("Start to decompress %s!", sourcefile)
446 dest_path = dest if dest else "."
447 if sourcefile.endswith(".tar.gz"):
448 with tarfile.open(sourcefile, "r:gz") as compressor:
449 compressor.extractall(dest_path)
450 elif sourcefile.endswith(".zip"):
451 with zipfile.ZipFile(sourcefile, 'r') as compressor:
452 compressor.extractall(dest_path)
453 else:
454 raise root_errors.UnsupportedCompressionFileType(
455 "Sorry, we could only support compression file type "
456 "for zip or tar.gz.")
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700457
Sam Chiu81bdc652018-06-29 18:45:08 +0800458# pylint: disable=old-style-class,no-init
459class TextColors:
460 """A class that defines common color ANSI code."""
461
462 HEADER = "\033[95m"
463 OKBLUE = "\033[94m"
464 OKGREEN = "\033[92m"
465 WARNING = "\033[93m"
466 FAIL = "\033[91m"
467 ENDC = "\033[0m"
468 BOLD = "\033[1m"
469 UNDERLINE = "\033[4m"
470
471
herbertxuedf01c422018-09-06 19:52:52 +0800472def PrintColorString(message, colors=TextColors.OKBLUE, **kwargs):
Sam Chiu81bdc652018-06-29 18:45:08 +0800473 """A helper function to print out colored text.
474
herbertxuedf01c422018-09-06 19:52:52 +0800475 Use print function "print(message, end="")" to show message in one line.
476 Example code:
477 DisplayMessages("Creating GCE instance...", end="")
478 # Job execute 20s
479 DisplayMessages("Done! (20s)")
480 Display:
481 Creating GCE instance...
482 # After job finished, messages update as following:
483 Creating GCE instance...Done! (20s)
484
Sam Chiu81bdc652018-06-29 18:45:08 +0800485 Args:
486 message: String, the message text.
487 colors: String, color code.
herbertxuedf01c422018-09-06 19:52:52 +0800488 **kwargs: dictionary of keyword based args to pass to func.
Sam Chiu81bdc652018-06-29 18:45:08 +0800489 """
herbertxuedf01c422018-09-06 19:52:52 +0800490 print(colors + message + TextColors.ENDC, **kwargs)
491 sys.stdout.flush()
Sam Chiu81bdc652018-06-29 18:45:08 +0800492
493
494def InteractWithQuestion(question, colors=TextColors.WARNING):
495 """A helper function to define the common way to run interactive cmd.
496
497 Args:
498 question: String, the question to ask user.
499 colors: String, color code.
500
501 Returns:
502 String, input from user.
503 """
504 return str(raw_input(colors + question + TextColors.ENDC).strip())
505
herbertxuedf01c422018-09-06 19:52:52 +0800506
herbertxue34776bb2018-07-03 21:57:48 +0800507def GetUserAnswerYes(question):
508 """Ask user about acloud setup question.
509
510 Args:
511 question: String, ask question for user.
512 Ex: "Are you sure to change bucket name:[y/n]"
513
514 Returns:
515 Boolean, True if answer is "Yes", False otherwise.
516 """
517 answer = InteractWithQuestion(question)
518 return answer.lower() in constants.USER_ANSWER_YES
519
Sam Chiu81bdc652018-06-29 18:45:08 +0800520
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700521class BatchHttpRequestExecutor(object):
522 """A helper class that executes requests in batch with retry.
523
524 This executor executes http requests in a batch and retry
525 those that have failed. It iteratively updates the dictionary
526 self._final_results with latest results, which can be retrieved
527 via GetResults.
528 """
529
530 def __init__(self,
531 execute_once_functor,
532 requests,
533 retry_http_codes=None,
534 max_retry=None,
535 sleep=None,
536 backoff_factor=None,
537 other_retriable_errors=None):
538 """Initializes the executor.
539
540 Args:
541 execute_once_functor: A function that execute requests in batch once.
542 It should return a dictionary like
543 {request_id: (response, exception)}
544 requests: A dictionary where key is request id picked by caller,
545 and value is a apiclient.http.HttpRequest.
546 retry_http_codes: A list of http codes to retry.
Fang Dengf24be082018-02-10 10:09:55 -0800547 max_retry: See utils.Retry.
548 sleep: See utils.Retry.
549 backoff_factor: See utils.Retry.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700550 other_retriable_errors: A tuple of error types that should be retried
551 other than errors.HttpError.
552 """
553 self._execute_once_functor = execute_once_functor
554 self._requests = requests
555 # A dictionary that maps request id to pending request.
556 self._pending_requests = {}
557 # A dictionary that maps request id to a tuple (response, exception).
558 self._final_results = {}
559 self._retry_http_codes = retry_http_codes
560 self._max_retry = max_retry
561 self._sleep = sleep
562 self._backoff_factor = backoff_factor
563 self._other_retriable_errors = other_retriable_errors
564
565 def _ShoudRetry(self, exception):
Sam Chiu81bdc652018-06-29 18:45:08 +0800566 """Check if an exception is retriable.
567
568 Args:
569 exception: An exception instance.
570 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700571 if isinstance(exception, self._other_retriable_errors):
572 return True
573
cylan0d77ae12018-05-18 08:36:48 +0000574 if (isinstance(exception, errors.HttpError)
575 and exception.code in self._retry_http_codes):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700576 return True
577 return False
578
579 def _ExecuteOnce(self):
580 """Executes pending requests and update it with failed, retriable ones.
581
582 Raises:
583 HasRetriableRequestsError: if some requests fail and are retriable.
584 """
585 results = self._execute_once_functor(self._pending_requests)
586 # Update final_results with latest results.
587 self._final_results.update(results)
588 # Clear pending_requests
589 self._pending_requests.clear()
590 for request_id, result in results.iteritems():
591 exception = result[1]
592 if exception is not None and self._ShoudRetry(exception):
593 # If this is a retriable exception, put it in pending_requests
594 self._pending_requests[request_id] = self._requests[request_id]
595 if self._pending_requests:
596 # If there is still retriable requests pending, raise an error
Fang Dengf24be082018-02-10 10:09:55 -0800597 # so that Retry will retry this function with pending_requests.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700598 raise errors.HasRetriableRequestsError(
cylan0d77ae12018-05-18 08:36:48 +0000599 "Retriable errors: %s" %
600 [str(results[rid][1]) for rid in self._pending_requests])
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700601
602 def Execute(self):
603 """Executes the requests and retry if necessary.
604
605 Will populate self._final_results.
606 """
cylan0d77ae12018-05-18 08:36:48 +0000607
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700608 def _ShouldRetryHandler(exc):
609 """Check if |exc| is a retriable exception.
610
611 Args:
612 exc: An exception.
613
614 Returns:
615 True if exception is of type HasRetriableRequestsError; False otherwise.
616 """
617 should_retry = isinstance(exc, errors.HasRetriableRequestsError)
618 if should_retry:
619 logger.info("Will retry failed requests.", exc_info=True)
620 logger.info("%s", exc)
621 return should_retry
622
623 try:
624 self._pending_requests = self._requests.copy()
Fang Dengf24be082018-02-10 10:09:55 -0800625 Retry(
cylan0d77ae12018-05-18 08:36:48 +0000626 _ShouldRetryHandler,
627 max_retries=self._max_retry,
Fang Dengf24be082018-02-10 10:09:55 -0800628 functor=self._ExecuteOnce,
629 sleep_multiplier=self._sleep,
630 retry_backoff_factor=self._backoff_factor)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700631 except errors.HasRetriableRequestsError:
632 logger.debug("Some requests did not succeed after retry.")
633
634 def GetResults(self):
635 """Returns final results.
636
637 Returns:
638 results, a dictionary in the following format
639 {request_id: (response, exception)}
640 request_ids are those from requests; response
641 is the http response for the request or None on error;
642 exception is an instance of DriverError or None if no error.
643 """
644 return self._final_results
cylan31fc5332018-09-17 22:12:08 +0800645
646
Sam Chiu99dfee32018-11-20 10:19:17 +0800647def DefaultEvaluator(result):
648 """Default Evaluator always return result is ok.
649
650 Args:
651 result:the return value of the target function.
652 """
653 return _EvaluatedResult(is_result_ok=True, result_message=result)
654
655
656def ReportEvaluator(report):
657 """Evalute the the acloud operation by the report.
658
659 Args:
660 report:acloud.public.report() object.
661 """
662 if report is None or report.errors:
663 return _EvaluatedResult(is_result_ok=False, result_message=report.errors)
664
665 return _EvaluatedResult(is_result_ok=True, result_message=None)
666
667
cylan31fc5332018-09-17 22:12:08 +0800668class TimeExecute(object):
669 """Count the function execute time."""
670
Sam Chiu99dfee32018-11-20 10:19:17 +0800671 def __init__(self, function_description=None, print_before_call=True,
672 print_status=True, result_evaluator=DefaultEvaluator,
673 display_waiting_dots=True):
cylan31fc5332018-09-17 22:12:08 +0800674 """Initializes the class.
675
676 Args:
677 function_description: String that describes function (e.g."Creating
678 Instance...")
679 print_before_call: Boolean, print the function description before
680 calling the function, default True.
681 print_status: Boolean, print the status of the function after the
682 function has completed, default True ("OK" or "Fail").
Sam Chiu99dfee32018-11-20 10:19:17 +0800683 result_evaluator: Func object. Pass func to evaluate result.
684 Default evaluator always report result is ok and
685 failed result will be identified only in exception
686 case.
687 display_waiting_dots: Boolean, if true print the function_description
688 followed by waiting dot.
cylan31fc5332018-09-17 22:12:08 +0800689 """
690 self._function_description = function_description
691 self._print_before_call = print_before_call
692 self._print_status = print_status
Sam Chiu99dfee32018-11-20 10:19:17 +0800693 self._result_evaluator = result_evaluator
694 self._display_waiting_dots = display_waiting_dots
cylan31fc5332018-09-17 22:12:08 +0800695
696 def __call__(self, func):
697 def DecoratorFunction(*args, **kargs):
698 """Decorator function.
699
700 Args:
701 *args: Arguments to pass to the functor.
702 **kwargs: Key-val based arguments to pass to the functor.
703
704 Raises:
705 Exception: The exception that functor(*args, **kwargs) throws.
706 """
707 timestart = time.time()
708 if self._print_before_call:
Sam Chiu99dfee32018-11-20 10:19:17 +0800709 waiting_dots = "..." if self._display_waiting_dots else ""
710 PrintColorString("%s %s"% (self._function_description,
711 waiting_dots), end="")
cylan31fc5332018-09-17 22:12:08 +0800712 try:
713 result = func(*args, **kargs)
Sam Chiu99dfee32018-11-20 10:19:17 +0800714 result_time = time.time() - timestart
cylan31fc5332018-09-17 22:12:08 +0800715 if not self._print_before_call:
716 PrintColorString("%s (%ds)" % (self._function_description,
Sam Chiu99dfee32018-11-20 10:19:17 +0800717 result_time),
cylan31fc5332018-09-17 22:12:08 +0800718 TextColors.OKGREEN)
719 if self._print_status:
Sam Chiu99dfee32018-11-20 10:19:17 +0800720 evaluated_result = self._result_evaluator(result)
721 if evaluated_result.is_result_ok:
722 PrintColorString("OK! (%ds)" % (result_time),
723 TextColors.OKGREEN)
724 else:
725 PrintColorString("Fail! (%ds)" % (result_time),
726 TextColors.FAIL)
727 PrintColorString("Error: %s" %
728 evaluated_result.result_message,
729 TextColors.FAIL)
cylan31fc5332018-09-17 22:12:08 +0800730 return result
731 except:
732 if self._print_status:
733 PrintColorString("Fail! (%ds)" % (time.time()-timestart),
734 TextColors.FAIL)
735 raise
736 return DecoratorFunction
cylan66713722018-10-06 01:38:26 +0800737
738
739def PickFreePort():
740 """Helper to pick a free port.
741
742 Returns:
743 Integer, a free port number.
744 """
745 tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
746 tcp_socket.bind(("", 0))
747 port = tcp_socket.getsockname()[1]
748 tcp_socket.close()
749 return port
750
751
752def _ExecuteCommand(cmd, args):
753 """Execute command.
754
755 Args:
756 cmd: Strings of execute binary name.
757 args: List of args to pass in with cmd.
758
759 Raises:
760 errors.NoExecuteBin: Can't find the execute bin file.
761 """
762 bin_path = find_executable(cmd)
763 if not bin_path:
764 raise root_errors.NoExecuteCmd("unable to locate %s" % cmd)
765 command = [bin_path] + args
766 logger.debug("Running '%s'", ' '.join(command))
767 with open(os.devnull, "w") as dev_null:
768 subprocess.check_call(command, stderr=dev_null, stdout=dev_null)
769
770
771# pylint: disable=too-many-locals
772def AutoConnect(ip_addr, rsa_key_file, target_vnc_port, target_adb_port, ssh_user):
773 """Autoconnect to an AVD instance.
774
775 Args:
776 ip_addr: String, use to build the adb & vnc tunnel between local
777 and remote instance.
778 rsa_key_file: String, Private key file path to use when creating
779 the ssh tunnels.
780 target_vnc_port: Integer of target vnc port number.
781 target_adb_port: Integer of target adb port number.
782 ssh_user: String of user login into the instance.
783
784 Returns:
785 NamedTuple of (vnc_port, adb_port) SSHTUNNEL of the connect, both are
786 integers.
787 """
788 local_free_vnc_port = PickFreePort()
789 local_free_adb_port = PickFreePort()
790 try:
791 ssh_tunnel_args = _SSH_TUNNEL_ARGS % {
792 "rsa_key_file": rsa_key_file,
793 "vnc_port": local_free_vnc_port,
794 "adb_port": local_free_adb_port,
795 "target_vnc_port": target_vnc_port,
796 "target_adb_port": target_adb_port,
797 "ssh_user": ssh_user,
798 "ip_addr": ip_addr}
Kevin Cheng835a4152018-10-11 10:46:57 -0700799 _ExecuteCommand(constants.SSH_BIN, ssh_tunnel_args.split())
cylan66713722018-10-06 01:38:26 +0800800 except subprocess.CalledProcessError:
801 PrintColorString("Failed to create ssh tunnels, retry with '#acloud "
802 "reconnect'.", TextColors.FAIL)
803 try:
804 adb_connect_args = _ADB_CONNECT_ARGS % {"adb_port": local_free_adb_port}
Kevin Cheng835a4152018-10-11 10:46:57 -0700805 _ExecuteCommand(constants.ADB_BIN, adb_connect_args.split())
cylan66713722018-10-06 01:38:26 +0800806 except subprocess.CalledProcessError:
807 PrintColorString("Failed to adb connect, retry with "
808 "'#acloud reconnect'", TextColors.FAIL)
809
810 return ForwardedPorts(vnc_port=local_free_vnc_port,
811 adb_port=local_free_adb_port)
Kevin Chengeb85e862018-10-09 15:35:13 -0700812
813
814def GetAnswerFromList(answer_list, enable_choose_all=False):
815 """Get answer from a list.
816
817 Args:
818 answer_list: list of the answers to choose from.
819
820 Return:
821 List holding the answer(s).
822 """
823 print("[0] to exit.")
824 start_index = 1
825 if enable_choose_all:
826 start_index = 2
827 print("[1] for all.")
828 for num, item in enumerate(answer_list, start_index):
829 print("[%d] %s" % (num, item))
830
831 choice = -1
832 max_choice = len(answer_list) + 1
833 while True:
834 try:
835 choice = raw_input("Enter your choice[0-%d]: " % max_choice)
836 choice = int(choice)
837 except ValueError:
838 print("'%s' is not a valid integer.", choice)
839 continue
840 # Filter out choices
841 if choice == 0:
842 print("Exiting acloud.")
843 sys.exit()
844 if enable_choose_all and choice == 1:
845 return answer_list
846 if choice < 0 or choice > max_choice:
847 print("please choose between 0 and %d" % max_choice)
848 else:
849 return [answer_list[choice-start_index]]
Kevin Chengae7a49d2018-10-18 14:11:22 -0700850
851
Sam Chiu7a477f52018-10-22 11:20:36 +0800852def LaunchVNCFromReport(report, avd_spec):
853 """Launch vnc client according to the instances report.
854
855 Args:
856 report: Report object, that stores and generates report.
857 avd_spec: AVDSpec object that tells us what we're going to create.
858 """
859 for device in report.data.get("devices", []):
cylan4569dca2018-11-02 12:12:53 +0800860 LaunchVncClient(device.get(constants.VNC_PORT),
861 avd_width=avd_spec.hw_property["x_res"],
862 avd_height=avd_spec.hw_property["y_res"])
Sam Chiu7a477f52018-10-22 11:20:36 +0800863
864
cylan4569dca2018-11-02 12:12:53 +0800865def LaunchVncClient(port=constants.DEFAULT_VNC_PORT, avd_width=None,
866 avd_height=None):
Kevin Chengae7a49d2018-10-18 14:11:22 -0700867 """Launch ssvnc.
868
869 Args:
870 port: Integer, port number.
Sam Chiu7a477f52018-10-22 11:20:36 +0800871 avd_width: String, the width of avd.
872 avd_height: String, the height of avd.
Kevin Chengae7a49d2018-10-18 14:11:22 -0700873 """
874 try:
875 os.environ[_ENV_DISPLAY]
876 except KeyError:
877 PrintColorString("Remote terminal can't support VNC. "
878 "Skipping VNC startup.", TextColors.FAIL)
879 return
880
881 if not find_executable(_VNC_BIN):
882 if GetUserAnswerYes(_CONFIRM_CONTINUE):
883 try:
884 PrintColorString("Installing ssvnc vnc client... ", end="")
885 sys.stdout.flush()
886 subprocess.check_output(_CMD_INSTALL_SSVNC, shell=True)
887 PrintColorString("Done", TextColors.OKGREEN)
888 except subprocess.CalledProcessError as cpe:
889 PrintColorString("Failed to install ssvnc: %s" %
890 cpe.output, TextColors.FAIL)
891 return
892 else:
893 return
894 ssvnc_env = os.environ.copy()
895 ssvnc_env.update(_SSVNC_ENV_VARS)
Sam Chiu7a477f52018-10-22 11:20:36 +0800896 # Override SSVNC_SCALE
897 if avd_width or avd_height:
898 scale_ratio = CalculateVNCScreenRatio(avd_width, avd_height)
899 ssvnc_env["SSVNC_SCALE"] = str(scale_ratio)
900 logger.debug("SSVNC_SCALE:%s", scale_ratio)
901
902 ssvnc_args = _CMD_START_VNC % {"bin": find_executable(_VNC_BIN),
903 "port": port}
Kevin Chengae7a49d2018-10-18 14:11:22 -0700904 subprocess.Popen(ssvnc_args.split(), env=ssvnc_env)
905
906
907def PrintDeviceSummary(report):
908 """Display summary of devices created.
909
910 -Display created device details from the report instance.
911 report example:
912 'data': [{'devices':[{'instance_name': 'ins-f6a397-none-53363',
913 'ip': u'35.234.10.162'}]}]
914 -Display error message from report.error.
915
916 Args:
917 report: A Report instance.
918 """
919 PrintColorString("\n")
920 PrintColorString("Device(s) created:")
921 for device in report.data.get("devices", []):
922 adb_serial = "(None)"
923 adb_port = device.get("adb_port")
924 if adb_port:
925 adb_serial = constants.LOCALHOST_ADB_SERIAL % adb_port
926 instance_name = device.get("instance_name")
927 instance_ip = device.get("ip")
928 instance_details = "" if not instance_name else "(%s[%s])" % (
929 instance_name, instance_ip)
930 PrintColorString(" - device serial: %s %s" % (adb_serial,
931 instance_details))
932
933 # TODO(b/117245508): Help user to delete instance if it got created.
934 if report.errors:
935 error_msg = "\n".join(report.errors)
936 PrintColorString("Fail in:\n%s\n" % error_msg, TextColors.FAIL)
Sam Chiu7a477f52018-10-22 11:20:36 +0800937
938
939def CalculateVNCScreenRatio(avd_width, avd_height):
940 """calculate the vnc screen scale ratio to fit into user's monitor.
941
942 Args:
943 avd_width: String, the width of avd.
944 avd_height: String, the height of avd.
945 Return:
946 Float, scale ratio for vnc client.
947 """
Kevin Cheng53aa5a52018-12-03 01:33:55 -0800948 try:
949 import Tkinter
950 # Some python interpreters may not be configured for Tk, just return default scale ratio.
951 except ImportError:
952 return _DEFAULT_DISPLAY_SCALE
Sam Chiu7a477f52018-10-22 11:20:36 +0800953 root = Tkinter.Tk()
954 margin = 100 # leave some space on user's monitor.
955 screen_height = root.winfo_screenheight() - margin
956 screen_width = root.winfo_screenwidth() - margin
957
Kevin Cheng53aa5a52018-12-03 01:33:55 -0800958 scale_h = _DEFAULT_DISPLAY_SCALE
959 scale_w = _DEFAULT_DISPLAY_SCALE
Sam Chiu7a477f52018-10-22 11:20:36 +0800960 if float(screen_height) < float(avd_height):
961 scale_h = round(float(screen_height) / float(avd_height), 1)
962
963 if float(screen_width) < float(avd_width):
964 scale_w = round(float(screen_width) / float(avd_width), 1)
965
966 logger.debug("scale_h: %s (screen_h: %s/avd_h: %s),"
967 " scale_w: %s (screen_w: %s/avd_w: %s)",
968 scale_h, screen_height, avd_height,
969 scale_w, screen_width, avd_width)
970
971 # Return the larger scale-down ratio.
972 return scale_h if scale_h < scale_w else scale_w
herbertxue07293a32018-11-05 20:40:11 +0800973
974
975def IsCommandRunning(command):
976 """Check if command is running.
977
978 Args:
979 command: String of command name.
980
981 Returns:
982 Boolean, True if command is running. False otherwise.
983 """
984 try:
985 with open(os.devnull, "w") as dev_null:
cylan4569dca2018-11-02 12:12:53 +0800986 subprocess.check_call([_CMD_PGREP, "-f", command],
herbertxue07293a32018-11-05 20:40:11 +0800987 stderr=dev_null, stdout=dev_null)
988 return True
989 except subprocess.CalledProcessError:
990 return False
991
992
993def AddUserGroupsToCmd(cmd, user_groups):
994 """Add the user groups to the command if necessary.
995
996 As part of local host setup to enable local instance support, the user is
997 added to certain groups. For those settings to take effect systemwide
998 requires the user to log out and log back in. In the scenario where the
999 user has run setup and hasn't logged out, we still want them to be able to
1000 launch a local instance so add the user to the groups as part of the
1001 command to ensure success.
1002
1003 The reason using here-doc instead of '&' is all operations need to be ran in
1004 ths same pid. Here's an example cmd:
1005 $ sg kvm << EOF
1006 sg libvirt
1007 sg cvdnetwork
1008 launch_cvd --cpus 2 --x_res 1280 --y_res 720 --dpi 160 --memory_mb 4096
1009 EOF
1010
1011 Args:
1012 cmd: String of the command to prepend the user groups to.
1013 user_groups: List of user groups name.(String)
1014
1015 Returns:
1016 String of the command with the user groups prepended to it if necessary,
1017 otherwise the same existing command.
1018 """
1019 user_group_cmd = ""
1020 if not CheckUserInGroups(user_groups):
1021 logger.debug("Need to add user groups to the command")
1022 for idx, group in enumerate(user_groups):
1023 user_group_cmd += _CMD_SG + group
1024 if idx == 0:
1025 user_group_cmd += " <<EOF\n"
1026 else:
1027 user_group_cmd += "\n"
1028 cmd += "\nEOF"
1029 user_group_cmd += cmd
1030 logger.debug("user group cmd: %s", user_group_cmd)
1031 return user_group_cmd
1032
1033
1034def CheckUserInGroups(group_name_list):
1035 """Check if the current user is in the group.
1036
1037 Args:
1038 group_name_list: The list of group name.
1039 Returns:
1040 True if current user is in all the groups.
1041 """
1042 logger.info("Checking if user is in following groups: %s", group_name_list)
1043 current_groups = [grp.getgrgid(g).gr_name for g in os.getgroups()]
1044 all_groups_present = True
1045 for group in group_name_list:
1046 if group not in current_groups:
1047 all_groups_present = False
1048 logger.info("missing group: %s", group)
1049 return all_groups_present
Sam Chiu6c738d62018-12-04 10:29:02 +08001050
1051
1052def IsSupportedPlatform(print_warning=False):
1053 """Check if user's os is the supported platform.
1054
1055 Args:
1056 print_warning: Boolean, print the unsupported warning
1057 if True.
1058 Returns:
1059 Boolean, True if user is using supported platform.
1060 """
1061 system = platform.system()
1062 dist = platform.linux_distribution()[0]
1063 platform_supported = (system in _SUPPORTED_SYSTEMS_AND_DISTS and
1064 dist in _SUPPORTED_SYSTEMS_AND_DISTS[system])
1065
1066 logger.info("supported system and dists: %s",
1067 _SUPPORTED_SYSTEMS_AND_DISTS)
1068 platform_supported_msg = ("%s[%s] %s supported platform" %
1069 (system,
1070 dist,
1071 "is a" if platform_supported else "is not a"))
1072 if print_warning and not platform_supported:
1073 PrintColorString(platform_supported_msg, TextColors.WARNING)
1074 else:
1075 logger.info(platform_supported_msg)
1076
1077 return platform_supported