xref: /aosp_15_r20/tools/acloud/internal/lib/utils.py (revision 800a58d989c669b8eb8a71d8df53b1ba3d411444)
1# 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.
14"""Common Utilities."""
15# pylint: disable=too-many-lines
16from __future__ import print_function
17
18import base64
19import binascii
20import collections
21import errno
22import getpass
23import grp
24import logging
25import os
26import platform
27import re
28import shlex
29import shutil
30import signal
31import struct
32import socket
33import stat
34import subprocess
35import sys
36import tarfile
37import tempfile
38import time
39import uuid
40import webbrowser
41import zipfile
42
43from acloud import errors
44from acloud.internal import constants
45
46
47logger = logging.getLogger(__name__)
48
49SSH_KEYGEN_CMD = ["ssh-keygen", "-t", "rsa", "-b", "4096"]
50SSH_KEYGEN_PUB_CMD = ["ssh-keygen", "-y"]
51SSH_ARGS = ["-o", "UserKnownHostsFile=/dev/null",
52            "-o", "StrictHostKeyChecking=no"]
53SSH_CMD = ["ssh"] + SSH_ARGS
54SCP_CMD = ["scp"] + SSH_ARGS
55GET_BUILD_VAR_CMD = ["build/soong/soong_ui.bash", "--dumpvar-mode"]
56DEFAULT_RETRY_BACKOFF_FACTOR = 1
57DEFAULT_SLEEP_MULTIPLIER = 0
58
59_SSH_TUNNEL_ARGS = (
60    "-i %(rsa_key_file)s -o ControlPath=none -o UserKnownHostsFile=/dev/null "
61    "-o StrictHostKeyChecking=no "
62    "%(port_mapping)s"
63    "-N -f -l %(ssh_user)s %(ip_addr)s")
64_SSH_COMMAND_PS = (
65    "exec %(ssh_bin)s -i %(rsa_key_file)s -o ControlPath=none "
66    "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "
67    "%(extra_args)s -l %(ssh_user)s %(ip_addr)s ps aux")
68PORT_MAPPING = "-L %(local_port)d:127.0.0.1:%(target_port)d "
69_RELEASE_PORT_CMD = "kill $(lsof -t -i :%d)"
70_WEBRTC_OPERATOR_PATTERN = re.compile(r"(.+)(webrtc_operator )(.+)")
71_PORT_8443 = 8443
72_PORT_1443 = 1443
73PortMapping = collections.namedtuple("PortMapping", ["local", "target"])
74# Acloud uses only part of default webrtc port range to support both local and remote.
75# The default webrtc port range is [15550, 15599].
76WEBRTC_PORT_START = 15555
77WEBRTC_PORT_END = 15579
78WEBRTC_PORTS_MAPPING = [PortMapping(port, port) for port in range(WEBRTC_PORT_START, WEBRTC_PORT_END + 1)]
79_RE_GROUP_WEBRTC = "local_webrtc_port"
80_RE_WEBRTC_SSH_TUNNEL_PATTERN = (
81    r"((.*-L\s)(?P<local_webrtc_port>\d+):127.0.0.1:%s)(.+%s)")
82_ADB_CONNECT_ARGS = "connect 127.0.0.1:%(adb_port)d"
83# Store the ports that vnc/adb are forwarded to, both are integers.
84ForwardedPorts = collections.namedtuple("ForwardedPorts", [constants.VNC_PORT,
85                                                           constants.ADB_PORT])
86
87AVD_PORT_DICT = {
88    constants.TYPE_GCE: ForwardedPorts(constants.GCE_VNC_PORT,
89                                       constants.GCE_ADB_PORT),
90    constants.TYPE_CF: ForwardedPorts(constants.CF_VNC_PORT,
91                                      constants.CF_ADB_PORT),
92    constants.TYPE_GF: ForwardedPorts(constants.GF_VNC_PORT,
93                                      constants.GF_ADB_PORT),
94    constants.TYPE_CHEEPS: ForwardedPorts(constants.CHEEPS_VNC_PORT,
95                                          constants.CHEEPS_ADB_PORT),
96    constants.TYPE_FVP: ForwardedPorts(None, constants.FVP_ADB_PORT),
97    constants.TYPE_TRUSTY: ForwardedPorts(None, constants.TRUSTY_ADB_PORT),
98}
99
100_VNC_BIN = "ssvnc"
101# search_dirs and the files can be symbolic links. The -H flag makes the
102# command skip the links except search_dirs. The returned files are unique.
103_CMD_FIND_FILES = "find -H %(search_dirs)s -type f"
104_CMD_KILL = ["pkill", "-9", "-f"]
105_CMD_SG = "sg "
106_CMD_START_VNC = "%(bin)s vnc://127.0.0.1:%(port)d"
107_CMD_INSTALL_SSVNC = "sudo apt-get --assume-yes install ssvnc"
108_ENV_DISPLAY = "DISPLAY"
109_SSVNC_ENV_VARS = {"SSVNC_NO_ENC_WARN": "1", "SSVNC_SCALE": "auto", "VNCVIEWER_X11CURSOR": "1"}
110_DEFAULT_DISPLAY_SCALE = 1.0
111_DIST_DIR = "DIST_DIR"
112
113# For webrtc
114_WEBRTC_URL = "https://%(webrtc_ip)s:%(webrtc_port)d"
115
116_CONFIRM_CONTINUE = ("In order to display the screen to the AVD, we'll need to "
117                     "install a vnc client (ssvnc). \nWould you like acloud to "
118                     "install it for you? (%s) \nPress 'y' to continue or "
119                     "anything else to abort it[y/N]: ") % _CMD_INSTALL_SSVNC
120_EvaluatedResult = collections.namedtuple("EvaluatedResult",
121                                          ["is_result_ok", "result_message"])
122# dict of supported system and their distributions.
123_SUPPORTED_SYSTEMS_AND_DISTS = {"Linux": ["Ubuntu", "ubuntu", "Debian", "debian"]}
124_DEFAULT_TIMEOUT_ERR = "Function did not complete within %d secs."
125_SSVNC_VIEWER_PATTERN = "vnc://127.0.0.1:%(vnc_port)d"
126
127# Determine the environment whether to support kvm.
128_KVM_PATH = "/dev/kvm"
129
130
131class TempDir:
132    """A context manager that ceates a temporary directory.
133
134    Attributes:
135        path: The path of the temporary directory.
136    """
137
138    def __init__(self):
139        self.path = tempfile.mkdtemp()
140        os.chmod(self.path, 0o700)
141        logger.debug("Created temporary dir %s", self.path)
142
143    def __enter__(self):
144        """Enter."""
145        return self.path
146
147    def __exit__(self, exc_type, exc_value, traceback):
148        """Exit.
149
150        Args:
151            exc_type: Exception type raised within the context manager.
152                      None if no execption is raised.
153            exc_value: Exception instance raised within the context manager.
154                       None if no execption is raised.
155            traceback: Traceback for exeception that is raised within
156                       the context manager.
157                       None if no execption is raised.
158        Raises:
159            EnvironmentError or OSError when failed to delete temp directory.
160        """
161        try:
162            if self.path:
163                shutil.rmtree(self.path)
164                logger.debug("Deleted temporary dir %s", self.path)
165        except EnvironmentError as e:
166            # Ignore error if there is no exception raised
167            # within the with-clause and the EnvironementError is
168            # about problem that directory or file does not exist.
169            if not exc_type and e.errno != errno.ENOENT:
170                raise
171        except Exception as e:  # pylint: disable=W0703
172            if exc_type:
173                logger.error(
174                    "Encountered error while deleting %s: %s",
175                    self.path,
176                    str(e),
177                    exc_info=True)
178            else:
179                raise
180
181
182def RetryOnException(retry_checker,
183                     max_retries,
184                     sleep_multiplier=0,
185                     retry_backoff_factor=1):
186    """Decorater which retries the function call if |retry_checker| returns true.
187
188    Args:
189        retry_checker: A callback function which should take an exception instance
190                       and return True if functor(*args, **kwargs) should be retried
191                       when such exception is raised, and return False if it should
192                       not be retried.
193        max_retries: Maximum number of retries allowed.
194        sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
195                          retry_backoff_factor is 1.  Will sleep
196                          sleep_multiplier * (
197                              retry_backoff_factor ** (attempt_count -  1))
198                          if retry_backoff_factor != 1.
199        retry_backoff_factor: See explanation of sleep_multiplier.
200
201    Returns:
202        The function wrapper.
203    """
204
205    def _Wrapper(func):
206        def _FunctionWrapper(*args, **kwargs):
207            return Retry(retry_checker, max_retries, func, sleep_multiplier,
208                         retry_backoff_factor, *args, **kwargs)
209
210        return _FunctionWrapper
211
212    return _Wrapper
213
214
215def Retry(retry_checker, max_retries, functor, sleep_multiplier,
216          retry_backoff_factor, *args, **kwargs):
217    """Conditionally retry a function.
218
219    Args:
220        retry_checker: A callback function which should take an exception instance
221                       and return True if functor(*args, **kwargs) should be retried
222                       when such exception is raised, and return False if it should
223                       not be retried.
224        max_retries: Maximum number of retries allowed.
225        functor: The function to call, will call functor(*args, **kwargs).
226        sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
227                          retry_backoff_factor is 1.  Will sleep
228                          sleep_multiplier * (
229                              retry_backoff_factor ** (attempt_count -  1))
230                          if retry_backoff_factor != 1.
231        retry_backoff_factor: See explanation of sleep_multiplier.
232        *args: Arguments to pass to the functor.
233        **kwargs: Key-val based arguments to pass to the functor.
234
235    Returns:
236        The return value of the functor.
237
238    Raises:
239        Exception: The exception that functor(*args, **kwargs) throws.
240    """
241    attempt_count = 0
242    while attempt_count <= max_retries:
243        try:
244            attempt_count += 1
245            return_value = functor(*args, **kwargs)
246            return return_value
247        except Exception as e:  # pylint: disable=W0703
248            if retry_checker(e) and attempt_count <= max_retries:
249                if retry_backoff_factor != 1:
250                    sleep = sleep_multiplier * (retry_backoff_factor**
251                                                (attempt_count - 1))
252                else:
253                    sleep = sleep_multiplier * attempt_count
254                time.sleep(sleep)
255            else:
256                raise
257
258
259def RetryExceptionType(exception_types, max_retries, functor, *args, **kwargs):
260    """Retry exception if it is one of the given types.
261
262    Args:
263        exception_types: A tuple of exception types, e.g. (ValueError, KeyError)
264        max_retries: Max number of retries allowed.
265        functor: The function to call. Will be retried if exception is raised and
266                 the exception is one of the exception_types.
267        *args: Arguments to pass to Retry function.
268        **kwargs: Key-val based arguments to pass to Retry functions.
269
270    Returns:
271        The value returned by calling functor.
272    """
273    return Retry(lambda e: isinstance(e, exception_types), max_retries,
274                 functor, *args, **kwargs)
275
276
277def PollAndWait(func, expected_return, timeout_exception, timeout_secs,
278                sleep_interval_secs, *args, **kwargs):
279    """Call a function until the function returns expected value or times out.
280
281    Args:
282        func: Function to call.
283        expected_return: The expected return value.
284        timeout_exception: Exception to raise when it hits timeout.
285        timeout_secs: Timeout seconds.
286                      If 0 or less than zero, the function will run once and
287                      we will not wait on it.
288        sleep_interval_secs: Time to sleep between two attemps.
289        *args: list of args to pass to func.
290        **kwargs: dictionary of keyword based args to pass to func.
291
292    Raises:
293        timeout_exception: if the run of function times out.
294    """
295    # TODO(fdeng): Currently this method does not kill
296    # |func|, if |func| takes longer than |timeout_secs|.
297    # We can use a more robust version from chromite.
298    start = time.time()
299    while True:
300        return_value = func(*args, **kwargs)
301        if return_value == expected_return:
302            return
303        if time.time() - start > timeout_secs:
304            raise timeout_exception
305        if sleep_interval_secs > 0:
306            time.sleep(sleep_interval_secs)
307
308
309def GenerateUniqueName(prefix=None, suffix=None):
310    """Generate a random unique name using uuid4.
311
312    Args:
313        prefix: String, desired prefix to prepend to the generated name.
314        suffix: String, desired suffix to append to the generated name.
315
316    Returns:
317        String, a random name.
318    """
319    name = uuid.uuid4().hex
320    if prefix:
321        name = "-".join([prefix, name])
322    if suffix:
323        name = "-".join([name, suffix])
324    return name
325
326
327def MakeTarFile(src_dict, dest):
328    """Archive files in tar.gz format to a file named as |dest|.
329
330    Args:
331        src_dict: A dictionary that maps a path to be archived
332                  to the corresponding name that appears in the archive.
333        dest: String, path to output file, e.g. /tmp/myfile.tar.gz
334    """
335    logger.info("Compressing %s into %s.", src_dict.keys(), dest)
336    with tarfile.open(dest, "w:gz") as tar:
337        for src, arcname in src_dict.items():
338            tar.add(src, arcname=arcname)
339
340def CreateSshKeyPairIfNotExist(private_key_path, public_key_path):
341    """Create the ssh key pair if they don't exist.
342
343    Case1. If the private key doesn't exist, we will create both the public key
344           and the private key.
345    Case2. If the private key exists but public key doesn't, we will create the
346           public key by using the private key.
347    Case3. If the public key exists but the private key doesn't, we will create
348           a new private key and overwrite the public key.
349
350    Args:
351        private_key_path: Path to the private key file.
352                          e.g. ~/.ssh/acloud_rsa
353        public_key_path: Path to the public key file.
354                         e.g. ~/.ssh/acloud_rsa.pub
355
356    Raises:
357        error.DriverError: If failed to create the key pair.
358    """
359    public_key_path = os.path.expanduser(public_key_path)
360    private_key_path = os.path.expanduser(private_key_path)
361    public_key_exist = os.path.exists(public_key_path)
362    private_key_exist = os.path.exists(private_key_path)
363    if public_key_exist and private_key_exist:
364        logger.debug(
365            "The ssh private key (%s) and public key (%s) already exist,"
366            "will not automatically create the key pairs.", private_key_path,
367            public_key_path)
368        return
369    key_folder = os.path.dirname(private_key_path)
370    if not os.path.exists(key_folder):
371        os.makedirs(key_folder)
372    try:
373        if private_key_exist:
374            cmd = SSH_KEYGEN_PUB_CMD + ["-f", private_key_path]
375            with open(public_key_path, 'w') as outfile:
376                stream_content = CheckOutput(cmd)
377                outfile.write(
378                    stream_content.rstrip('\n') + " " + getpass.getuser())
379            logger.info(
380                "The ssh public key (%s) do not exist, "
381                "automatically creating public key, calling: %s",
382                public_key_path, " ".join(cmd))
383        else:
384            cmd = SSH_KEYGEN_CMD + [
385                "-C", getpass.getuser(), "-f", private_key_path
386            ]
387            logger.info(
388                "Creating public key from private key (%s) via cmd: %s",
389                private_key_path, " ".join(cmd))
390            subprocess.check_call(cmd, stdout=sys.stderr, stderr=sys.stdout)
391    except subprocess.CalledProcessError as e:
392        raise errors.DriverError("Failed to create ssh key pair: %s" % str(e))
393    except OSError as e:
394        raise errors.DriverError(
395            "Failed to create ssh key pair, please make sure "
396            "'ssh-keygen' is installed: %s" % str(e))
397
398    # By default ssh-keygen will create a public key file
399    # by append .pub to the private key file name. Rename it
400    # to what's requested by public_key_path.
401    default_pub_key_path = "%s.pub" % private_key_path
402    try:
403        if default_pub_key_path != public_key_path:
404            os.rename(default_pub_key_path, public_key_path)
405    except OSError as e:
406        raise errors.DriverError(
407            "Failed to rename %s to %s: %s" % (default_pub_key_path,
408                                               public_key_path, str(e)))
409
410    logger.info("Created ssh private key (%s) and public key (%s)",
411                private_key_path, public_key_path)
412
413
414def VerifyRsaPubKey(rsa):
415    """Verify the format of rsa public key.
416
417    Args:
418        rsa: content of rsa public key. It should follow the format of
419             ssh-rsa AAAAB3NzaC1yc2EA.... [email protected]
420
421    Raises:
422        DriverError if the format is not correct.
423    """
424    if not rsa or not all(ord(c) < 128 for c in rsa):
425        raise errors.DriverError(
426            "rsa key is empty or contains non-ascii character: %s" % rsa)
427
428    elements = rsa.split()
429    if len(elements) != 3:
430        raise errors.DriverError("rsa key is invalid, wrong format: %s" % rsa)
431
432    key_type, data, _ = elements
433    try:
434        binary_data = base64.decodebytes(data.encode())
435        # number of bytes of int type
436        int_length = 4
437        # binary_data is like "7ssh-key..." in a binary format.
438        # The first 4 bytes should represent 7, which should be
439        # the length of the following string "ssh-key".
440        # And the next 7 bytes should be string "ssh-key".
441        # We will verify that the rsa conforms to this format.
442        # ">I" in the following line means "big-endian unsigned integer".
443        type_length = struct.unpack(">I", binary_data[:int_length])[0]
444        if binary_data[int_length:int_length + type_length] != key_type.encode():
445            raise errors.DriverError("rsa key is invalid: %s" % rsa)
446    except (struct.error, binascii.Error) as e:
447        raise errors.DriverError(
448            "rsa key is invalid: %s, error: %s" % (rsa, str(e)))
449
450
451def Decompress(sourcefile, dest=None):
452    """Decompress .zip or .tar.gz.
453
454    Args:
455        sourcefile: A string, a source file path to decompress.
456        dest: A string, a folder path as decompress destination.
457
458    Raises:
459        errors.UnsupportedCompressionFileType: Not supported extension.
460    """
461    logger.info("Start to decompress %s!", sourcefile)
462    dest_path = dest if dest else "."
463    if sourcefile.endswith(".tar.gz"):
464        with tarfile.open(sourcefile, "r:gz") as compressor:
465            compressor.extractall(dest_path)
466    elif sourcefile.endswith(".zip"):
467        with zipfile.ZipFile(sourcefile, 'r') as compressor:
468            compressor.extractall(dest_path)
469    else:
470        raise errors.UnsupportedCompressionFileType(
471            "Sorry, we could only support compression file type "
472            "for zip or tar.gz.")
473
474
475# pylint: disable=no-init
476class TextColors:
477    """A class that defines common color ANSI code."""
478
479    HEADER = "\033[95m"
480    OKBLUE = "\033[94m"
481    OKGREEN = "\033[92m"
482    WARNING = "\033[33m"
483    FAIL = "\033[91m"
484    ENDC = "\033[0m"
485    BOLD = "\033[1m"
486    UNDERLINE = "\033[4m"
487
488
489def PrintColorString(message, colors=TextColors.OKBLUE, **kwargs):
490    """A helper function to print out colored text.
491
492    Use print function "print(message, end="")" to show message in one line.
493    Example code:
494        DisplayMessages("Creating GCE instance...", end="")
495        # Job execute 20s
496        DisplayMessages("Done! (20s)")
497    Display:
498        Creating GCE instance...
499        # After job finished, messages update as following:
500        Creating GCE instance...Done! (20s)
501
502    Args:
503        message: String, the message text.
504        colors: String, color code.
505        **kwargs: dictionary of keyword based args to pass to func.
506    """
507    print(colors + message + TextColors.ENDC, **kwargs)
508    sys.stdout.flush()
509
510
511def InteractWithQuestion(question, colors=TextColors.WARNING):
512    """A helper function to define the common way to run interactive cmd.
513
514    Args:
515        question: String, the question to ask user.
516        colors: String, color code.
517
518    Returns:
519        String, input from user.
520    """
521    return str(input(colors + question + TextColors.ENDC).strip())
522
523
524def GetUserAnswerYes(question):
525    """Ask user about acloud setup question.
526
527    Args:
528        question: String of question for user. Enter is equivalent to pressing
529                  n. We should hint user with upper case N surrounded in square
530                  brackets.
531                  Ex: "Are you sure to change bucket name[y/N]:"
532
533    Returns:
534        Boolean, True if answer is "Yes", False otherwise.
535    """
536    answer = InteractWithQuestion(question)
537    return answer.lower() in constants.USER_ANSWER_YES
538
539
540class BatchHttpRequestExecutor:
541    """A helper class that executes requests in batch with retry.
542
543    This executor executes http requests in a batch and retry
544    those that have failed. It iteratively updates the dictionary
545    self._final_results with latest results, which can be retrieved
546    via GetResults.
547    """
548
549    def __init__(self,
550                 execute_once_functor,
551                 requests,
552                 retry_http_codes=None,
553                 max_retry=None,
554                 sleep=None,
555                 backoff_factor=None,
556                 other_retriable_errors=None):
557        """Initializes the executor.
558
559        Args:
560            execute_once_functor: A function that execute requests in batch once.
561                                  It should return a dictionary like
562                                  {request_id: (response, exception)}
563            requests: A dictionary where key is request id picked by caller,
564                      and value is a apiclient.http.HttpRequest.
565            retry_http_codes: A list of http codes to retry.
566            max_retry: See utils.Retry.
567            sleep: See utils.Retry.
568            backoff_factor: See utils.Retry.
569            other_retriable_errors: A tuple of error types that should be retried
570                                    other than errors.HttpError.
571        """
572        self._execute_once_functor = execute_once_functor
573        self._requests = requests
574        # A dictionary that maps request id to pending request.
575        self._pending_requests = {}
576        # A dictionary that maps request id to a tuple (response, exception).
577        self._final_results = {}
578        self._retry_http_codes = retry_http_codes
579        self._max_retry = max_retry
580        self._sleep = sleep
581        self._backoff_factor = backoff_factor
582        self._other_retriable_errors = other_retriable_errors
583
584    def _ShoudRetry(self, exception):
585        """Check if an exception is retriable.
586
587        Args:
588            exception: An exception instance.
589        """
590        if isinstance(exception, self._other_retriable_errors):
591            return True
592
593        if (isinstance(exception, errors.HttpError)
594                and exception.code in self._retry_http_codes):
595            return True
596        return False
597
598    def _ExecuteOnce(self):
599        """Executes pending requests and update it with failed, retriable ones.
600
601        Raises:
602            HasRetriableRequestsError: if some requests fail and are retriable.
603        """
604        results = self._execute_once_functor(self._pending_requests)
605        # Update final_results with latest results.
606        self._final_results.update(results)
607        # Clear pending_requests
608        self._pending_requests.clear()
609        for request_id, result in results.items():
610            exception = result[1]
611            if exception is not None and self._ShoudRetry(exception):
612                # If this is a retriable exception, put it in pending_requests
613                self._pending_requests[request_id] = self._requests[request_id]
614        if self._pending_requests:
615            # If there is still retriable requests pending, raise an error
616            # so that Retry will retry this function with pending_requests.
617            raise errors.HasRetriableRequestsError(
618                "Retriable errors: %s" %
619                [str(results[rid][1]) for rid in self._pending_requests])
620
621    def Execute(self):
622        """Executes the requests and retry if necessary.
623
624        Will populate self._final_results.
625        """
626
627        def _ShouldRetryHandler(exc):
628            """Check if |exc| is a retriable exception.
629
630            Args:
631                exc: An exception.
632
633            Returns:
634                True if exception is of type HasRetriableRequestsError; False otherwise.
635            """
636            should_retry = isinstance(exc, errors.HasRetriableRequestsError)
637            if should_retry:
638                logger.info("Will retry failed requests.", exc_info=True)
639                logger.info("%s", exc)
640            return should_retry
641
642        try:
643            self._pending_requests = self._requests.copy()
644            Retry(
645                _ShouldRetryHandler,
646                max_retries=self._max_retry,
647                functor=self._ExecuteOnce,
648                sleep_multiplier=self._sleep,
649                retry_backoff_factor=self._backoff_factor)
650        except errors.HasRetriableRequestsError:
651            logger.debug("Some requests did not succeed after retry.")
652
653    def GetResults(self):
654        """Returns final results.
655
656        Returns:
657            results, a dictionary in the following format
658            {request_id: (response, exception)}
659            request_ids are those from requests; response
660            is the http response for the request or None on error;
661            exception is an instance of DriverError or None if no error.
662        """
663        return self._final_results
664
665
666def DefaultEvaluator(result):
667    """Default Evaluator always return result is ok.
668
669    Args:
670        result:the return value of the target function.
671
672    Returns:
673        _EvaluatedResults namedtuple.
674    """
675    return _EvaluatedResult(is_result_ok=True, result_message=result)
676
677
678def ReportEvaluator(report):
679    """Evalute the acloud operation by the report.
680
681    Args:
682        report: acloud.public.report() object.
683
684    Returns:
685        _EvaluatedResults namedtuple.
686    """
687    if report is None or report.errors:
688        return _EvaluatedResult(is_result_ok=False,
689                                result_message=report.errors)
690
691    return _EvaluatedResult(is_result_ok=True, result_message=None)
692
693
694def BootEvaluator(boot_dict):
695    """Evaluate if the device booted successfully.
696
697    Args:
698        boot_dict: Dict of instance_name:boot error.
699
700    Returns:
701        _EvaluatedResults namedtuple.
702    """
703    if boot_dict:
704        return _EvaluatedResult(is_result_ok=False, result_message=boot_dict)
705    return _EvaluatedResult(is_result_ok=True, result_message=None)
706
707
708class TimeExecute:
709    """Count the function execute time."""
710
711    def __init__(self, function_description=None, print_before_call=True,
712                 print_status=True, result_evaluator=DefaultEvaluator,
713                 display_waiting_dots=True):
714        """Initializes the class.
715
716        Args:
717            function_description: String that describes function (e.g."Creating
718                                  Instance...")
719            print_before_call: Boolean, print the function description before
720                               calling the function, default True.
721            print_status: Boolean, print the status of the function after the
722                          function has completed, default True ("OK" or "Fail").
723            result_evaluator: Func object. Pass func to evaluate result.
724                              Default evaluator always report result is ok and
725                              failed result will be identified only in exception
726                              case.
727            display_waiting_dots: Boolean, if true print the function_description
728                                  followed by waiting dot.
729        """
730        self._function_description = function_description
731        self._print_before_call = print_before_call
732        self._print_status = print_status
733        self._result_evaluator = result_evaluator
734        self._display_waiting_dots = display_waiting_dots
735
736    def __call__(self, func):
737        def DecoratorFunction(*args, **kargs):
738            """Decorator function.
739
740            Args:
741                *args: Arguments to pass to the functor.
742                **kwargs: Key-val based arguments to pass to the functor.
743
744            Raises:
745                Exception: The exception that functor(*args, **kwargs) throws.
746            """
747            timestart = time.time()
748            if self._print_before_call:
749                waiting_dots = "..." if self._display_waiting_dots else ""
750                PrintColorString("%s %s"% (self._function_description,
751                                           waiting_dots), end="")
752            try:
753                result = func(*args, **kargs)
754                result_time = time.time() - timestart
755                if not self._print_before_call:
756                    PrintColorString("%s (%ds)" % (self._function_description,
757                                                   result_time),
758                                     TextColors.OKGREEN)
759                if self._print_status:
760                    evaluated_result = self._result_evaluator(result)
761                    if evaluated_result.is_result_ok:
762                        PrintColorString("OK! (%ds)" % (result_time),
763                                         TextColors.OKGREEN)
764                    else:
765                        PrintColorString("Fail! (%ds)" % (result_time),
766                                         TextColors.FAIL)
767                        PrintColorString("Error: %s" %
768                                         evaluated_result.result_message,
769                                         TextColors.FAIL)
770                return result
771            except:
772                if self._print_status:
773                    PrintColorString("Fail! (%ds)" % (time.time() - timestart),
774                                     TextColors.FAIL)
775                raise
776        return DecoratorFunction
777
778
779def PickFreePort():
780    """Helper to pick a free port.
781
782    Returns:
783        Integer, a free port number.
784    """
785    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
786    tcp_socket.bind(("", 0))
787    port = tcp_socket.getsockname()[1]
788    tcp_socket.close()
789    return port
790
791
792def CheckPortFree(port):
793    """Check the availablity of the tcp port.
794
795    Args:
796        Integer, a port number.
797
798    Raises:
799        PortOccupied: This port is not available.
800    """
801    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
802    try:
803        tcp_socket.bind(("", port))
804    except socket.error as port_error:
805        raise errors.PortOccupied("Port (%d) is taken, please choose another "
806                                  "port." % port) from port_error
807    tcp_socket.close()
808
809
810def _ExecuteCommand(cmd, args):
811    """Execute command.
812
813    Args:
814        cmd: Strings of execute binary name.
815        args: List of args to pass in with cmd.
816
817    Raises:
818        errors.NoExecuteBin: Can't find the execute bin file.
819    """
820    bin_path = FindExecutable(cmd)
821    if not bin_path:
822        raise errors.NoExecuteCmd("unable to locate %s" % cmd)
823    command = [bin_path] + args
824    logger.debug("Running '%s'", ' '.join(command))
825    with open(os.devnull, "w") as dev_null:
826        subprocess.check_call(command, stderr=dev_null, stdout=dev_null)
827
828
829def ReleasePort(port):
830    """Release local port.
831
832    Args:
833        port: Integer of local port number.
834    """
835    try:
836        with open(os.devnull, "w") as dev_null:
837            subprocess.check_call(_RELEASE_PORT_CMD % port,
838                                  stderr=dev_null, stdout=dev_null, shell=True)
839    except subprocess.CalledProcessError:
840        logger.debug("The port %d is available.", constants.WEBRTC_LOCAL_PORT)
841
842
843def EstablishSshTunnel(ip_addr, rsa_key_file, ssh_user,
844                       port_mapping, extra_args_ssh_tunnel=None):
845    """Create an ssh tunnel.
846
847    Args:
848        ip_addr: String, use to build the adb & vnc tunnel between local
849                 and remote instance.
850        rsa_key_file: String, Private key file path to use when creating
851                      the ssh tunnels.
852        ssh_user: String of user login into the instance.
853        port_mapping: List of tuples, each tuple is a pair of integers
854                      representing a local port and a remote port.
855        extra_args_ssh_tunnel: String, extra args for ssh tunnel connection.
856
857    Raises:
858        subprocess.CalledProcessError if the ssh command fails.
859    """
860    port_mapping = [PORT_MAPPING % {
861        "local_port": ports[0],
862        "target_port": ports[1]} for ports in port_mapping]
863    ssh_tunnel_args = _SSH_TUNNEL_ARGS % {
864        "rsa_key_file": rsa_key_file,
865        "ssh_user": ssh_user,
866        "ip_addr": ip_addr,
867        "port_mapping": " ".join(port_mapping)}
868    ssh_tunnel_args_list = shlex.split(ssh_tunnel_args)
869    if extra_args_ssh_tunnel:
870        ssh_tunnel_args_list.extend(shlex.split(extra_args_ssh_tunnel))
871    _ExecuteCommand(constants.SSH_BIN, ssh_tunnel_args_list)
872
873
874def EstablishWebRTCSshTunnel(ip_addr, webrtc_local_port, rsa_key_file, ssh_user,
875                             extra_args_ssh_tunnel=None):
876    """Create ssh tunnels for webrtc.
877
878    Pick up an available local port to establish one WebRTC tunnel and forward to
879    the port of the webrtc operator of the remote instance.
880
881    Args:
882        ip_addr: String, use to build the adb & vnc tunnel between local
883                 and remote instance.
884        webrtc_local_port: Integer, pick a free port as webrtc local port.
885        rsa_key_file: String, Private key file path to use when creating
886                      the ssh tunnels.
887        ssh_user: String of user login into the instance.
888        extra_args_ssh_tunnel: String, extra args for ssh tunnel connection.
889
890    Raises:
891        subprocess.CalledProcessError if the ssh command fails.
892    """
893    webrtc_server_port = GetWebRTCServerPort(
894        ip_addr, rsa_key_file, ssh_user, extra_args_ssh_tunnel)
895
896    # TODO(b/209502647): design a better way to forward webrtc ports.
897    if extra_args_ssh_tunnel:
898        for webrtc_port in WEBRTC_PORTS_MAPPING:
899            ReleasePort(webrtc_port.local)
900    port_mapping = (WEBRTC_PORTS_MAPPING +
901                    [PortMapping(webrtc_local_port, webrtc_server_port)])
902    try:
903        EstablishSshTunnel(ip_addr, rsa_key_file, ssh_user,
904                           port_mapping, extra_args_ssh_tunnel)
905    except subprocess.CalledProcessError as e:
906        PrintColorString("\n%s\nFailed to create ssh tunnels, retry with '#acloud "
907                         "reconnect'." % e, TextColors.FAIL)
908
909
910def GetWebRTCServerPort(ip_addr, rsa_key_file, ssh_user,
911                        extra_args_ssh_tunnel=None):
912    """Get WebRTC server port.
913
914    List all process information to find the "webrtc_operator" process, then
915    determine the WebRTC server port is 8443 or 1443.
916
917    Args:
918        ip_addr: String, use to build the adb & vnc tunnel between local
919                 and remote instance.
920        rsa_key_file: String, Private key file path to use when creating
921                      the ssh tunnels.
922        ssh_user: String of user login into the instance.
923        extra_args_ssh_tunnel: String, extra args for ssh tunnel connection.
924
925    Returns:
926        The WebRTC server port number.
927
928    Raises:
929        subprocess.CalledProcessError if the ssh command fails.
930    """
931    ssh_cmd = _SSH_COMMAND_PS % {
932        "ssh_bin": FindExecutable(constants.SSH_BIN),
933        "rsa_key_file": rsa_key_file,
934        "ssh_user": ssh_user,
935        "extra_args": extra_args_ssh_tunnel or "",
936        "ip_addr": ip_addr}
937    logger.info("Running command \"%s\"", ssh_cmd)
938    try:
939        process = subprocess.Popen(
940            ssh_cmd, shell=True, stdin=None, universal_newlines=True,
941            stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
942        stdout, _ = process.communicate()
943        for line in stdout.splitlines():
944            webrtc_match = _WEBRTC_OPERATOR_PATTERN.match(line)
945            if webrtc_match:
946                return _PORT_8443
947    except subprocess.CalledProcessError as e:
948        logger.debug("Failed to list processes: %s", e)
949    return _PORT_1443
950
951
952def GetWebrtcPortFromSSHTunnel(ip):
953    """Get forwarding webrtc port from ssh tunnel.
954
955    Args:
956        ip: String, ip address.
957
958    Returns:
959        webrtc local port.
960    """
961    re_pattern = re.compile(_RE_WEBRTC_SSH_TUNNEL_PATTERN %
962                            (constants.WEBRTC_LOCAL_PORT, ip))
963    process_output = CheckOutput(constants.COMMAND_PS)
964    for line in process_output.splitlines():
965        match = re_pattern.match(line)
966        if match:
967            webrtc_port = int(match.group(_RE_GROUP_WEBRTC))
968            return webrtc_port
969
970    logger.debug("Can't get webrtc local port from ip %s.", ip)
971    return None
972
973
974# TODO(147337696): create ssh tunnels tear down as adb and vnc.
975# pylint: disable=too-many-locals
976def AutoConnect(ip_addr, rsa_key_file, target_vnc_port, target_adb_port,
977                ssh_user, client_adb_port=None, extra_args_ssh_tunnel=None):
978    """Autoconnect to an AVD instance.
979
980    Args:
981        ip_addr: String, use to build the adb & vnc tunnel between local
982                 and remote instance.
983        rsa_key_file: String, Private key file path to use when creating
984                      the ssh tunnels.
985        target_vnc_port: Integer of target vnc port number.
986        target_adb_port: Integer of target adb port number.
987        ssh_user: String of user login into the instance.
988        client_adb_port: Integer, Specified adb port to establish connection.
989        extra_args_ssh_tunnel: String, extra args for ssh tunnel connection.
990
991    Returns:
992        NamedTuple of (vnc_port, adb_port) SSHTUNNEL of the connect, both are
993        integers.
994    """
995    local_adb_port = client_adb_port or PickFreePort()
996    port_mapping = [(local_adb_port, target_adb_port)]
997    local_free_vnc_port = None
998    if target_vnc_port:
999        local_free_vnc_port = PickFreePort()
1000        port_mapping.append((local_free_vnc_port, target_vnc_port))
1001    try:
1002        EstablishSshTunnel(ip_addr, rsa_key_file, ssh_user,
1003                           port_mapping, extra_args_ssh_tunnel)
1004    except subprocess.CalledProcessError as e:
1005        PrintColorString("\n%s\nFailed to create ssh tunnels, retry with '#acloud "
1006                         "reconnect'." % e, TextColors.FAIL)
1007        return ForwardedPorts(vnc_port=None, adb_port=None)
1008
1009    try:
1010        adb_connect_args = _ADB_CONNECT_ARGS % {"adb_port": local_adb_port}
1011        _ExecuteCommand(constants.ADB_BIN, adb_connect_args.split())
1012    except subprocess.CalledProcessError:
1013        PrintColorString("Failed to adb connect, retry with "
1014                         "'#acloud reconnect'", TextColors.FAIL)
1015
1016    return ForwardedPorts(vnc_port=local_free_vnc_port,
1017                          adb_port=local_adb_port)
1018
1019
1020def FindRemoteFiles(ssh_obj, search_dirs):
1021    """Get all files, except symbolic links, under remote directories.
1022
1023    Args:
1024        ssh_obj: An Ssh object.
1025        search_dirs: A list of strings, the remote directories.
1026
1027    Returns:
1028        A list of strings, the file paths.
1029
1030    Raises:
1031        errors.SubprocessFail if the ssh execution returns non-zero.
1032    """
1033    if not search_dirs:
1034        return []
1035    ssh_cmd = (ssh_obj.GetBaseCmd(constants.SSH_BIN) + " " +
1036               _CMD_FIND_FILES % {"search_dirs": " ".join(search_dirs)})
1037    proc = subprocess.run(ssh_cmd, shell=True, capture_output=True,
1038                          check=False)
1039    if proc.returncode != 0:
1040        raise errors.SubprocessFail("`%s` returned %d. with standard error: %s" %
1041                                    (ssh_cmd, proc.returncode, proc.stderr.decode()))
1042    if proc.stderr:
1043        logger.debug("`%s` stderr: %s", ssh_cmd, proc.stderr.decode())
1044    if proc.stdout:
1045        return proc.stdout.decode().splitlines()
1046    return []
1047
1048
1049def GetAnswerFromList(answer_list, enable_choose_all=False):
1050    """Get answer from a list.
1051
1052    Args:
1053        answer_list: list of the answers to choose from.
1054        enable_choose_all: True to choose all items from answer list.
1055
1056    Return:
1057        List holding the answer(s).
1058    """
1059    print("[0] to exit.")
1060    start_index = 1
1061    max_choice = len(answer_list)
1062
1063    for num, item in enumerate(answer_list, start_index):
1064        print("[%d] %s" % (num, item))
1065    if enable_choose_all:
1066        max_choice += 1
1067        print("[%d] for all." % max_choice)
1068
1069    choice = -1
1070
1071    while True:
1072        try:
1073            choice = input("Enter your choice[0-%d]: " % max_choice)
1074            choice = int(choice)
1075        except ValueError:
1076            print("'%s' is not a valid integer.", choice)
1077            continue
1078        # Filter out choices
1079        if choice == 0:
1080            sys.exit(constants.EXIT_BY_USER)
1081        if enable_choose_all and choice == max_choice:
1082            return answer_list
1083        if choice < 0 or choice > max_choice:
1084            print("please choose between 0 and %d" % max_choice)
1085        else:
1086            return [answer_list[choice-start_index]]
1087
1088
1089def LaunchVNCFromReport(report, avd_spec, no_prompts=False):
1090    """Launch vnc client according to the instances report.
1091
1092    Args:
1093        report: Report object, that stores and generates report.
1094        avd_spec: AVDSpec object that tells us what we're going to create.
1095        no_prompts: Boolean, True to skip all prompts.
1096    """
1097    for device in report.data.get("devices", []):
1098        if device.get(constants.VNC_PORT):
1099            LaunchVncClient(device.get(constants.VNC_PORT),
1100                            avd_width=avd_spec.hw_property["x_res"],
1101                            avd_height=avd_spec.hw_property["y_res"],
1102                            no_prompts=no_prompts)
1103        else:
1104            PrintColorString("No VNC port specified, skipping VNC startup.",
1105                             TextColors.FAIL)
1106
1107
1108def LaunchBrowserFromReport(report):
1109    """Open browser when autoconnect to webrtc according to the instances report.
1110
1111    Args:
1112        report: Report object, that stores and generates report.
1113    """
1114    for device in report.data.get("devices", []):
1115        if device.get("ip"):
1116            LaunchBrowser(constants.WEBRTC_LOCAL_HOST,
1117                          device.get(constants.WEBRTC_PORT,
1118                                     constants.WEBRTC_LOCAL_PORT))
1119
1120
1121def LaunchBrowser(ip_addr, port):
1122    """Launch browser to connect the webrtc AVD.
1123
1124    Args:
1125        ip_addr: String, use to connect to webrtc AVD on the instance.
1126        port: Integer, port number.
1127    """
1128    webrtc_link = _WEBRTC_URL % {
1129        "webrtc_ip": ip_addr,
1130        "webrtc_port": port}
1131    PrintColorString("WebRTC AVD URL: %s "% webrtc_link)
1132    if os.environ.get(_ENV_DISPLAY, None):
1133        webbrowser.open_new_tab(webrtc_link)
1134    else:
1135        PrintColorString("Remote terminal can't support launch webbrowser.",
1136                         TextColors.FAIL)
1137
1138
1139def LaunchVncClient(port, avd_width=None, avd_height=None, no_prompts=False):
1140    """Launch ssvnc.
1141
1142    Args:
1143        port: Integer, port number.
1144        avd_width: String, the width of avd.
1145        avd_height: String, the height of avd.
1146        no_prompts: Boolean, True to skip all prompts.
1147    """
1148    try:
1149        os.environ[_ENV_DISPLAY]
1150    except KeyError:
1151        PrintColorString("Remote terminal can't support VNC. "
1152                         "Skipping VNC startup. "
1153                         "VNC server is listening at 127.0.0.1:{}.".format(port),
1154                         TextColors.FAIL)
1155        return
1156
1157    if IsSupportedPlatform() and not FindExecutable(_VNC_BIN):
1158        if no_prompts or GetUserAnswerYes(_CONFIRM_CONTINUE):
1159            try:
1160                PrintColorString("Installing ssvnc vnc client... ", end="")
1161                sys.stdout.flush()
1162                CheckOutput(_CMD_INSTALL_SSVNC, shell=True)
1163                PrintColorString("Done", TextColors.OKGREEN)
1164            except subprocess.CalledProcessError as cpe:
1165                PrintColorString("Failed to install ssvnc: %s" %
1166                                 cpe.output, TextColors.FAIL)
1167                return
1168        else:
1169            return
1170    ssvnc_env = os.environ.copy()
1171    ssvnc_env.update(_SSVNC_ENV_VARS)
1172    # Override SSVNC_SCALE
1173    if avd_width or avd_height:
1174        scale_ratio = CalculateVNCScreenRatio(avd_width, avd_height)
1175        ssvnc_env["SSVNC_SCALE"] = str(scale_ratio)
1176        logger.debug("SSVNC_SCALE:%s", scale_ratio)
1177
1178    ssvnc_args = _CMD_START_VNC % {"bin": FindExecutable(_VNC_BIN),
1179                                   "port": port}
1180    subprocess.Popen(ssvnc_args.split(), env=ssvnc_env)
1181
1182
1183def PrintDeviceSummary(report):
1184    """Display summary of devices.
1185
1186    -Display device details from the report instance.
1187        report example:
1188            'data': [{'devices':[{'instance_name': 'ins-f6a397-none-53363',
1189                                  'ip': u'35.234.10.162'}]}]
1190    -Display error message from report.error.
1191
1192    Args:
1193        report: A Report instance.
1194    """
1195    PrintColorString("\n")
1196    PrintColorString("Device summary:")
1197    for device in report.data.get("devices", []):
1198        adb_serial = device.get(constants.DEVICE_SERIAL)
1199        if not adb_serial:
1200            adb_port = device.get("adb_port")
1201            if adb_port:
1202                adb_serial = constants.LOCALHOST_ADB_SERIAL % adb_port
1203            else:
1204                adb_serial = "(None)"
1205
1206        instance_name = device.get("instance_name")
1207        instance_ip = device.get("ip")
1208        instance_details = "" if not instance_name else "(%s[%s])" % (
1209            instance_name, instance_ip)
1210        PrintColorString(f" - device serial: {adb_serial} {instance_details}")
1211        PrintColorString("\n")
1212        PrintColorString("Note: To ensure Tradefed uses this AVD, please run:")
1213        PrintColorString("\texport ANDROID_SERIAL=%s" % adb_serial)
1214        ssh_command = device.get("ssh_command")
1215        if ssh_command:
1216            PrintColorString("\n")
1217            PrintColorString("Note: To ssh connect to the device, please run:")
1218            PrintColorString(f"\tssh command: {ssh_command}")
1219        screen_command = device.get("screen_command")
1220        if screen_command:
1221            PrintColorString("\n")
1222            PrintColorString("Note: To access the console, please run:")
1223            PrintColorString(f"\tscreen command: {screen_command}")
1224
1225    # TODO(b/117245508): Help user to delete instance if it got created.
1226    if report.errors:
1227        error_msg = "\n".join(report.errors)
1228        PrintColorString("Fail in:\n%s\n" % error_msg, TextColors.FAIL)
1229
1230
1231# pylint: disable=import-outside-toplevel
1232def CalculateVNCScreenRatio(avd_width, avd_height):
1233    """calculate the vnc screen scale ratio to fit into user's monitor.
1234
1235    Args:
1236        avd_width: String, the width of avd.
1237        avd_height: String, the height of avd.
1238    Return:
1239        Float, scale ratio for vnc client.
1240    """
1241    try:
1242        import Tkinter
1243    # Some python interpreters may not be configured for Tk, just return default scale ratio.
1244    except ImportError:
1245        try:
1246            import tkinter as Tkinter
1247        except ImportError:
1248            PrintColorString(
1249                "no module named tkinter, vnc display scale were not be fit."
1250                "please run 'sudo apt-get install python3-tk' to install it.")
1251            return _DEFAULT_DISPLAY_SCALE
1252    root = Tkinter.Tk()
1253    margin = 100 # leave some space on user's monitor.
1254    screen_height = root.winfo_screenheight() - margin
1255    screen_width = root.winfo_screenwidth() - margin
1256
1257    scale_h = _DEFAULT_DISPLAY_SCALE
1258    scale_w = _DEFAULT_DISPLAY_SCALE
1259    if float(screen_height) < float(avd_height):
1260        scale_h = round(float(screen_height) / float(avd_height), 1)
1261
1262    if float(screen_width) < float(avd_width):
1263        scale_w = round(float(screen_width) / float(avd_width), 1)
1264
1265    logger.debug("scale_h: %s (screen_h: %s/avd_h: %s),"
1266                 " scale_w: %s (screen_w: %s/avd_w: %s)",
1267                 scale_h, screen_height, avd_height,
1268                 scale_w, screen_width, avd_width)
1269
1270    # Return the larger scale-down ratio.
1271    return scale_h if scale_h < scale_w else scale_w
1272
1273
1274def IsCommandRunning(command):
1275    """Check if command is running.
1276
1277    Args:
1278        command: String of command name.
1279
1280    Returns:
1281        Boolean, True if command is running. False otherwise.
1282    """
1283    try:
1284        with open(os.devnull, "w") as dev_null:
1285            subprocess.check_call([constants.CMD_PGREP, "-af", command],
1286                                  stderr=dev_null, stdout=dev_null)
1287        return True
1288    except subprocess.CalledProcessError:
1289        return False
1290
1291
1292def AddUserGroupsToCmd(cmd, user_groups):
1293    """Add the user groups to the command if necessary.
1294
1295    As part of local host setup to enable local instance support, the user is
1296    added to certain groups. For those settings to take effect systemwide
1297    requires the user to log out and log back in. In the scenario where the
1298    user has run setup and hasn't logged out, we still want them to be able to
1299    launch a local instance so add the user to the groups as part of the
1300    command to ensure success.
1301
1302    The reason using here-doc instead of '&' is all operations need to be ran in
1303    ths same pid.  Here's an example cmd:
1304    $ sg kvm  << EOF
1305    sg libvirt
1306    sg cvdnetwork
1307    launch_cvd --cpus 2 --x_res 1280 --y_res 720 --dpi 160 --memory_mb 4096
1308    EOF
1309
1310    Args:
1311        cmd: String of the command to prepend the user groups to.
1312        user_groups: List of user groups name.(String)
1313
1314    Returns:
1315        String of the command with the user groups prepended to it if necessary,
1316        otherwise the same existing command.
1317    """
1318    user_group_cmd = ""
1319    if not CheckUserInGroups(user_groups):
1320        logger.debug("Need to add user groups to the command")
1321        for idx, group in enumerate(user_groups):
1322            user_group_cmd += _CMD_SG + group
1323            if idx == 0:
1324                user_group_cmd += " <<EOF\n"
1325            else:
1326                user_group_cmd += "\n"
1327        cmd += "\nEOF"
1328    user_group_cmd += cmd
1329    logger.debug("user group cmd: %s", user_group_cmd)
1330    return user_group_cmd
1331
1332
1333def CheckUserInGroups(group_name_list):
1334    """Check if the current user is in the group.
1335
1336    Args:
1337        group_name_list: The list of group name.
1338    Returns:
1339        True if current user is in all the groups.
1340    """
1341    logger.info("Checking if user is in following groups: %s", group_name_list)
1342    all_groups = [g.gr_name for g in grp.getgrall()]
1343    for group in group_name_list:
1344        if group not in all_groups:
1345            logger.info("This group doesn't exist: %s", group)
1346            return False
1347        if getpass.getuser() not in grp.getgrnam(group).gr_mem:
1348            logger.info("Current user isn't in this group: %s", group)
1349            return False
1350    return True
1351
1352
1353def IsSupportedPlatform(print_warning=False):
1354    """Check if user's os is the supported platform.
1355
1356    platform.version() return such as '#1 SMP Debian 5.6.14-1rodete2...'
1357    and use to judge supported or not.
1358
1359    Args:
1360        print_warning: Boolean, print the unsupported warning
1361                       if True.
1362    Returns:
1363        Boolean, True if user is using supported platform.
1364    """
1365    system = platform.system()
1366    # TODO(b/161085678): After python3 fully migrated, then use distro to fix.
1367    platform_supported = False
1368    if system in _SUPPORTED_SYSTEMS_AND_DISTS:
1369        for dist in _SUPPORTED_SYSTEMS_AND_DISTS[system]:
1370            if dist in platform.version():
1371                platform_supported = True
1372                break
1373
1374    logger.info("Updated supported system and dists: %s",
1375                _SUPPORTED_SYSTEMS_AND_DISTS)
1376    platform_supported_msg = ("%s[%s] %s supported platform" %
1377                              (system,
1378                               platform.version(),
1379                               "is a" if platform_supported else "is not a"))
1380    if print_warning and not platform_supported:
1381        PrintColorString(platform_supported_msg, TextColors.WARNING)
1382    else:
1383        logger.info(platform_supported_msg)
1384
1385    return platform_supported
1386
1387def IsSupportedKvm():
1388    """Check if support kvm.
1389
1390    Returns:
1391        True if environment supported kvm.
1392    """
1393    if os.path.exists(_KVM_PATH):
1394        return True
1395
1396    PrintColorString(
1397        "The environment doesn't support virtualization. Please run "
1398        "the remote instance by \"acloud create\" instead. If you want to "
1399        "launch AVD on the local instance, Please refer to http://go/"
1400        "acloud-cloudtop#acloud-create-local-instance-on-the-cloudtop",
1401        TextColors.FAIL)
1402    return False
1403
1404def GetDistDir():
1405    """Return the absolute path to the dist dir."""
1406    android_build_top = os.environ.get(constants.ENV_ANDROID_BUILD_TOP)
1407    if not android_build_top:
1408        return None
1409    dist_cmd = GET_BUILD_VAR_CMD[:]
1410    dist_cmd.append(_DIST_DIR)
1411    try:
1412        dist_dir = CheckOutput(dist_cmd, cwd=android_build_top)
1413    except subprocess.CalledProcessError:
1414        return None
1415    return os.path.join(android_build_top, dist_dir.strip())
1416
1417
1418def CleanupProcess(pattern):
1419    """Cleanup process with pattern.
1420
1421    Args:
1422        pattern: String, string of process pattern.
1423    """
1424    if IsCommandRunning(pattern):
1425        command_kill = _CMD_KILL + [pattern]
1426        subprocess.check_call(command_kill)
1427
1428
1429def TimeoutException(timeout_secs, timeout_error=_DEFAULT_TIMEOUT_ERR):
1430    """Decorater which function timeout setup and raise custom exception.
1431
1432    Args:
1433        timeout_secs: Number of maximum seconds of waiting time.
1434        timeout_error: String to describe timeout exception.
1435
1436    Returns:
1437        The function wrapper.
1438    """
1439    if timeout_error == _DEFAULT_TIMEOUT_ERR:
1440        timeout_error = timeout_error % timeout_secs
1441
1442    def _Wrapper(func):
1443        # pylint: disable=unused-argument
1444        def _HandleTimeout(signum, frame):
1445            raise errors.FunctionTimeoutError(timeout_error)
1446
1447        def _FunctionWrapper(*args, **kwargs):
1448            signal.signal(signal.SIGALRM, _HandleTimeout)
1449            signal.alarm(timeout_secs)
1450            try:
1451                result = func(*args, **kwargs)
1452            finally:
1453                signal.alarm(0)
1454            return result
1455
1456        return _FunctionWrapper
1457
1458    return _Wrapper
1459
1460
1461def GetBuildEnvironmentVariable(variable_name):
1462    """Get build environment variable.
1463
1464    Args:
1465        variable_name: String of variable name.
1466
1467    Returns:
1468        String, the value of the variable.
1469
1470    Raises:
1471        errors.GetAndroidBuildEnvVarError: No environment variable found.
1472    """
1473    try:
1474        return os.environ[variable_name]
1475    except KeyError as no_env_error:
1476        raise errors.GetAndroidBuildEnvVarError(
1477            "Could not get environment var: %s\n"
1478            "Try to run 'source build/envsetup.sh && lunch <target>'"
1479            % variable_name
1480        ) from no_env_error
1481
1482
1483# pylint: disable=no-member,import-outside-toplevel
1484def FindExecutable(filename):
1485    """A compatibility function to find execution file path.
1486
1487    Args:
1488        filename: String of execution filename.
1489
1490    Returns:
1491        String: execution file path.
1492    """
1493    try:
1494        from distutils.spawn import find_executable
1495        return find_executable(filename)
1496    except ImportError:
1497        return shutil.which(filename)
1498
1499
1500def GetDictItems(namedtuple_object):
1501    """A compatibility function to access the OrdereDict object from the given namedtuple object.
1502
1503    Args:
1504        namedtuple_object: namedtuple object.
1505
1506    Returns:
1507        collections.namedtuple._asdict().items() when using python3.
1508    """
1509    return namedtuple_object._asdict().items()
1510
1511
1512def CleanupSSVncviewer(vnc_port):
1513    """Cleanup the old disconnected ssvnc viewer.
1514
1515    Args:
1516        vnc_port: Integer, port number of vnc.
1517    """
1518    ssvnc_viewer_pattern = _SSVNC_VIEWER_PATTERN % {"vnc_port":vnc_port}
1519    CleanupProcess(ssvnc_viewer_pattern)
1520
1521
1522def CheckOutput(cmd, **kwargs):
1523    """Call subprocess.check_output to get output.
1524
1525    The subprocess.check_output return type is "bytes" in python 3, we have
1526    to convert bytes as string with .decode() in advance.
1527
1528    Args:
1529        cmd: String of command.
1530        **kwargs: dictionary of keyword based args to pass to func.
1531
1532    Return:
1533        String to command output.
1534    """
1535    return subprocess.check_output(cmd, **kwargs).decode()
1536
1537
1538def Popen(*command, **popen_args):
1539    """Execute subprocess.Popen command and log the output.
1540
1541    This method waits for the process to terminate. It kills the process
1542    if it's interrupted due to timeout.
1543
1544    Args:
1545        command: Strings, the command.
1546        popen_kwargs: The arguments to be passed to subprocess.Popen.
1547
1548    Raises:
1549        errors.SubprocessFail if the process returns non-zero.
1550    """
1551    proc = None
1552    try:
1553        logger.info("Execute %s", command)
1554        popen_args["stdin"] = subprocess.PIPE
1555        popen_args["stdout"] = subprocess.PIPE
1556        popen_args["stderr"] = subprocess.PIPE
1557
1558        # Some OTA tools are Python scripts in different versions. The
1559        # PYTHONPATH for acloud may be incompatible with the tools.
1560        if "env" not in popen_args and "PYTHONPATH" in os.environ:
1561            popen_env = os.environ.copy()
1562            del popen_env["PYTHONPATH"]
1563            popen_args["env"] = popen_env
1564
1565        proc = subprocess.Popen(command, **popen_args)
1566        stdout, stderr = proc.communicate()
1567        logger.info("%s stdout: %s", command[0], stdout)
1568        logger.info("%s stderr: %s", command[0], stderr)
1569
1570        if proc.returncode != 0:
1571            raise errors.SubprocessFail("%s returned %d." %
1572                                        (command[0], proc.returncode))
1573    finally:
1574        if proc and proc.poll() is None:
1575            logger.info("Kill %s", command[0])
1576            proc.kill()
1577
1578
1579def SetExecutable(path):
1580    """Grant the persmission to execute a file.
1581
1582    Args:
1583        path: String, the file path.
1584
1585    Raises:
1586        OSError if any file operation fails.
1587    """
1588    mode = os.stat(path).st_mode
1589    os.chmod(path, mode | (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH |
1590                           stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH))
1591
1592
1593def SetDirectoryTreeExecutable(dir_path):
1594    """Grant the permission to execute all files in a directory.
1595
1596    Args:
1597        dir_path: String, the directory path.
1598
1599    Raises:
1600        OSError if any file operation fails.
1601    """
1602    for parent_dir, _, file_names in os.walk(dir_path):
1603        for name in file_names:
1604            SetExecutable(os.path.join(parent_dir, name))
1605
1606
1607def GetCvdPorts():
1608    """Get CVD ports
1609
1610
1611    Returns:
1612        ForwardedPorts: vnc port and adb port.
1613    """
1614    return AVD_PORT_DICT[constants.TYPE_CF]
1615
1616
1617def SetCvdPorts(base_instance_num):
1618    """Adjust ports by base_instance_num.
1619
1620    Args:
1621        base_instance_num: int, cuttlefish base_instance_num.
1622    """
1623    offset = (base_instance_num or 1) - 1
1624    AVD_PORT_DICT[constants.TYPE_CF] = ForwardedPorts(
1625        constants.CF_VNC_PORT + offset, constants.CF_ADB_PORT + offset)
1626
1627    # TODO: adjust WebRTC ports
1628