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