1# Copyright 2019 Microsoft Corporation
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#
15# Requires Python 2.6+ and Openssl 1.0+
16#
17
18import os
19import re
20import platform
21import sys
22
23import azurelinuxagent.common.conf as conf
24import azurelinuxagent.common.utils.shellutil as shellutil
25from azurelinuxagent.common.utils.flexible_version import FlexibleVersion
26from azurelinuxagent.common.future import ustr, get_linux_distribution
27
28__DAEMON_VERSION_ENV_VARIABLE = '_AZURE_GUEST_AGENT_DAEMON_VERSION_'
29"""
30    The daemon process sets this variable's value to the daemon's version number.
31    The variable is set only on versions >= 2.2.53
32"""
33
34
35def set_daemon_version(version):
36    """
37    Sets the value of the _AZURE_GUEST_AGENT_DAEMON_VERSION_ environment variable.
38
39    The given 'version' can be a FlexibleVersion or a string that can be parsed into a FlexibleVersion
40    """
41    flexible_version = version if isinstance(version, FlexibleVersion) else FlexibleVersion(version)
42    os.environ[__DAEMON_VERSION_ENV_VARIABLE] = ustr(flexible_version)
43
44
45def get_daemon_version():
46    """
47    Retrieves the value of the _AZURE_GUEST_AGENT_DAEMON_VERSION_ environment variable.
48    The value indicates the version of the daemon that started the current agent process or, if the current
49    process is the daemon, the version of the current process.
50    If the variable is not set (because the agent is < 2.2.53, or the process was not started by the daemon and
51    the process is not the daemon itself) the function returns "0.0.0.0"
52    """
53    if __DAEMON_VERSION_ENV_VARIABLE in os.environ:
54        return FlexibleVersion(os.environ[__DAEMON_VERSION_ENV_VARIABLE])
55    return FlexibleVersion("0.0.0.0")
56
57
58def get_f5_platform():
59    """
60    Add this workaround for detecting F5 products because BIG-IP/IQ/etc do
61    not show their version info in the /etc/product-version location. Instead,
62    the version and product information is contained in the /VERSION file.
63    """
64    result = [None, None, None, None]
65    f5_version = re.compile("^Version: (\d+\.\d+\.\d+)")  # pylint: disable=W1401
66    f5_product = re.compile("^Product: ([\w-]+)")  # pylint: disable=W1401
67
68    with open('/VERSION', 'r') as fh:
69        content = fh.readlines()
70        for line in content:
71            version_matches = f5_version.match(line)
72            product_matches = f5_product.match(line)
73            if version_matches:
74                result[1] = version_matches.group(1)
75            elif product_matches:
76                result[3] = product_matches.group(1)
77                if result[3] == "BIG-IP":
78                    result[0] = "bigip"
79                    result[2] = "bigip"
80                elif result[3] == "BIG-IQ":
81                    result[0] = "bigiq"
82                    result[2] = "bigiq"
83                elif result[3] == "iWorkflow":
84                    result[0] = "iworkflow"
85                    result[2] = "iworkflow"
86    return result
87
88
89def get_checkpoint_platform():
90    take = build = release = ""
91    full_name = open("/etc/cp-release").read().strip()
92    with open("/etc/cloud-version") as f:
93        for line in f:
94            k, _, v = line.partition(": ")
95            v = v.strip()
96            if k == "release":
97                release = v
98            elif k == "take":
99                take = v
100            elif k == "build":
101                build = v
102    return ["gaia", take + "." + build, release, full_name]
103
104
105def get_distro():
106    if 'FreeBSD' in platform.system():
107        release = re.sub('\-.*\Z', '', ustr(platform.release()))  # pylint: disable=W1401
108        osinfo = ['freebsd', release, '', 'freebsd']
109    elif 'OpenBSD' in platform.system():
110        release = re.sub('\-.*\Z', '', ustr(platform.release()))  # pylint: disable=W1401
111        osinfo = ['openbsd', release, '', 'openbsd']
112    elif 'Linux' in platform.system():
113        osinfo = get_linux_distribution(0, 'alpine')
114    elif 'NS-BSD' in platform.system():
115        release = re.sub('\-.*\Z', '', ustr(platform.release()))  # pylint: disable=W1401
116        osinfo = ['nsbsd', release, '', 'nsbsd']
117    else:
118        try:
119            # dist() removed in Python 3.8
120            osinfo = list(platform.dist()) + ['']  # pylint: disable=W1505,E1101
121        except Exception:
122            osinfo = ['UNKNOWN', 'FFFF', '', '']
123
124    # The platform.py lib has issue with detecting oracle linux distribution.
125    # Merge the following patch provided by oracle as a temporary fix.
126    if os.path.exists("/etc/oracle-release"):
127        osinfo[2] = "oracle"
128        osinfo[3] = "Oracle Linux"
129
130    if os.path.exists("/etc/euleros-release"):
131        osinfo[0] = "euleros"
132
133    if os.path.exists("/etc/mariner-release"):
134        osinfo[0] = "mariner"
135
136    # The platform.py lib has issue with detecting BIG-IP linux distribution.
137    # Merge the following patch provided by F5.
138    if os.path.exists("/shared/vadc"):
139        osinfo = get_f5_platform()
140
141    if os.path.exists("/etc/cp-release"):
142        osinfo = get_checkpoint_platform()
143
144    if os.path.exists("/home/guestshell/azure"):
145        osinfo = ['iosxe', 'csr1000v', '', 'Cisco IOSXE Linux']
146
147    # Remove trailing whitespace and quote in distro name
148    osinfo[0] = osinfo[0].strip('"').strip(' ').lower()
149    return osinfo
150
151COMMAND_ABSENT = ustr("Absent")
152COMMAND_FAILED = ustr("Failed")
153
154
155def get_lis_version():
156    """
157    This uses the Linux kernel's 'modinfo' command to retrieve the
158    "version" field for the "hv_vmbus" kernel module (the LIS
159    drivers). This is the documented method to retrieve the LIS module
160    version. Every Linux guest on Hyper-V will have this driver, but
161    it may not be installed as a module (it could instead be built
162    into the kernel). In that case, this will return "Absent" instead
163    of the version, indicating the driver version can be deduced from
164    the kernel version. It will only return "Failed" in the presence
165    of an exception.
166
167    This function is used to generate telemetry for the version of the
168    LIS drivers installed on the VM. The function and associated
169    telemetry can be removed after a few releases.
170    """
171    try:
172        modinfo_output = shellutil.run_command(["modinfo", "-F", "version", "hv_vmbus"])
173        if modinfo_output:
174            return modinfo_output
175        # If the system doesn't have LIS drivers, 'modinfo' will
176        # return nothing on stdout, which will cause 'run_command'
177        # to return an empty string.
178        return COMMAND_ABSENT
179    except Exception:
180        # Ignore almost every possible exception because this is in a
181        # critical code path. Unfortunately the logger isn't already
182        # imported in this module or we'd log this too.
183        return COMMAND_FAILED
184
185def has_logrotate():
186    try:
187        logrotate_version = shellutil.run_command(["logrotate", "--version"]).split("\n")[0]
188        return logrotate_version
189    except shellutil.CommandError:
190        # A non-zero return code means that logrotate isn't present on
191        # the system; --version shouldn't fail otherwise.
192        return COMMAND_ABSENT
193    except Exception:
194        return COMMAND_FAILED
195
196
197AGENT_NAME = "WALinuxAgent"
198AGENT_LONG_NAME = "Azure Linux Agent"
199AGENT_VERSION = '2.2.54.2'
200AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION)
201AGENT_DESCRIPTION = """
202The Azure Linux Agent supports the provisioning and running of Linux
203VMs in the Azure cloud. This package should be installed on Linux disk
204images that are built to run in the Azure environment.
205"""
206
207AGENT_DIR_GLOB = "{0}-*".format(AGENT_NAME)
208AGENT_PKG_GLOB = "{0}-*.zip".format(AGENT_NAME)
209
210AGENT_PATTERN = "{0}-(.*)".format(AGENT_NAME)
211AGENT_NAME_PATTERN = re.compile(AGENT_PATTERN)
212AGENT_PKG_PATTERN = re.compile(AGENT_PATTERN+"\.zip")  # pylint: disable=W1401
213AGENT_DIR_PATTERN = re.compile(".*/{0}".format(AGENT_PATTERN))
214
215# The execution mode of the VM - IAAS or PAAS. Linux VMs are only executed in IAAS mode.
216AGENT_EXECUTION_MODE = "IAAS"
217
218EXT_HANDLER_PATTERN = b".*/WALinuxAgent-(\d+.\d+.\d+[.\d+]*).*-run-exthandlers"  # pylint: disable=W1401
219EXT_HANDLER_REGEX = re.compile(EXT_HANDLER_PATTERN)
220
221__distro__ = get_distro()
222DISTRO_NAME = __distro__[0]
223DISTRO_VERSION = __distro__[1]
224DISTRO_CODE_NAME = __distro__[2]
225DISTRO_FULL_NAME = __distro__[3]
226
227PY_VERSION = sys.version_info
228PY_VERSION_MAJOR = sys.version_info[0]
229PY_VERSION_MINOR = sys.version_info[1]
230PY_VERSION_MICRO = sys.version_info[2]
231
232
233# Set the CURRENT_AGENT and CURRENT_VERSION to match the agent directory name
234# - This ensures the agent will "see itself" using the same name and version
235#   as the code that downloads agents.
236def set_current_agent():
237    path = os.getcwd()
238    lib_dir = conf.get_lib_dir()
239    if lib_dir[-1] != os.path.sep:
240        lib_dir += os.path.sep
241    agent = path[len(lib_dir):].split(os.path.sep)[0]
242    match = AGENT_NAME_PATTERN.match(agent)
243    if match:
244        version = match.group(1)
245    else:
246        agent = AGENT_LONG_VERSION
247        version = AGENT_VERSION
248    return agent, FlexibleVersion(version)
249
250
251def is_agent_package(path):
252    path = os.path.basename(path)
253    return not re.match(AGENT_PKG_PATTERN, path) is None
254
255
256def is_agent_path(path):
257    path = os.path.basename(path)
258    return not re.match(AGENT_NAME_PATTERN, path) is None
259
260
261CURRENT_AGENT, CURRENT_VERSION = set_current_agent()
262
263
264def set_goal_state_agent():
265    agent = None
266    if os.path.isdir("/proc"):
267        pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
268    else:
269        pids = []
270    for pid in pids:
271        try:
272            pname = open(os.path.join('/proc', pid, 'cmdline'), 'rb').read()
273            match = EXT_HANDLER_REGEX.match(pname)
274            if match:
275                agent = match.group(1)
276                if PY_VERSION_MAJOR > 2:
277                    agent = agent.decode('UTF-8')
278                break
279        except IOError:
280            continue
281    if agent is None:
282        agent = CURRENT_VERSION
283    return agent
284
285
286GOAL_STATE_AGENT_VERSION = set_goal_state_agent()
287
288
289def is_current_agent_installed():
290    return CURRENT_AGENT == AGENT_LONG_VERSION
291