|
63 | 63 | CONFIG_SECTION = "mount-watchdog"
|
64 | 64 | MOUNT_CONFIG_SECTION = "mount"
|
65 | 65 | CLIENT_INFO_SECTION = "client-info"
|
| 66 | +ENABLE_VERSION_CHECK = "enable_version_check" |
66 | 67 | CLIENT_SOURCE_STR_LEN_LIMIT = 100
|
67 | 68 | DISABLE_FETCH_EC2_METADATA_TOKEN_ITEM = "disable_fetch_ec2_metadata_token"
|
68 | 69 | DEFAULT_UNKNOWN_VALUE = "unknown"
|
|
75 | 76 |
|
76 | 77 | STATE_FILE_DIR = "/var/run/efs"
|
77 | 78 | STUNNEL_PID_FILE = "stunnel.pid"
|
| 79 | +LAST_VERSION_CHECK_FILE = "last_version_check.json" |
78 | 80 |
|
79 | 81 | DEFAULT_NFS_PORT = "2049"
|
80 | 82 | PRIVATE_KEY_FILE = "/etc/amazon/efs/privateKey.pem"
|
|
182 | 184 | 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"
|
183 | 185 |
|
184 | 186 |
|
| 187 | +def check_if_platform_is_mac(): |
| 188 | + return sys.platform in MAC_OS_PLATFORM_LIST |
| 189 | + |
| 190 | + |
| 191 | +def get_system_release_version(): |
| 192 | + # MacOS does not maintain paths /etc/os-release and /etc/sys-release |
| 193 | + if check_if_platform_is_mac(): |
| 194 | + return platform.platform() |
| 195 | + |
| 196 | + try: |
| 197 | + with open(SYSTEM_RELEASE_PATH) as f: |
| 198 | + return f.read().strip() |
| 199 | + except IOError: |
| 200 | + logging.debug("Unable to read %s", SYSTEM_RELEASE_PATH) |
| 201 | + |
| 202 | + try: |
| 203 | + with open(OS_RELEASE_PATH) as f: |
| 204 | + for line in f: |
| 205 | + if "PRETTY_NAME" in line: |
| 206 | + return line.split("=")[1].strip() |
| 207 | + except IOError: |
| 208 | + logging.debug("Unable to read %s", OS_RELEASE_PATH) |
| 209 | + |
| 210 | + return DEFAULT_UNKNOWN_VALUE |
| 211 | + |
| 212 | + |
| 213 | +class Version: |
| 214 | + """This class is used for the version check logic. An instance of this class represents |
| 215 | + a semantic version following the format major.minor.patch. It is useful for comparing versions.""" |
| 216 | + |
| 217 | + def __init__(self, version_str): |
| 218 | + self.version_str = version_str |
| 219 | + |
| 220 | + # The version list will have the format [major, minor, patch] |
| 221 | + self.version = version_str.split(".") |
| 222 | + if len(self.version) != 3: |
| 223 | + raise ValueError( |
| 224 | + "Version class must be instantiated with a version string that follows the format 'major.minor.patch'." |
| 225 | + ) |
| 226 | + |
| 227 | + def __validate_comparison_input(self, other): |
| 228 | + """Assert that other has type Version""" |
| 229 | + if not isinstance(other, self.__class__): |
| 230 | + raise TypeError( |
| 231 | + "Version comparisons are only permitted between Version instances." |
| 232 | + ) |
| 233 | + |
| 234 | + def __lt__(self, other): |
| 235 | + """returns True if self < other""" |
| 236 | + self.__validate_comparison_input(other) |
| 237 | + return (not self.__eq__(other)) and (not self.__gt__(other)) |
| 238 | + |
| 239 | + def __gt__(self, other): |
| 240 | + """returns True if self > other""" |
| 241 | + self.__validate_comparison_input(other) |
| 242 | + return self.version > other.version |
| 243 | + |
| 244 | + def __eq__(self, other): |
| 245 | + """returns True if self == other""" |
| 246 | + self.__validate_comparison_input(other) |
| 247 | + return self.version == other.version |
| 248 | + |
| 249 | + def __str__(self): |
| 250 | + return self.version_str |
| 251 | + |
| 252 | + |
| 253 | +class EFSUtilsVersionChecker: |
| 254 | + GITHUB_TIMEOUT_SEC = 0.300 |
| 255 | + VERSION_CHECK_POLL_INTERVAL_SEC = 3600 |
| 256 | + VERSION_CHECK_FILE_KEY = "time" |
| 257 | + GITHUB_TAG_API_URL = "https://api.github.com/repos/aws/efs-utils/tags" |
| 258 | + SHOULD_CHECK_AMZN_REPOS = "Amazon Linux" in get_system_release_version() |
| 259 | + |
| 260 | + @staticmethod |
| 261 | + def get_latest_version_by_github(): |
| 262 | + """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""" |
| 263 | + logging.debug("Querying github for the latest amazon-efs-utils version") |
| 264 | + with urlopen( |
| 265 | + EFSUtilsVersionChecker.GITHUB_TAG_API_URL, |
| 266 | + timeout=EFSUtilsVersionChecker.GITHUB_TIMEOUT_SEC, |
| 267 | + ) as response: |
| 268 | + html = response.read() |
| 269 | + string = html.decode("utf-8") |
| 270 | + json_obj = json.loads(string) |
| 271 | + latest_version_dict = json_obj[0] |
| 272 | + latest_version_str = latest_version_dict["name"] |
| 273 | + logging.debug( |
| 274 | + "Latest amazon-efs-utils version found on github: " |
| 275 | + + latest_version_str[1:] |
| 276 | + ) |
| 277 | + return latest_version_str[1:] |
| 278 | + |
| 279 | + @staticmethod |
| 280 | + def get_latest_version_by_yum(): |
| 281 | + """ |
| 282 | + Queries yum and returns a string with the latest tag (version) for efs-utils in the form of (e.g.) 1.34.5. |
| 283 | + Returns an empty string if amazon-efs-utils is not available on Yum |
| 284 | + """ |
| 285 | + logging.debug("Querying yum for the latest amazon-efs-utils version") |
| 286 | + ps_yum = subprocess.Popen( |
| 287 | + ["yum", "info", "amazon-efs-utils"], |
| 288 | + stdout=subprocess.PIPE, |
| 289 | + ) |
| 290 | + ps_grep = subprocess.Popen( |
| 291 | + ["grep", "Available Packages", "-A", "4"], |
| 292 | + stdin=ps_yum.stdout, |
| 293 | + stdout=subprocess.PIPE, |
| 294 | + ) |
| 295 | + latest_version_str = subprocess.check_output( |
| 296 | + ["awk", "/Version/ {printf $3}"], stdin=ps_grep.stdout |
| 297 | + ).decode("utf-8") |
| 298 | + |
| 299 | + if not latest_version_str: |
| 300 | + logging.debug("amazon-efs-utils was not found by yum") |
| 301 | + return "" |
| 302 | + |
| 303 | + logging.debug( |
| 304 | + "Latest amazon-efs-utils version found on yum: " + latest_version_str |
| 305 | + ) |
| 306 | + return latest_version_str |
| 307 | + |
| 308 | + @staticmethod |
| 309 | + def warn_newer_version_available_yum(current_version, newer_version): |
| 310 | + message = ( |
| 311 | + "We recommend you upgrade to the latest version of efs-utils by running 'yum update amazon-efs-utils'. " |
| 312 | + + "Current version: " |
| 313 | + + str(current_version) |
| 314 | + + ". Latest version: " |
| 315 | + + str(newer_version) |
| 316 | + ) |
| 317 | + logging.warning(message) |
| 318 | + |
| 319 | + @staticmethod |
| 320 | + def warn_newer_version_available_github(current_version, newer_version): |
| 321 | + message = ( |
| 322 | + "We recommend you install the latest version of efs-utils from github. " |
| 323 | + + "Current version: " |
| 324 | + + str(current_version) |
| 325 | + + ". Latest version: " |
| 326 | + + str(newer_version) |
| 327 | + ) |
| 328 | + logging.warning(message) |
| 329 | + |
| 330 | + @staticmethod |
| 331 | + def get_last_version_check_time(): |
| 332 | + """ |
| 333 | + Return the date that the last amazon-efs-utils version check was performed. |
| 334 | + Returns None if the version check file does not exist (indicates check hasn't happened yet). |
| 335 | + """ |
| 336 | + version_check_file = os.path.join(STATE_FILE_DIR, LAST_VERSION_CHECK_FILE) |
| 337 | + if not os.path.exists(version_check_file): |
| 338 | + return None |
| 339 | + |
| 340 | + with open(version_check_file, "r") as f: |
| 341 | + data = json.load(f) |
| 342 | + if EFSUtilsVersionChecker.VERSION_CHECK_FILE_KEY not in data: |
| 343 | + return None |
| 344 | + last_version_check_time = datetime.strptime( |
| 345 | + data[EFSUtilsVersionChecker.VERSION_CHECK_FILE_KEY], |
| 346 | + CERT_DATETIME_FORMAT, |
| 347 | + ) |
| 348 | + |
| 349 | + return last_version_check_time |
| 350 | + |
| 351 | + @staticmethod |
| 352 | + def version_check_ready(): |
| 353 | + """Inspect the last version check file and return true if the time since last version check |
| 354 | + is greater than VERSION_CHECK_POLL_INTERVAL""" |
| 355 | + last_version_check_time = EFSUtilsVersionChecker.get_last_version_check_time() |
| 356 | + if not last_version_check_time: |
| 357 | + return True |
| 358 | + |
| 359 | + elapsed_seconds = (get_utc_now() - last_version_check_time).total_seconds() |
| 360 | + return elapsed_seconds >= EFSUtilsVersionChecker.VERSION_CHECK_POLL_INTERVAL_SEC |
| 361 | + |
| 362 | + @staticmethod |
| 363 | + def update_version_check_file(): |
| 364 | + """Write current time into the version check file""" |
| 365 | + current_time_str = get_utc_now().strftime(CERT_DATETIME_FORMAT) |
| 366 | + dictionary = { |
| 367 | + EFSUtilsVersionChecker.VERSION_CHECK_FILE_KEY: current_time_str, |
| 368 | + } |
| 369 | + |
| 370 | + if not os.path.exists(STATE_FILE_DIR): |
| 371 | + logging.warning( |
| 372 | + "update_version_check_file failed: " |
| 373 | + + str(STATE_FILE_DIR) |
| 374 | + + " does not exist." |
| 375 | + ) |
| 376 | + return |
| 377 | + |
| 378 | + with open(os.path.join(STATE_FILE_DIR, LAST_VERSION_CHECK_FILE), "w+") as f: |
| 379 | + json.dump(dictionary, f) |
| 380 | + |
| 381 | + @staticmethod |
| 382 | + def check_if_using_old_version(current_version_string): |
| 383 | + """Log a warning and print to console if newer version of amazon-efs-utils is available. |
| 384 | + The check will first query yum, and then if that fails, |
| 385 | + it will pull the latest tag from the github api. |
| 386 | + """ |
| 387 | + current_version = Version(current_version_string) |
| 388 | + |
| 389 | + if EFSUtilsVersionChecker.SHOULD_CHECK_AMZN_REPOS: |
| 390 | + try: |
| 391 | + latest_yum_version = Version( |
| 392 | + EFSUtilsVersionChecker.get_latest_version_by_yum() |
| 393 | + ) |
| 394 | + if latest_yum_version > current_version: |
| 395 | + EFSUtilsVersionChecker.warn_newer_version_available_yum( |
| 396 | + current_version, latest_yum_version |
| 397 | + ) |
| 398 | + EFSUtilsVersionChecker.update_version_check_file() |
| 399 | + return |
| 400 | + except Exception as err: |
| 401 | + logging.debug( |
| 402 | + "Failed to query Yum for latest version of amazon-efs-utils. " |
| 403 | + + str(err) |
| 404 | + ) |
| 405 | + pass |
| 406 | + |
| 407 | + try: |
| 408 | + latest_github_version = Version( |
| 409 | + EFSUtilsVersionChecker.get_latest_version_by_github() |
| 410 | + ) |
| 411 | + if latest_github_version > current_version: |
| 412 | + EFSUtilsVersionChecker.warn_newer_version_available_github( |
| 413 | + current_version, latest_github_version |
| 414 | + ) |
| 415 | + except Exception as err: |
| 416 | + logging.debug( |
| 417 | + "Failed to query Github for latest version of amazon-efs-utils. This is expected when Github is not reachable. " |
| 418 | + + str(err) |
| 419 | + ) |
| 420 | + pass |
| 421 | + |
| 422 | + EFSUtilsVersionChecker.update_version_check_file() |
| 423 | + |
| 424 | + @staticmethod |
| 425 | + def should_check_efs_utils_version(config): |
| 426 | + """Returns True if a customer has enabled the amazon-efs-utils version check, |
| 427 | + and if the last version check occurred more than VERSION_CHECK_POLL_INTERVAL seconds ago.""" |
| 428 | + version_check_enabled = get_boolean_config_item_value( |
| 429 | + config, CONFIG_SECTION, ENABLE_VERSION_CHECK, default_value=True |
| 430 | + ) |
| 431 | + return ( |
| 432 | + version_check_enabled and EFSUtilsVersionChecker.version_check_ready() |
| 433 | + ) |
| 434 | + |
| 435 | + |
185 | 436 | def fatal_error(user_message, log_message=None):
|
186 | 437 | if log_message is None:
|
187 | 438 | log_message = user_message
|
@@ -869,32 +1120,6 @@ def is_pid_running(pid):
|
869 | 1120 | return False
|
870 | 1121 |
|
871 | 1122 |
|
872 |
| -def check_if_platform_is_mac(): |
873 |
| - return sys.platform in MAC_OS_PLATFORM_LIST |
874 |
| - |
875 |
| - |
876 |
| -def get_system_release_version(): |
877 |
| - # MacOS does not maintain paths /etc/os-release and /etc/sys-release |
878 |
| - if check_if_platform_is_mac(): |
879 |
| - return platform.platform() |
880 |
| - |
881 |
| - try: |
882 |
| - with open(SYSTEM_RELEASE_PATH) as f: |
883 |
| - return f.read().strip() |
884 |
| - except IOError: |
885 |
| - logging.debug("Unable to read %s", SYSTEM_RELEASE_PATH) |
886 |
| - |
887 |
| - try: |
888 |
| - with open(OS_RELEASE_PATH) as f: |
889 |
| - for line in f: |
890 |
| - if "PRETTY_NAME" in line: |
891 |
| - return line.split("=")[1].strip() |
892 |
| - except IOError: |
893 |
| - logging.debug("Unable to read %s", OS_RELEASE_PATH) |
894 |
| - |
895 |
| - return DEFAULT_UNKNOWN_VALUE |
896 |
| - |
897 |
| - |
898 | 1123 | def find_command_path(command, install_method):
|
899 | 1124 | # If not running on macOS, use linux paths
|
900 | 1125 | if not check_if_platform_is_mac():
|
@@ -2149,6 +2374,9 @@ def main():
|
2149 | 2374 | )
|
2150 | 2375 | check_child_procs(child_procs)
|
2151 | 2376 |
|
| 2377 | + if EFSUtilsVersionChecker.should_check_efs_utils_version(config): |
| 2378 | + EFSUtilsVersionChecker.check_if_using_old_version(VERSION) |
| 2379 | + |
2152 | 2380 | time.sleep(poll_interval_sec)
|
2153 | 2381 | else:
|
2154 | 2382 | logging.info("amazon-efs-mount-watchdog is not enabled")
|
|
0 commit comments