azext_edge/edge/util/version_check.py (107 lines of code) (raw):

# coding=utf-8 # ---------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License file in the project root for license information. # ---------------------------------------------------------------------------------------------- import os import re from datetime import datetime, timedelta from typing import Optional import requests from azure.cli.core.azclierror import ValidationError from knack.log import get_logger from rich.console import Console from ...constants import VERSION as CURRENT_CLI_VERSION logger = get_logger(__name__) GH_BASE_RAW_CONTENT_ENDPOINT = "https://raw.githubusercontent.com/" GH_CLI_CONSTANTS_ENDPOINT = ( f"{GH_BASE_RAW_CONTENT_ENDPOINT}Azure/azure-iot-ops-cli-extension/main/azext_edge/constants.py" ) SESSION_FILE_NAME = "iotOpsVersion.json" SESSION_KEY_LAST_FETCHED = "lastFetched" SESSION_KEY_LATEST_VERSION = "latestVersion" SESSION_KEY_FORMAT_VERSION = "formatVersion" FORMAT_VERSION_V1_VALUE = "v1" FETCH_LATEST_AFTER_DAYS = 1 CONFIG_ROOT_LABEL = "iotops" CONFIG_ACTION_LABEL = "check_latest" console = Console(width=88, stderr=True, highlight=False, safe_box=True) def check_latest(cli_ctx, force_refresh: Optional[bool] = False, throw_if_upgrade: Optional[bool] = False): should_check_latest = cli_ctx.config.getboolean(CONFIG_ROOT_LABEL, CONFIG_ACTION_LABEL, fallback=True) if not should_check_latest and not force_refresh: logger.debug("Check for updates is disabled.") return index = IndexManager(cli_ctx) upgrade_semver = index.upgrade_available(force_refresh=force_refresh) if upgrade_semver: update_text = "{}Update available{}. Install with '{}az extension add --upgrade --name azure-iot-ops{}'." update_text_no_markup = update_text.format("", "", "", "") logger.debug(update_text_no_markup) if throw_if_upgrade: raise ValidationError(update_text_no_markup) only_show_errors = getattr(cli_ctx, "only_show_errors", False) if not only_show_errors: console.print( f":dim_button: [italic]{update_text.format('[yellow]', '[/yellow]', '[green]', '[/green]')}", ) class IndexManager: def __init__(self, cli_ctx): self.cli_ctx = cli_ctx self.config_dir = self.cli_ctx.config.config_dir def upgrade_available(self, force_refresh: Optional[bool] = False) -> Optional[str]: try: # Import here for exception safety from packaging import version from azure.cli.core._session import Session self.iot_ops_session = Session(encoding="utf8") self.iot_ops_session.load(os.path.join(self.config_dir, SESSION_FILE_NAME)) latest_cli_version = CURRENT_CLI_VERSION last_fetched = self.iot_ops_session.get(SESSION_KEY_LAST_FETCHED) if last_fetched: last_fetched = datetime.strptime(last_fetched, "%Y-%m-%d %H:%M:%S.%f") latest_cli_version = self.iot_ops_session.get(SESSION_KEY_LATEST_VERSION) or CURRENT_CLI_VERSION if ( not last_fetched or force_refresh or datetime.now() > last_fetched + timedelta(days=FETCH_LATEST_AFTER_DAYS) ): # Record attempted last fetch self.iot_ops_session[SESSION_KEY_LAST_FETCHED] = str(datetime.now()) # Set format version though only v1 is supported now self.iot_ops_session[SESSION_KEY_FORMAT_VERSION] = FORMAT_VERSION_V1_VALUE if check_connectivity(url=GH_CLI_CONSTANTS_ENDPOINT, max_retries=0): _just_fetched_gh_version = get_latest_from_github(url=GH_CLI_CONSTANTS_ENDPOINT, timeout=10) if _just_fetched_gh_version: latest_cli_version = _just_fetched_gh_version self.iot_ops_session[SESSION_KEY_LATEST_VERSION] = latest_cli_version is_upgrade = version.parse(latest_cli_version) > version.parse(CURRENT_CLI_VERSION) if is_upgrade: return latest_cli_version # If anything goes wrong CLI should not crash except Exception as ae: logger.debug(ae) def get_latest_from_github(url: str = GH_CLI_CONSTANTS_ENDPOINT, timeout: int = 10) -> Optional[str]: try: response = requests.get(url, timeout=timeout) if response.status_code != 200: logger.debug( "Failed to fetch the latest version from '%s' with status code '%s' and reason '%s'", url, response.status_code, response.reason, ) return None for line in response.iter_lines(): txt = line.decode("utf-8", errors="ignore") if txt.startswith("VERSION"): match = re.search(r"VERSION = \"(.*)\"$", txt) if match: return match.group(1) except Exception as ex: logger.info("Failed to get the latest version from '%s'. %s", url, str(ex)) def check_connectivity(url: str = GH_CLI_CONSTANTS_ENDPOINT, max_retries: int = 3, timeout: int = 10) -> bool: # TODO: Evaluate function as general util import timeit start = timeit.default_timer() success = False try: with requests.Session() as s: s.mount(url, requests.adapters.HTTPAdapter(max_retries=max_retries)) s.head(url, timeout=timeout) success = True except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as ex: logger.info("Connectivity problem detected.") logger.debug(ex) stop = timeit.default_timer() logger.debug("Connectivity check: %s sec", stop - start) return success