1#!/usr/bin/env python
2#
3# Copyright 2018 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""Common code used by acloud setup tools."""
17
18from __future__ import print_function
19import logging
20import re
21import subprocess
22
23from acloud import errors
24
25
26logger = logging.getLogger(__name__)
27
28PKG_INSTALL_CMD = "sudo apt-get --assume-yes install %s"
29APT_CHECK_CMD = "LANG=en_US.UTF-8 apt-cache policy %s"
30_INSTALLED_RE = re.compile(r"(.*\s*Installed:)(?P<installed_ver>.*\s?)")
31_CANDIDATE_RE = re.compile(r"(.*\s*Candidate:)(?P<candidate_ver>.*\s?)")
32
33
34def CheckCmdOutput(cmd, print_cmd=True, **kwargs):
35    """Helper function to run subprocess.check_output.
36
37    This function will return the command output for parsing the result and will
38    raise Error if command return code was non-zero.
39
40    Args:
41        cmd: String, the cmd string.
42        print_cmd: True to print cmd to stdout.
43        kwargs: Other option args to subprocess.
44
45    Returns:
46        Return cmd output as a byte string.
47        If the return code was non-zero it raises a CalledProcessError.
48    """
49    if print_cmd:
50        print("Run command: %s" % cmd)
51
52    logger.debug("Run command: %s", cmd)
53    return subprocess.check_output(cmd, **kwargs)
54
55
56def InstallPackage(pkg):
57    """Install package.
58
59    Args:
60        pkg: String, the name of package.
61
62    Raises:
63        PackageInstallError: package is not installed.
64    """
65    try:
66        print(CheckCmdOutput(PKG_INSTALL_CMD % pkg,
67                             shell=True,
68                             stderr=subprocess.STDOUT))
69    except subprocess.CalledProcessError as cpe:
70        logger.error("Package install for %s failed: %s", pkg, cpe.output)
71        raise errors.PackageInstallError(
72            "Could not install package [" + pkg + "], :" + str(cpe.output))
73
74    if not PackageInstalled(pkg, compare_version=False):
75        raise errors.PackageInstallError(
76            "Package was not detected as installed after installation [" +
77            pkg + "]")
78
79
80def PackageInstalled(pkg_name, compare_version=True):
81    """Check if the package is installed or not.
82
83    This method will validate that the specified package is installed
84    (via apt cache policy) and check if the installed version is up-to-date.
85
86    Args:
87        pkg_name: String, the package name.
88        compare_version: Boolean, True to compare version.
89
90    Returns:
91        True if package is installed.and False if not installed or
92        the pre-installed package is not the same version as the repo candidate
93        version.
94
95    Raises:
96        UnableToLocatePkgOnRepositoryError: Unable to locate package on repository.
97    """
98    try:
99        pkg_info = CheckCmdOutput(
100            APT_CHECK_CMD % pkg_name,
101            print_cmd=False,
102            shell=True,
103            stderr=subprocess.STDOUT)
104
105        logger.debug("Check package install status")
106        logger.debug(pkg_info)
107    except subprocess.CalledProcessError as error:
108        # Unable locate package name on repository.
109        raise errors.UnableToLocatePkgOnRepositoryError(
110            "Could not find package [" + pkg_name + "] on repository, :" +
111            str(error.output) + ", have you forgotten to run 'apt update'?")
112
113    installed_ver = None
114    candidate_ver = None
115    for line in pkg_info.splitlines():
116        match = _INSTALLED_RE.match(line)
117        if match:
118            installed_ver = match.group("installed_ver").strip()
119            continue
120        match = _CANDIDATE_RE.match(line)
121        if match:
122            candidate_ver = match.group("candidate_ver").strip()
123            continue
124
125    # package isn't installed
126    if installed_ver == "(none)":
127        logger.debug("Package is not installed, status is (none)")
128        return False
129    # couldn't find the package
130    if not (installed_ver and candidate_ver):
131        logger.debug("Version info not found [installed: %s ,candidate: %s]",
132                     installed_ver,
133                     candidate_ver)
134        return False
135    # TODO(148116924):Setup process should ask user to update package if the
136    # minimax required version is specified.
137    if compare_version and installed_ver != candidate_ver:
138        logger.warning("Package %s version at %s, expected %s",
139                       pkg_name, installed_ver, candidate_ver)
140    return True
141