Skip to content

Commit

Permalink
Add warning if using old version
Browse files Browse the repository at this point in the history
Perform a check in watchdog to see if newer version of
amazon-efs-utils is available on yum or github.  If so, log a warning to the
watchdog log file.  To disable this version check, we've added a
config value, enable_version_check.  If check fails,
we silently fail.

Also added Amazon Linux 2023 as supported distribution.
  • Loading branch information
RyanStan committed Jun 1, 2023
1 parent 550df10 commit 3dd89ca
Show file tree
Hide file tree
Showing 5 changed files with 447 additions and 28 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The `efs-utils` package has been verified against the following Linux distributi
|----------------------| ----- | --------- |
| Amazon Linux 2017.09 | `rpm` | `upstart` |
| Amazon Linux 2 | `rpm` | `systemd` |
| Amazon Linux 2023 | `rpm` | `systemd` |
| CentOS 7 | `rpm` | `systemd` |
| CentOS 8 | `rpm` | `systemd` |
| RHEL 7 | `rpm`| `systemd` |
Expand Down Expand Up @@ -72,6 +73,7 @@ The `efs-utils` package has been verified against the following MacOS distributi
- [The way to access instance metadata](#the-way-to-access-instance-metadata)
- [Use the assumed profile credentials for IAM](#use-the-assumed-profile-credentials-for-iam)
- [Enabling FIPS Mode](#enabling-fips-mode)
- [Disabling Version Check](#disabling-version-check)
- [License Summary](#license-summary)


Expand Down Expand Up @@ -558,6 +560,18 @@ Threading:PTHREAD Sockets:POLL,IPv6 SSL:ENGINE,OCSP,FIPS Auth:LIBWRAP

For more information on how to configure OpenSSL with FIPS see the [OpenSSL FIPS README](https://github.com/openssl/openssl/blob/master/README-FIPS.md).

## Disabling Version Check
By default, once an hour, the watchdog daemon service will check to see if a newer version of amazon-efs-utils is available on github or yum.
You can disable this check by setting the `enable_version_check` field in `/etc/amazon/efs/efs-utils.conf` to `false`. For example,
```bash
sudo sed -i 's/enable_version_check = true/enable_version_check = false/' /etc/amazon/efs/efs-utils.conf
```
Or on MacOS:
```bash
VERSION=<efs-utils version, e.g. 1.34.1>
sudo sed -i 's/enable_version_check = true/enable_version_check = false/' /usr/local/Cellar/amazon-efs-utils/${VERSION}/libexec/etc/amazon/efs/efs-utils.conf
```

## License Summary

This code is made available under the MIT license.
4 changes: 4 additions & 0 deletions dist/efs-utils.conf
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ stunnel_health_check_enabled = true
stunnel_health_check_interval_min = 5
stunnel_health_check_command_timeout_sec = 30

# By default, once an hour, the watchdog process will check the latest version of amazon-efs-utils available
# on yum and github. If we detect that the currently installed version is outdated, we'll log a warning.
enable_version_check = true

[cloudwatch-log]
# enabled = true
log_group_name = /aws/efs/utils
Expand Down
4 changes: 2 additions & 2 deletions src/mount_efs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1623,11 +1623,11 @@ def bootstrap_tls(
cert_details["privateKey"] = get_private_key_path()
cert_details["fsId"] = fs_id

start_watchdog(init_system)

if not os.path.exists(state_file_dir):
create_required_directory(config, state_file_dir)

start_watchdog(init_system)

verify_level = int(options.get("verify", DEFAULT_STUNNEL_VERIFY_LEVEL))
ocsp_enabled = is_ocsp_enabled(config, options)

Expand Down
280 changes: 254 additions & 26 deletions src/watchdog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
CONFIG_SECTION = "mount-watchdog"
MOUNT_CONFIG_SECTION = "mount"
CLIENT_INFO_SECTION = "client-info"
ENABLE_VERSION_CHECK = "enable_version_check"
CLIENT_SOURCE_STR_LEN_LIMIT = 100
DISABLE_FETCH_EC2_METADATA_TOKEN_ITEM = "disable_fetch_ec2_metadata_token"
DEFAULT_UNKNOWN_VALUE = "unknown"
Expand All @@ -75,6 +76,7 @@

STATE_FILE_DIR = "/var/run/efs"
STUNNEL_PID_FILE = "stunnel.pid"
LAST_VERSION_CHECK_FILE = "last_version_check.json"

DEFAULT_NFS_PORT = "2049"
PRIVATE_KEY_FILE = "/etc/amazon/efs/privateKey.pem"
Expand Down Expand Up @@ -182,6 +184,255 @@
STUNNEL_INSTALLATION_MESSAGE = "Please install it following the instructions at: https://docs.aws.amazon.com/efs/latest/ug/using-amazon-efs-utils.html#upgrading-stunnel"


def check_if_platform_is_mac():
return sys.platform in MAC_OS_PLATFORM_LIST


def get_system_release_version():
# MacOS does not maintain paths /etc/os-release and /etc/sys-release
if check_if_platform_is_mac():
return platform.platform()

try:
with open(SYSTEM_RELEASE_PATH) as f:
return f.read().strip()
except IOError:
logging.debug("Unable to read %s", SYSTEM_RELEASE_PATH)

try:
with open(OS_RELEASE_PATH) as f:
for line in f:
if "PRETTY_NAME" in line:
return line.split("=")[1].strip()
except IOError:
logging.debug("Unable to read %s", OS_RELEASE_PATH)

return DEFAULT_UNKNOWN_VALUE


class Version:
"""This class is used for the version check logic. An instance of this class represents
a semantic version following the format major.minor.patch. It is useful for comparing versions."""

def __init__(self, version_str):
self.version_str = version_str

# The version list will have the format [major, minor, patch]
self.version = version_str.split(".")
if len(self.version) != 3:
raise ValueError(
"Version class must be instantiated with a version string that follows the format 'major.minor.patch'."
)

def __validate_comparison_input(self, other):
"""Assert that other has type Version"""
if not isinstance(other, self.__class__):
raise TypeError(
"Version comparisons are only permitted between Version instances."
)

def __lt__(self, other):
"""returns True if self < other"""
self.__validate_comparison_input(other)
return (not self.__eq__(other)) and (not self.__gt__(other))

def __gt__(self, other):
"""returns True if self > other"""
self.__validate_comparison_input(other)
return self.version > other.version

def __eq__(self, other):
"""returns True if self == other"""
self.__validate_comparison_input(other)
return self.version == other.version

def __str__(self):
return self.version_str


class EFSUtilsVersionChecker:
GITHUB_TIMEOUT_SEC = 0.300
VERSION_CHECK_POLL_INTERVAL_SEC = 3600
VERSION_CHECK_FILE_KEY = "time"
GITHUB_TAG_API_URL = "https://api.github.com/repos/aws/efs-utils/tags"
SHOULD_CHECK_AMZN_REPOS = "Amazon Linux" in get_system_release_version()

@staticmethod
def get_latest_version_by_github():
"""Queries the github api and returns a string with the latest tag (version) for efs-utils in the form of (e.g.) 1.34.5"""
logging.debug("Querying github for the latest amazon-efs-utils version")
with urlopen(
EFSUtilsVersionChecker.GITHUB_TAG_API_URL,
timeout=EFSUtilsVersionChecker.GITHUB_TIMEOUT_SEC,
) as response:
html = response.read()
string = html.decode("utf-8")
json_obj = json.loads(string)
latest_version_dict = json_obj[0]
latest_version_str = latest_version_dict["name"]
logging.debug(
"Latest amazon-efs-utils version found on github: "
+ latest_version_str[1:]
)
return latest_version_str[1:]

@staticmethod
def get_latest_version_by_yum():
"""
Queries yum and returns a string with the latest tag (version) for efs-utils in the form of (e.g.) 1.34.5.
Returns an empty string if amazon-efs-utils is not available on Yum
"""
logging.debug("Querying yum for the latest amazon-efs-utils version")
ps_yum = subprocess.Popen(
["yum", "info", "amazon-efs-utils"],
stdout=subprocess.PIPE,
)
ps_grep = subprocess.Popen(
["grep", "Available Packages", "-A", "4"],
stdin=ps_yum.stdout,
stdout=subprocess.PIPE,
)
latest_version_str = subprocess.check_output(
["awk", "/Version/ {printf $3}"], stdin=ps_grep.stdout
).decode("utf-8")

if not latest_version_str:
logging.debug("amazon-efs-utils was not found by yum")
return ""

logging.debug(
"Latest amazon-efs-utils version found on yum: " + latest_version_str
)
return latest_version_str

@staticmethod
def warn_newer_version_available_yum(current_version, newer_version):
message = (
"We recommend you upgrade to the latest version of efs-utils by running 'yum update amazon-efs-utils'. "
+ "Current version: "
+ str(current_version)
+ ". Latest version: "
+ str(newer_version)
)
logging.warning(message)

@staticmethod
def warn_newer_version_available_github(current_version, newer_version):
message = (
"We recommend you install the latest version of efs-utils from github. "
+ "Current version: "
+ str(current_version)
+ ". Latest version: "
+ str(newer_version)
)
logging.warning(message)

@staticmethod
def get_last_version_check_time():
"""
Return the date that the last amazon-efs-utils version check was performed.
Returns None if the version check file does not exist (indicates check hasn't happened yet).
"""
version_check_file = os.path.join(STATE_FILE_DIR, LAST_VERSION_CHECK_FILE)
if not os.path.exists(version_check_file):
return None

with open(version_check_file, "r") as f:
data = json.load(f)
if EFSUtilsVersionChecker.VERSION_CHECK_FILE_KEY not in data:
return None
last_version_check_time = datetime.strptime(
data[EFSUtilsVersionChecker.VERSION_CHECK_FILE_KEY],
CERT_DATETIME_FORMAT,
)

return last_version_check_time

@staticmethod
def version_check_ready():
"""Inspect the last version check file and return true if the time since last version check
is greater than VERSION_CHECK_POLL_INTERVAL"""
last_version_check_time = EFSUtilsVersionChecker.get_last_version_check_time()
if not last_version_check_time:
return True

elapsed_seconds = (get_utc_now() - last_version_check_time).total_seconds()
return elapsed_seconds >= EFSUtilsVersionChecker.VERSION_CHECK_POLL_INTERVAL_SEC

@staticmethod
def update_version_check_file():
"""Write current time into the version check file"""
current_time_str = get_utc_now().strftime(CERT_DATETIME_FORMAT)
dictionary = {
EFSUtilsVersionChecker.VERSION_CHECK_FILE_KEY: current_time_str,
}

if not os.path.exists(STATE_FILE_DIR):
logging.warning(
"update_version_check_file failed: "
+ str(STATE_FILE_DIR)
+ " does not exist."
)
return

with open(os.path.join(STATE_FILE_DIR, LAST_VERSION_CHECK_FILE), "w+") as f:
json.dump(dictionary, f)

@staticmethod
def check_if_using_old_version(current_version_string):
"""Log a warning and print to console if newer version of amazon-efs-utils is available.
The check will first query yum, and then if that fails,
it will pull the latest tag from the github api.
"""
current_version = Version(current_version_string)

if EFSUtilsVersionChecker.SHOULD_CHECK_AMZN_REPOS:
try:
latest_yum_version = Version(
EFSUtilsVersionChecker.get_latest_version_by_yum()
)
if latest_yum_version > current_version:
EFSUtilsVersionChecker.warn_newer_version_available_yum(
current_version, latest_yum_version
)
EFSUtilsVersionChecker.update_version_check_file()
return
except Exception as err:
logging.debug(
"Failed to query Yum for latest version of amazon-efs-utils. "
+ str(err)
)
pass

try:
latest_github_version = Version(
EFSUtilsVersionChecker.get_latest_version_by_github()
)
if latest_github_version > current_version:
EFSUtilsVersionChecker.warn_newer_version_available_github(
current_version, latest_github_version
)
except Exception as err:
logging.debug(
"Failed to query Github for latest version of amazon-efs-utils. This is expected when Github is not reachable. "
+ str(err)
)
pass

EFSUtilsVersionChecker.update_version_check_file()

@staticmethod
def should_check_efs_utils_version(config):
"""Returns True if a customer has enabled the amazon-efs-utils version check,
and if the last version check occurred more than VERSION_CHECK_POLL_INTERVAL seconds ago."""
version_check_enabled = get_boolean_config_item_value(
config, CONFIG_SECTION, ENABLE_VERSION_CHECK, default_value=True
)
return (
version_check_enabled and EFSUtilsVersionChecker.version_check_ready()
)


def fatal_error(user_message, log_message=None):
if log_message is None:
log_message = user_message
Expand Down Expand Up @@ -869,32 +1120,6 @@ def is_pid_running(pid):
return False


def check_if_platform_is_mac():
return sys.platform in MAC_OS_PLATFORM_LIST


def get_system_release_version():
# MacOS does not maintain paths /etc/os-release and /etc/sys-release
if check_if_platform_is_mac():
return platform.platform()

try:
with open(SYSTEM_RELEASE_PATH) as f:
return f.read().strip()
except IOError:
logging.debug("Unable to read %s", SYSTEM_RELEASE_PATH)

try:
with open(OS_RELEASE_PATH) as f:
for line in f:
if "PRETTY_NAME" in line:
return line.split("=")[1].strip()
except IOError:
logging.debug("Unable to read %s", OS_RELEASE_PATH)

return DEFAULT_UNKNOWN_VALUE


def find_command_path(command, install_method):
# If not running on macOS, use linux paths
if not check_if_platform_is_mac():
Expand Down Expand Up @@ -2149,6 +2374,9 @@ def main():
)
check_child_procs(child_procs)

if EFSUtilsVersionChecker.should_check_efs_utils_version(config):
EFSUtilsVersionChecker.check_if_using_old_version(VERSION)

time.sleep(poll_interval_sec)
else:
logging.info("amazon-efs-mount-watchdog is not enabled")
Expand Down
Loading

0 comments on commit 3dd89ca

Please sign in to comment.