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
24from acloud.internal.lib import utils
25
26
27logger = logging.getLogger(__name__)
28
29PKG_INSTALL_CMD = "sudo apt-get --assume-yes install %s"
30APT_CHECK_CMD = "LANG=en_US.UTF-8 apt-cache policy %s"
31_INSTALLED_RE = re.compile(r"(.*\s*Installed:)(?P<installed_ver>.*\s?)")
32_CANDIDATE_RE = re.compile(r"(.*\s*Candidate:)(?P<candidate_ver>.*\s?)")
33
34
35def CheckCmdOutput(cmd, print_cmd=True, **kwargs):
36    """Helper function to run subprocess.check_output.
37
38    This function will return the command output for parsing the result and will
39    raise Error if command return code was non-zero.
40
41    Args:
42        cmd: String, the cmd string.
43        print_cmd: True to print cmd to stdout.
44        kwargs: Other option args to subprocess.
45
46    Returns:
47        Return cmd output as a byte string.
48        If the return code was non-zero it raises a CalledProcessError.
49    """
50    if print_cmd:
51        print("Run command: %s" % cmd)
52
53    logger.debug("Run command: %s", cmd)
54    return utils.CheckOutput(cmd, **kwargs)
55
56
57def InstallPackage(pkg):
58    """Install package.
59
60    Args:
61        pkg: String, the name of package.
62
63    Raises:
64        PackageInstallError: package is not installed.
65    """
66    try:
67        print(CheckCmdOutput(PKG_INSTALL_CMD % pkg,
68                             shell=True,
69                             stderr=subprocess.STDOUT))
70    except subprocess.CalledProcessError as cpe:
71        logger.error("Package install for %s failed: %s", pkg, cpe.output)
72        raise errors.PackageInstallError(
73            "Could not install package [" + pkg + "], :" + str(cpe.output))
74
75    if not PackageInstalled(pkg, compare_version=False):
76        raise errors.PackageInstallError(
77            "Package was not detected as installed after installation [" +
78            pkg + "]")
79
80
81def PackageInstalled(pkg_name, compare_version=True):
82    """Check if the package is installed or not.
83
84    This method will validate that the specified package is installed
85    (via apt cache policy) and check if the installed version is up-to-date.
86
87    Args:
88        pkg_name: String, the package name.
89        compare_version: Boolean, True to compare version.
90
91    Returns:
92        True if package is installed.and False if not installed or
93        the pre-installed package is not the same version as the repo candidate
94        version.
95
96    Raises:
97        UnableToLocatePkgOnRepositoryError: Unable to locate package on repository.
98    """
99    try:
100        pkg_info = CheckCmdOutput(
101            APT_CHECK_CMD % pkg_name,
102            print_cmd=False,
103            shell=True,
104            stderr=subprocess.STDOUT)
105
106        logger.debug("Check package install status")
107        logger.debug(pkg_info)
108    except subprocess.CalledProcessError as error:
109        # Unable locate package name on repository.
110        raise errors.UnableToLocatePkgOnRepositoryError(
111            "Could not find package [" + pkg_name + "] on repository, :" +
112            str(error.output) + ", have you forgotten to run 'apt update'?")
113
114    installed_ver = None
115    candidate_ver = None
116    for line in pkg_info.splitlines():
117        match = _INSTALLED_RE.match(line)
118        if match:
119            installed_ver = match.group("installed_ver").strip()
120            continue
121        match = _CANDIDATE_RE.match(line)
122        if match:
123            candidate_ver = match.group("candidate_ver").strip()
124            continue
125
126    # package isn't installed
127    if installed_ver == "(none)":
128        logger.debug("Package is not installed, status is (none)")
129        return False
130    # couldn't find the package
131    if not (installed_ver and candidate_ver):
132        logger.debug("Version info not found [installed: %s ,candidate: %s]",
133                     installed_ver,
134                     candidate_ver)
135        return False
136    # TODO(148116924):Setup process should ask user to update package if the
137    # minimax required version is specified.
138    if compare_version and installed_ver != candidate_ver:
139        logger.warning("Package %s version at %s, expected %s",
140                       pkg_name, installed_ver, candidate_ver)
141    return True
142