From bb475c69fabfcf8e5b31bc45d1d2a44fe19a769e Mon Sep 17 00:00:00 2001 From: Brett Holman Date: Fri, 2 Aug 2024 12:58:58 -0600 Subject: [PATCH] chore: Add helper, refactor utilities into separate module (#5573) --- cloudinit/cmd/main.py | 9 +- cloudinit/config/cc_ansible.py | 10 +- cloudinit/config/cc_apt_configure.py | 8 +- cloudinit/config/cc_ca_certs.py | 6 +- cloudinit/config/cc_growpart.py | 4 +- cloudinit/config/cc_resizefs.py | 6 +- cloudinit/config/cc_rsyslog.py | 4 +- cloudinit/config/cc_set_passwords.py | 8 +- cloudinit/config/cc_ssh.py | 4 +- cloudinit/config/cc_update_etc_hosts.py | 4 +- cloudinit/config/modules.py | 8 +- cloudinit/config/schema.py | 12 +- cloudinit/distros/__init__.py | 5 +- cloudinit/distros/alpine.py | 4 +- cloudinit/distros/ug_util.py | 6 +- cloudinit/features.py | 5 + cloudinit/lifecycle.py | 242 ++++++++++++++++++ cloudinit/net/network_state.py | 4 +- cloudinit/netinfo.py | 4 +- cloudinit/sources/DataSourceConfigDrive.py | 4 +- cloudinit/sources/DataSourceDigitalOcean.py | 4 +- cloudinit/sources/DataSourceNoCloud.py | 8 +- cloudinit/sources/__init__.py | 4 +- cloudinit/ssh_util.py | 6 +- cloudinit/stages.py | 3 +- cloudinit/util.py | 208 +-------------- tests/integration_tests/cmd/test_schema.py | 4 +- .../datasources/test_nocloud.py | 4 +- .../modules/test_combined.py | 5 +- .../modules/test_ubuntu_pro.py | 4 +- tests/unittests/config/test_cc_ansible.py | 8 +- tests/unittests/config/test_cc_ssh.py | 4 +- tests/unittests/config/test_cc_ubuntu_pro.py | 9 +- tests/unittests/conftest.py | 4 +- tests/unittests/distros/test_create_users.py | 7 +- tests/unittests/net/test_network_state.py | 4 +- tests/unittests/sources/test_digitalocean.py | 4 +- tests/unittests/test_log.py | 10 +- tests/unittests/test_util.py | 40 ++- 39 files changed, 380 insertions(+), 317 deletions(-) create mode 100644 cloudinit/lifecycle.py diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index 590173ae4fc..54ba79e1bbc 100644 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -31,6 +31,7 @@ from cloudinit import warnings from cloudinit import reporting from cloudinit import atomic_helper +from cloudinit import lifecycle from cloudinit.cmd.devel import read_cfg_paths from cloudinit.config import cc_set_hostname from cloudinit.config.modules import Modules @@ -94,7 +95,7 @@ def log_ppid(distro, bootstage_name): if distro.is_linux: ppid = os.getppid() if 1 != ppid and distro.uses_systemd(): - util.deprecate( + lifecycle.deprecate( deprecated=( "Unsupported configuration: boot stage called " f"by PID [{ppid}] outside of systemd" @@ -255,7 +256,7 @@ def attempt_cmdline_url(path, network=True, cmdline=None) -> Tuple[int, str]: is_cloud_cfg = False if is_cloud_cfg: if cmdline_name == "url": - return util.deprecate( + return lifecycle.deprecate( deprecated="The kernel command line key `url`", deprecated_version="22.3", extra_message=" Please use `cloud-config-url` " @@ -650,7 +651,7 @@ def main_modules(action_name, args): log_ppid(init.distro, bootstage_name) if name == "init": - util.deprecate( + lifecycle.deprecate( deprecated="`--mode init`", deprecated_version="24.1", extra_message="Use `cloud-init init` instead.", @@ -983,7 +984,7 @@ def main(sysv_args=None): parser_mod = subparsers.add_parser( "modules", help="Activate modules using a given configuration key." ) - extra_help = util.deprecate( + extra_help = lifecycle.deprecate( deprecated="`init`", deprecated_version="24.1", extra_message="Use `cloud-init init` instead.", diff --git a/cloudinit/config/cc_ansible.py b/cloudinit/config/cc_ansible.py index fce8ae3b4c4..3b9e931a58d 100644 --- a/cloudinit/config/cc_ansible.py +++ b/cloudinit/config/cc_ansible.py @@ -8,13 +8,13 @@ from copy import deepcopy from typing import Optional -from cloudinit import subp +from cloudinit import lifecycle, subp from cloudinit.cloud import Cloud from cloudinit.config import Config from cloudinit.config.schema import MetaSchema from cloudinit.distros import ALL_DISTROS, Distro from cloudinit.settings import PER_INSTANCE -from cloudinit.util import Version, get_cfg_by_path +from cloudinit.util import get_cfg_by_path meta: MetaSchema = { "id": "cc_ansible", @@ -39,13 +39,13 @@ def __init__(self, distro: Distro): # and cloud-init might not have that set, default: /root self.env["HOME"] = os.environ.get("HOME", "/root") - def get_version(self) -> Optional[Version]: + def get_version(self) -> Optional[lifecycle.Version]: stdout, _ = self.do_as(self.cmd_version) first_line = stdout.splitlines().pop(0) matches = re.search(r"([\d\.]+)", first_line) if matches: version = matches.group(0) - return Version.from_str(version) + return lifecycle.Version.from_str(version) return None def pull(self, *args) -> str: @@ -210,7 +210,7 @@ def run_ansible_pull(pull: AnsiblePull, cfg: dict): v = pull.get_version() if not v: LOG.warning("Cannot parse ansible version") - elif v < Version(2, 7, 0): + elif v < lifecycle.Version(2, 7, 0): # diff was added in commit edaa0b52450ade9b86b5f63097ce18ebb147f46f if cfg.get("diff"): raise ValueError( diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py index b79b6483b9e..787270e665d 100644 --- a/cloudinit/config/cc_apt_configure.py +++ b/cloudinit/config/cc_apt_configure.py @@ -17,7 +17,7 @@ from textwrap import indent from typing import Dict, Iterable, List, Mapping -from cloudinit import features, subp, templater, util +from cloudinit import features, lifecycle, subp, templater, util from cloudinit.cloud import Cloud from cloudinit.config import Config from cloudinit.config.schema import MetaSchema @@ -745,7 +745,7 @@ def add_apt_sources( def convert_v1_to_v2_apt_format(srclist): """convert v1 apt format to v2 (dict in apt_sources)""" srcdict = {} - util.deprecate( + lifecycle.deprecate( deprecated="Config key 'apt_sources'", deprecated_version="22.1", extra_message="Use 'apt' instead", @@ -824,7 +824,7 @@ def convert_v2_to_v3_apt_format(oldcfg): # no old config, so no new one to be created if not needtoconvert: return oldcfg - util.deprecate( + lifecycle.deprecate( deprecated=f"The following config key(s): {needtoconvert}", deprecated_version="22.1", ) @@ -832,7 +832,7 @@ def convert_v2_to_v3_apt_format(oldcfg): # if old AND new config are provided, prefer the new one (LP #1616831) newaptcfg = oldcfg.get("apt", None) if newaptcfg is not None: - util.deprecate( + lifecycle.deprecate( deprecated="Support for combined old and new apt module keys", deprecated_version="22.1", ) diff --git a/cloudinit/config/cc_ca_certs.py b/cloudinit/config/cc_ca_certs.py index 4e80947fd13..d6dbc977f88 100644 --- a/cloudinit/config/cc_ca_certs.py +++ b/cloudinit/config/cc_ca_certs.py @@ -7,7 +7,7 @@ import logging import os -from cloudinit import subp, util +from cloudinit import lifecycle, subp, util from cloudinit.cloud import Cloud from cloudinit.config import Config from cloudinit.config.schema import MetaSchema @@ -231,7 +231,7 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: @param args: Any module arguments from cloud.cfg """ if "ca-certs" in cfg: - util.deprecate( + lifecycle.deprecate( deprecated="Key 'ca-certs'", deprecated_version="22.1", extra_message="Use 'ca_certs' instead.", @@ -254,7 +254,7 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: # If there is a remove_defaults option set to true, disable the system # default trusted CA certs first. if "remove-defaults" in ca_cert_cfg: - util.deprecate( + lifecycle.deprecate( deprecated="Key 'remove-defaults'", deprecated_version="22.1", extra_message="Use 'remove_defaults' instead.", diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py index e1a56f91f09..459f0a3cded 100644 --- a/cloudinit/config/cc_growpart.py +++ b/cloudinit/config/cc_growpart.py @@ -20,7 +20,7 @@ from pathlib import Path from typing import Optional, Tuple -from cloudinit import subp, temp_utils, util +from cloudinit import lifecycle, subp, temp_utils, util from cloudinit.cloud import Cloud from cloudinit.config import Config from cloudinit.config.schema import MetaSchema @@ -542,7 +542,7 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: mode = mycfg.get("mode", "auto") if util.is_false(mode): if mode != "off": - util.deprecate( + lifecycle.deprecate( deprecated=f"Growpart's 'mode' key with value '{mode}'", deprecated_version="22.2", extra_message="Use 'off' instead.", diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py index 77a2a26a7c4..b90db58ff88 100644 --- a/cloudinit/config/cc_resizefs.py +++ b/cloudinit/config/cc_resizefs.py @@ -15,7 +15,7 @@ import stat from typing import Optional -from cloudinit import subp, util +from cloudinit import lifecycle, subp, util from cloudinit.cloud import Cloud from cloudinit.config import Config from cloudinit.config.schema import MetaSchema @@ -56,8 +56,8 @@ def _resize_btrfs(mount_point, devpth): # btrfs has exclusive operations and resize may fail if btrfs is busy # doing one of the operations that prevents resize. As of btrfs 5.10 # the resize operation can be queued - btrfs_with_queue = util.Version.from_str("5.10") - system_btrfs_ver = util.Version.from_str( + btrfs_with_queue = lifecycle.Version.from_str("5.10") + system_btrfs_ver = lifecycle.Version.from_str( subp.subp(["btrfs", "--version"])[0].split("v")[-1].strip() ) if system_btrfs_ver >= btrfs_with_queue: diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py index 3edf9972bf9..88ec1c4f3a1 100644 --- a/cloudinit/config/cc_rsyslog.py +++ b/cloudinit/config/cc_rsyslog.py @@ -17,7 +17,7 @@ import re from textwrap import dedent -from cloudinit import log, subp, util +from cloudinit import lifecycle, log, subp, util from cloudinit.cloud import Cloud from cloudinit.config import Config from cloudinit.config.schema import MetaSchema @@ -153,7 +153,7 @@ def load_config(cfg: dict, distro: Distro) -> dict: distro_config = distro_default_rsyslog_config(distro) if isinstance(cfg.get("rsyslog"), list): - util.deprecate( + lifecycle.deprecate( deprecated="The rsyslog key with value of type 'list'", deprecated_version="22.2", ) diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py index 21408105c74..224ae6b85fe 100644 --- a/cloudinit/config/cc_set_passwords.py +++ b/cloudinit/config/cc_set_passwords.py @@ -12,7 +12,7 @@ from string import ascii_letters, digits from typing import List -from cloudinit import features, subp, util +from cloudinit import features, lifecycle, subp, util from cloudinit.cloud import Cloud from cloudinit.config import Config from cloudinit.config.schema import MetaSchema @@ -71,7 +71,7 @@ def handle_ssh_pwauth(pw_auth, distro: Distro): cfg_name = "PasswordAuthentication" if isinstance(pw_auth, str): - util.deprecate( + lifecycle.deprecate( deprecated="Using a string value for the 'ssh_pwauth' key", deprecated_version="22.2", extra_message="Use a boolean value with 'ssh_pwauth'.", @@ -128,7 +128,7 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: chfg = cfg["chpasswd"] users_list = util.get_cfg_option_list(chfg, "users", default=[]) if "list" in chfg and chfg["list"]: - util.deprecate( + lifecycle.deprecate( deprecated="Config key 'lists'", deprecated_version="22.3", extra_message="Use 'users' instead.", @@ -137,7 +137,7 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: LOG.debug("Handling input for chpasswd as list.") plist = util.get_cfg_option_list(chfg, "list", plist) else: - util.deprecate( + lifecycle.deprecate( deprecated="The chpasswd multiline string", deprecated_version="22.2", extra_message="Use string type instead.", diff --git a/cloudinit/config/cc_ssh.py b/cloudinit/config/cc_ssh.py index 00687cf867d..947469b5b6d 100644 --- a/cloudinit/config/cc_ssh.py +++ b/cloudinit/config/cc_ssh.py @@ -14,7 +14,7 @@ import sys from typing import List, Optional, Sequence -from cloudinit import ssh_util, subp, util +from cloudinit import lifecycle, ssh_util, subp, util from cloudinit.cloud import Cloud from cloudinit.config import Config from cloudinit.config.schema import MetaSchema @@ -75,7 +75,7 @@ def set_redhat_keyfile_perms(keyfile: str) -> None: """ permissions_public = 0o644 ssh_version = ssh_util.get_opensshd_upstream_version() - if ssh_version and ssh_version < util.Version(9, 0): + if ssh_version and ssh_version < lifecycle.Version(9, 0): # fedora 37, centos 9 stream and below has sshd # versions less than 9 and private key permissions are # set to 0o640 from sshd-keygen. diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py index 45bb2df7d4b..dcd50701a20 100644 --- a/cloudinit/config/cc_update_etc_hosts.py +++ b/cloudinit/config/cc_update_etc_hosts.py @@ -10,7 +10,7 @@ import logging -from cloudinit import templater, util +from cloudinit import lifecycle, templater, util from cloudinit.cloud import Cloud from cloudinit.config import Config from cloudinit.config.schema import MetaSchema @@ -33,7 +33,7 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: if util.translate_bool(manage_hosts, addons=["template"]): if manage_hosts == "template": - util.deprecate( + lifecycle.deprecate( deprecated="Value 'template' for key 'manage_etc_hosts'", deprecated_version="22.2", extra_message="Use 'true' instead.", diff --git a/cloudinit/config/modules.py b/cloudinit/config/modules.py index f775802d74a..a82e1ff8e8e 100644 --- a/cloudinit/config/modules.py +++ b/cloudinit/config/modules.py @@ -12,7 +12,7 @@ from types import ModuleType from typing import Dict, List, NamedTuple, Optional -from cloudinit import config, importer, type_utils, util +from cloudinit import config, importer, lifecycle, type_utils, util from cloudinit.distros import ALL_DISTROS from cloudinit.helpers import ConfigMerger from cloudinit.reporting.events import ReportEventStack @@ -194,7 +194,7 @@ def _fixup_modules(self, raw_mods) -> List[ModuleDetails]: if not mod_name: continue if freq and freq not in FREQUENCIES: - util.deprecate( + lifecycle.deprecate( deprecated=( f"Config specified module {raw_name} has an unknown" f" frequency {freq}" @@ -205,7 +205,7 @@ def _fixup_modules(self, raw_mods) -> List[ModuleDetails]: # default meta attribute "frequency" value is used. freq = None if mod_name in RENAMED_MODULES: - util.deprecate( + lifecycle.deprecate( deprecated=( f"Module has been renamed from {mod_name} to " f"{RENAMED_MODULES[mod_name]}. Update any" @@ -278,7 +278,7 @@ def _run_modules(self, mostly_mods: List[ModuleDetails]): func_signature = signature(mod.handle) func_params = func_signature.parameters if len(func_params) == 5: - util.deprecate( + lifecycle.deprecate( deprecated="Config modules with a `log` parameter", deprecated_version="23.2", ) diff --git a/cloudinit/config/schema.py b/cloudinit/config/schema.py index 062ab92ecd8..a2fceecabcb 100644 --- a/cloudinit/config/schema.py +++ b/cloudinit/config/schema.py @@ -31,7 +31,7 @@ import yaml -from cloudinit import features, importer, safeyaml +from cloudinit import features, importer, lifecycle, safeyaml from cloudinit.cmd.devel import read_cfg_paths from cloudinit.handlers import INCLUSION_TYPES_MAP, type_from_starts_with from cloudinit.helpers import Paths @@ -42,7 +42,6 @@ get_modules_from_dir, load_text_file, load_yaml, - should_log_deprecation, write_file, ) @@ -795,8 +794,11 @@ def validate_cloudconfig_schema( if isinstance( schema_error, SchemaDeprecationError ): # pylint: disable=W1116 - if schema_error.version == "devel" or should_log_deprecation( - schema_error.version, features.DEPRECATION_INFO_BOUNDARY + if ( + schema_error.version == "devel" + or lifecycle.should_log_deprecation( + schema_error.version, features.DEPRECATION_INFO_BOUNDARY + ) ): deprecations.append(SchemaProblem(path, schema_error.message)) else: @@ -818,7 +820,7 @@ def validate_cloudconfig_schema( deprecations, prefix="Deprecated cloud-config provided: ", ) - # This warning doesn't fit the standardized util.deprecated() + # This warning doesn't fit the standardized lifecycle.deprecated() # utility format, but it is a deprecation log, so log it directly. LOG.deprecated(message) # type: ignore if strict and (errors or deprecations or info_deprecations): diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index e6bfb1d3b48..1afef63de95 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -36,6 +36,7 @@ from cloudinit import ( helpers, importer, + lifecycle, net, persistence, ssh_util, @@ -710,7 +711,7 @@ def add_user(self, name, **kwargs): groups = groups.split(",") if isinstance(groups, dict): - util.deprecate( + lifecycle.deprecate( deprecated=f"The user {name} has a 'groups' config value " "of type dict", deprecated_version="22.3", @@ -848,7 +849,7 @@ def create_user(self, name, **kwargs): if kwargs["sudo"]: self.write_sudo_rules(name, kwargs["sudo"]) elif kwargs["sudo"] is False: - util.deprecate( + lifecycle.deprecate( deprecated=f"The value of 'false' in user {name}'s " "'sudo' config", deprecated_version="22.2", diff --git a/cloudinit/distros/alpine.py b/cloudinit/distros/alpine.py index a1d0d900c9f..dae4b61564e 100644 --- a/cloudinit/distros/alpine.py +++ b/cloudinit/distros/alpine.py @@ -13,7 +13,7 @@ from datetime import datetime from typing import Any, Dict, Optional -from cloudinit import distros, helpers, subp, util +from cloudinit import distros, helpers, lifecycle, subp, util from cloudinit.distros.parsers.hostname import HostnameConf from cloudinit.settings import PER_ALWAYS, PER_INSTANCE @@ -248,7 +248,7 @@ def add_user(self, name, **kwargs): if isinstance(groups, str): groups = groups.split(",") elif isinstance(groups, dict): - util.deprecate( + lifecycle.deprecate( deprecated=f"The user {name} has a 'groups' config value " "of type dict", deprecated_version="22.3", diff --git a/cloudinit/distros/ug_util.py b/cloudinit/distros/ug_util.py index b8d14937488..2d0a887e7c4 100644 --- a/cloudinit/distros/ug_util.py +++ b/cloudinit/distros/ug_util.py @@ -11,7 +11,7 @@ import logging -from cloudinit import type_utils, util +from cloudinit import lifecycle, type_utils, util LOG = logging.getLogger(__name__) @@ -175,7 +175,7 @@ def normalize_users_groups(cfg, distro): # Translate it into a format that will be more useful going forward if isinstance(old_user, str): old_user = {"name": old_user} - util.deprecate( + lifecycle.deprecate( deprecated="'user' of type string", deprecated_version="22.2", extra_message="Use 'users' list instead.", @@ -208,7 +208,7 @@ def normalize_users_groups(cfg, distro): base_users = cfg.get("users", []) if isinstance(base_users, (dict, str)): - util.deprecate( + lifecycle.deprecate( deprecated=f"'users' of type {type(base_users)}", deprecated_version="22.2", extra_message="Use 'users' as a list.", diff --git a/cloudinit/features.py b/cloudinit/features.py index c3fdae18658..4f9a59e9925 100644 --- a/cloudinit/features.py +++ b/cloudinit/features.py @@ -107,6 +107,11 @@ the different log levels is that logs at DEPRECATED level result in a return code of 2 from `cloud-init status`. +This may may also be used in some limited cases where new error messages may be +logged which increase the risk of regression in stable downstreams where the +error was previously unreported yet downstream users expected stable behavior +across new cloud-init releases. + format: :: = | diff --git a/cloudinit/lifecycle.py b/cloudinit/lifecycle.py new file mode 100644 index 00000000000..871333ef6fb --- /dev/null +++ b/cloudinit/lifecycle.py @@ -0,0 +1,242 @@ +# This file is part of cloud-init. See LICENSE file for license information. +import collections +import functools +import logging +from typing import NamedTuple, Optional + +from cloudinit import features, log + +LOG = logging.getLogger(__name__) + + +class DeprecationLog(NamedTuple): + log_level: int + message: str + + +@functools.total_ordering +class Version( + collections.namedtuple("Version", ["major", "minor", "patch", "rev"]) +): + """A class for comparing versions. + + Implemented as a named tuple with all ordering methods. Comparisons + between X.Y.N and X.Y always treats the more specific number as larger. + + :param major: the most significant number in a version + :param minor: next greatest significant number after major + :param patch: next greatest significant number after minor + :param rev: the least significant number in a version + + :raises TypeError: If invalid arguments are given. + :raises ValueError: If invalid arguments are given. + + Examples: + >>> Version(2, 9) == Version.from_str("2.9") + True + >>> Version(2, 9, 1) > Version.from_str("2.9.1") + False + >>> Version(3, 10) > Version.from_str("3.9.9.9") + True + >>> Version(3, 7) >= Version.from_str("3.7") + True + + """ + + def __new__( + cls, major: int = -1, minor: int = -1, patch: int = -1, rev: int = -1 + ) -> "Version": + """Default of -1 allows us to tiebreak in favor of the most specific + number""" + return super(Version, cls).__new__(cls, major, minor, patch, rev) + + @classmethod + def from_str(cls, version: str) -> "Version": + """Create a Version object from a string. + + :param version: A period-delimited version string, max 4 segments. + + :raises TypeError: Raised if invalid arguments are given. + :raises ValueError: Raised if invalid arguments are given. + + :return: A Version object. + """ + return cls(*(list(map(int, version.split("."))))) + + def __gt__(self, other): + return 1 == self._compare_version(other) + + def __eq__(self, other): + return ( + self.major == other.major + and self.minor == other.minor + and self.patch == other.patch + and self.rev == other.rev + ) + + def __iter__(self): + """Iterate over the version (drop sentinels)""" + for n in (self.major, self.minor, self.patch, self.rev): + if n != -1: + yield str(n) + else: + break + + def __str__(self): + return ".".join(self) + + def __hash__(self): + return hash(str(self)) + + def _compare_version(self, other: "Version") -> int: + """Compare this Version to another. + + :param other: A Version object. + + :return: -1 if self > other, 1 if self < other, else 0 + """ + if self == other: + return 0 + if self.major > other.major: + return 1 + if self.minor > other.minor: + return 1 + if self.patch > other.patch: + return 1 + if self.rev > other.rev: + return 1 + return -1 + + +def should_log_deprecation(version: str, boundary_version: str) -> bool: + """Determine if a deprecation message should be logged. + + :param version: The version in which the thing was deprecated. + :param boundary_version: The version at which deprecation level is logged. + + :return: True if the message should be logged, else False. + """ + return boundary_version == "devel" or Version.from_str( + version + ) <= Version.from_str(boundary_version) + + +def log_with_downgradable_level( + *, + logger: logging.Logger, + version: str, + requested_level: int, + msg: str, + args, +): + """Log a message at the requested level, if that is acceptable. + + If the log level is too high due to the version boundary, log at DEBUG + level. Useful to add new warnings to previously unguarded code without + disrupting stable downstreams. + + :param logger: Logger object to log with + :param version: Version string of the version that this log was introduced + :param level: Preferred level at which this message should be logged + :param msg: Message, as passed to the logger. + :param args: Message formatting args, ass passed to the logger + + :return: True if the message should be logged, else False. + """ + if should_log_deprecation(version, features.DEPRECATION_INFO_BOUNDARY): + logger.log(requested_level, msg, args) + else: + logger.debug(msg, args) + + +def deprecate( + *, + deprecated: str, + deprecated_version: str, + extra_message: Optional[str] = None, + schedule: int = 5, + skip_log: bool = False, +) -> DeprecationLog: + """Mark a "thing" as deprecated. Deduplicated deprecations are + logged. + + :param deprecated: Noun to be deprecated. Write this as the start + of a sentence, with no period. Version and extra message will + be appended. + :param deprecated_version: The version in which the thing was + deprecated + :param extra_message: A remedy for the user's problem. A good + message will be actionable and specific (i.e., don't use a + generic "Use updated key." if the user used a deprecated key). + End the string with a period. + :param schedule: Manually set the deprecation schedule. Defaults to + 5 years. Leave a comment explaining your reason for deviation if + setting this value. + :param skip_log: Return log text rather than logging it. Useful for + running prior to logging setup. + :return: NamedTuple containing log level and log message + DeprecationLog(level: int, message: str) + + Note: uses keyword-only arguments to improve legibility + """ + if not hasattr(deprecate, "log"): + setattr(deprecate, "log", set()) + message = extra_message or "" + dedup = hash(deprecated + message + deprecated_version + str(schedule)) + version = Version.from_str(deprecated_version) + version_removed = Version(version.major + schedule, version.minor) + deprecate_msg = ( + f"{deprecated} is deprecated in " + f"{deprecated_version} and scheduled to be removed in " + f"{version_removed}. {message}" + ).rstrip() + if not should_log_deprecation( + deprecated_version, features.DEPRECATION_INFO_BOUNDARY + ): + level = logging.INFO + elif hasattr(LOG, "deprecated"): + level = log.DEPRECATED + else: + level = logging.WARN + log_cache = getattr(deprecate, "log") + if not skip_log and dedup not in log_cache: + log_cache.add(dedup) + LOG.log(level, deprecate_msg) + return DeprecationLog(level, deprecate_msg) + + +def deprecate_call( + *, deprecated_version: str, extra_message: str, schedule: int = 5 +): + """Mark a "thing" as deprecated. Deduplicated deprecations are + logged. + + :param deprecated_version: The version in which the thing was + deprecated + :param extra_message: A remedy for the user's problem. A good + message will be actionable and specific (i.e., don't use a + generic "Use updated key." if the user used a deprecated key). + End the string with a period. + :param schedule: Manually set the deprecation schedule. Defaults to + 5 years. Leave a comment explaining your reason for deviation if + setting this value. + + Note: uses keyword-only arguments to improve legibility + """ + + def wrapper(func): + @functools.wraps(func) + def decorator(*args, **kwargs): + # don't log message multiple times + out = func(*args, **kwargs) + deprecate( + deprecated_version=deprecated_version, + deprecated=func.__name__, + extra_message=extra_message, + schedule=schedule, + ) + return out + + return decorator + + return wrapper diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py index 9f34467be78..25471dc172c 100644 --- a/cloudinit/net/network_state.py +++ b/cloudinit/net/network_state.py @@ -9,7 +9,7 @@ import logging from typing import TYPE_CHECKING, Any, Dict, Optional -from cloudinit import safeyaml, util +from cloudinit import lifecycle, safeyaml, util from cloudinit.net import ( find_interface_name_from_mac, get_interfaces_by_mac, @@ -86,7 +86,7 @@ def warn_deprecated_all_devices(dikt: dict) -> None: """Warn about deprecations of v2 properties for all devices""" if "gateway4" in dikt or "gateway6" in dikt: - util.deprecate( + lifecycle.deprecate( deprecated="The use of `gateway4` and `gateway6`", deprecated_version="22.4", extra_message="For more info check out: " diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 8b3db620018..6aee531638d 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -15,7 +15,7 @@ from ipaddress import IPv4Network from typing import Dict, List, Union -from cloudinit import subp, util +from cloudinit import lifecycle, subp, util from cloudinit.net.network_state import net_prefix_to_ipv4_mask from cloudinit.simpletable import SimpleTable @@ -95,7 +95,7 @@ def _netdev_info_iproute_json(ipaddr_json): return devs -@util.deprecate_call( +@lifecycle.deprecate_call( deprecated_version="22.1", extra_message="Required by old iproute2 versions that don't " "support ip json output. Consider upgrading to a more recent version.", diff --git a/cloudinit/sources/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py index d5db34cd1d7..5ca6c27d176 100644 --- a/cloudinit/sources/DataSourceConfigDrive.py +++ b/cloudinit/sources/DataSourceConfigDrive.py @@ -9,7 +9,7 @@ import logging import os -from cloudinit import sources, subp, util +from cloudinit import lifecycle, sources, subp, util from cloudinit.event import EventScope, EventType from cloudinit.net import eni from cloudinit.sources.DataSourceIBMCloud import get_ibm_platform @@ -176,7 +176,7 @@ def network_config(self): elif self.network_eni is not None: self._network_config = eni.convert_eni_data(self.network_eni) LOG.debug("network config provided via converted eni data") - util.deprecate( + lifecycle.deprecate( deprecated="Eni network configuration in ConfigDrive", deprecated_version="24.3", extra_message=( diff --git a/cloudinit/sources/DataSourceDigitalOcean.py b/cloudinit/sources/DataSourceDigitalOcean.py index 951006ed815..ec35af782f0 100644 --- a/cloudinit/sources/DataSourceDigitalOcean.py +++ b/cloudinit/sources/DataSourceDigitalOcean.py @@ -9,7 +9,7 @@ import logging import cloudinit.sources.helpers.digitalocean as do_helper -from cloudinit import sources, util +from cloudinit import lifecycle, sources, util LOG = logging.getLogger(__name__) @@ -55,7 +55,7 @@ def _unpickle(self, ci_pkl_version: int) -> None: self._deprecate() def _deprecate(self): - util.deprecate( + lifecycle.deprecate( deprecated="DataSourceDigitalOcean", deprecated_version="23.2", extra_message="Deprecated in favour of DataSourceConfigDrive.", diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py index 0bf6e7c4ee2..289205e8599 100644 --- a/cloudinit/sources/DataSourceNoCloud.py +++ b/cloudinit/sources/DataSourceNoCloud.py @@ -13,7 +13,7 @@ import os from functools import partial -from cloudinit import dmi, sources, util +from cloudinit import dmi, lifecycle, sources, util from cloudinit.net import eni LOG = logging.getLogger(__name__) @@ -131,7 +131,7 @@ def _pp2d_callback(mp, data): label = self.ds_cfg.get("fs_label", "cidata") if label is not None: if label.lower() != "cidata": - util.deprecate( + lifecycle.deprecate( deprecated="Custom fs_label keys", deprecated_version="24.3", extra_message="This key isn't supported by ds-identify.", @@ -272,7 +272,7 @@ def check_instance_id(self, sys_cfg): def network_config(self): if self._network_config is None: if self._network_eni is not None: - util.deprecate( + lifecycle.deprecate( deprecated="Eni network configuration in NoCloud", deprecated_version="24.3", extra_message=( @@ -424,7 +424,7 @@ def ds_detect(self): For backwards compatiblity, check for that dsname. """ log_deprecated = partial( - util.deprecate, + lifecycle.deprecate, deprecated="The 'nocloud-net' datasource name", deprecated_version="24.1", extra_message=( diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py index eb39ddc7bb3..87b49fcaecc 100644 --- a/cloudinit/sources/__init__.py +++ b/cloudinit/sources/__init__.py @@ -19,7 +19,7 @@ from enum import Enum, unique from typing import Any, Dict, List, Optional, Tuple, Union -from cloudinit import atomic_helper, dmi, importer, net, type_utils +from cloudinit import atomic_helper, dmi, importer, lifecycle, net, type_utils from cloudinit import user_data as ud from cloudinit import util from cloudinit.atomic_helper import write_json @@ -1230,7 +1230,7 @@ def parse_cmdline_or_dmi(input: str) -> str: deprecated = ds_parse_1 or ds_parse_2 if deprecated: dsname = deprecated.group(1).strip() - util.deprecate( + lifecycle.deprecate( deprecated=( f"Defining the datasource on the command line using " f"ci.ds={dsname} or " diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py index cad85d596b8..70002086738 100644 --- a/cloudinit/ssh_util.py +++ b/cloudinit/ssh_util.py @@ -12,7 +12,7 @@ from contextlib import suppress from typing import List, Sequence, Tuple -from cloudinit import subp, util +from cloudinit import lifecycle, subp, util LOG = logging.getLogger(__name__) @@ -671,7 +671,7 @@ def get_opensshd_upstream_version(): upstream_version = "9.0" full_version = get_opensshd_version() if full_version is None: - return util.Version.from_str(upstream_version) + return lifecycle.Version.from_str(upstream_version) if "p" in full_version: upstream_version = full_version[: full_version.find("p")] elif " " in full_version: @@ -679,7 +679,7 @@ def get_opensshd_upstream_version(): else: upstream_version = full_version try: - upstream_version = util.Version.from_str(upstream_version) + upstream_version = lifecycle.Version.from_str(upstream_version) return upstream_version except (ValueError, TypeError): LOG.warning("Could not parse sshd version: %s", upstream_version) diff --git a/cloudinit/stages.py b/cloudinit/stages.py index d564cbbc289..1d911aaf3ac 100644 --- a/cloudinit/stages.py +++ b/cloudinit/stages.py @@ -22,6 +22,7 @@ handlers, helpers, importer, + lifecycle, net, sources, type_utils, @@ -914,7 +915,7 @@ def _consume_vendordata(self, vendor_source, frequency=PER_INSTANCE): return if isinstance(enabled, str): - util.deprecate( + lifecycle.deprecate( deprecated=f"Use of string '{enabled}' for " "'vendor_data:enabled' field", deprecated_version="23.1", diff --git a/cloudinit/util.py b/cloudinit/util.py index faa3e847b84..87b8aa071dc 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -12,7 +12,6 @@ import contextlib import copy as obj_copy import email -import functools import glob import grp import gzip @@ -38,7 +37,7 @@ from collections import deque, namedtuple from contextlib import contextmanager, suppress from errno import ENOENT -from functools import lru_cache, total_ordering +from functools import lru_cache from pathlib import Path from types import ModuleType from typing import ( @@ -51,7 +50,6 @@ Generator, List, Mapping, - NamedTuple, Optional, Sequence, TypeVar, @@ -65,7 +63,6 @@ from cloudinit import ( features, importer, - log, mergers, net, settings, @@ -94,11 +91,6 @@ FALSE_STRINGS = ("off", "0", "no", "false") -class DeprecationLog(NamedTuple): - log_level: int - message: str - - def kernel_version(): return tuple(map(int, os.uname().release.split(".")[:2])) @@ -3143,204 +3135,6 @@ def error(msg, rc=1, fmt="Error:\n{}", sys_exit=False): return rc -@total_ordering -class Version(namedtuple("Version", ["major", "minor", "patch", "rev"])): - """A class for comparing versions. - - Implemented as a named tuple with all ordering methods. Comparisons - between X.Y.N and X.Y always treats the more specific number as larger. - - :param major: the most significant number in a version - :param minor: next greatest significant number after major - :param patch: next greatest significant number after minor - :param rev: the least significant number in a version - - :raises TypeError: If invalid arguments are given. - :raises ValueError: If invalid arguments are given. - - Examples: - >>> Version(2, 9) == Version.from_str("2.9") - True - >>> Version(2, 9, 1) > Version.from_str("2.9.1") - False - >>> Version(3, 10) > Version.from_str("3.9.9.9") - True - >>> Version(3, 7) >= Version.from_str("3.7") - True - - """ - - def __new__( - cls, major: int = -1, minor: int = -1, patch: int = -1, rev: int = -1 - ) -> "Version": - """Default of -1 allows us to tiebreak in favor of the most specific - number""" - return super(Version, cls).__new__(cls, major, minor, patch, rev) - - @classmethod - def from_str(cls, version: str) -> "Version": - """Create a Version object from a string. - - :param version: A period-delimited version string, max 4 segments. - - :raises TypeError: Raised if invalid arguments are given. - :raises ValueError: Raised if invalid arguments are given. - - :return: A Version object. - """ - return cls(*(list(map(int, version.split("."))))) - - def __gt__(self, other): - return 1 == self._compare_version(other) - - def __eq__(self, other): - return ( - self.major == other.major - and self.minor == other.minor - and self.patch == other.patch - and self.rev == other.rev - ) - - def __iter__(self): - """Iterate over the version (drop sentinels)""" - for n in (self.major, self.minor, self.patch, self.rev): - if n != -1: - yield str(n) - else: - break - - def __str__(self): - return ".".join(self) - - def __hash__(self): - return hash(str(self)) - - def _compare_version(self, other: "Version") -> int: - """Compare this Version to another. - - :param other: A Version object. - - :return: -1 if self > other, 1 if self < other, else 0 - """ - if self == other: - return 0 - if self.major > other.major: - return 1 - if self.minor > other.minor: - return 1 - if self.patch > other.patch: - return 1 - if self.rev > other.rev: - return 1 - return -1 - - -def should_log_deprecation(version: str, boundary_version: str) -> bool: - """Determine if a deprecation message should be logged. - - :param version: The version in which the thing was deprecated. - :param boundary_version: The version at which deprecation level is logged. - - :return: True if the message should be logged, else False. - """ - return boundary_version == "devel" or Version.from_str( - version - ) <= Version.from_str(boundary_version) - - -def deprecate( - *, - deprecated: str, - deprecated_version: str, - extra_message: Optional[str] = None, - schedule: int = 5, - skip_log: bool = False, -) -> DeprecationLog: - """Mark a "thing" as deprecated. Deduplicated deprecations are - logged. - - @param deprecated: Noun to be deprecated. Write this as the start - of a sentence, with no period. Version and extra message will - be appended. - @param deprecated_version: The version in which the thing was - deprecated - @param extra_message: A remedy for the user's problem. A good - message will be actionable and specific (i.e., don't use a - generic "Use updated key." if the user used a deprecated key). - End the string with a period. - @param schedule: Manually set the deprecation schedule. Defaults to - 5 years. Leave a comment explaining your reason for deviation if - setting this value. - @param skip_log: Return log text rather than logging it. Useful for - running prior to logging setup. - @return: NamedTuple containing log level and log message - DeprecationLog(level: int, message: str) - - Note: uses keyword-only arguments to improve legibility - """ - if not hasattr(deprecate, "log"): - setattr(deprecate, "log", set()) - message = extra_message or "" - dedup = hash(deprecated + message + deprecated_version + str(schedule)) - version = Version.from_str(deprecated_version) - version_removed = Version(version.major + schedule, version.minor) - deprecate_msg = ( - f"{deprecated} is deprecated in " - f"{deprecated_version} and scheduled to be removed in " - f"{version_removed}. {message}" - ).rstrip() - if not should_log_deprecation( - deprecated_version, features.DEPRECATION_INFO_BOUNDARY - ): - level = logging.INFO - elif hasattr(LOG, "deprecated"): - level = log.DEPRECATED - else: - level = logging.WARN - log_cache = getattr(deprecate, "log") - if not skip_log and dedup not in log_cache: - log_cache.add(dedup) - LOG.log(level, deprecate_msg) - return DeprecationLog(level, deprecate_msg) - - -def deprecate_call( - *, deprecated_version: str, extra_message: str, schedule: int = 5 -): - """Mark a "thing" as deprecated. Deduplicated deprecations are - logged. - - @param deprecated_version: The version in which the thing was - deprecated - @param extra_message: A remedy for the user's problem. A good - message will be actionable and specific (i.e., don't use a - generic "Use updated key." if the user used a deprecated key). - End the string with a period. - @param schedule: Manually set the deprecation schedule. Defaults to - 5 years. Leave a comment explaining your reason for deviation if - setting this value. - - Note: uses keyword-only arguments to improve legibility - """ - - def wrapper(func): - @functools.wraps(func) - def decorator(*args, **kwargs): - # don't log message multiple times - out = func(*args, **kwargs) - deprecate( - deprecated_version=deprecated_version, - deprecated=func.__name__, - extra_message=extra_message, - schedule=schedule, - ) - return out - - return decorator - - return wrapper - - def read_hotplug_enabled_file(paths: "Paths") -> dict: content: dict = {"scopes": []} try: diff --git a/tests/integration_tests/cmd/test_schema.py b/tests/integration_tests/cmd/test_schema.py index 3155a07919b..c954484012a 100644 --- a/tests/integration_tests/cmd/test_schema.py +++ b/tests/integration_tests/cmd/test_schema.py @@ -3,7 +3,7 @@ import pytest -from cloudinit.util import should_log_deprecation +from cloudinit import lifecycle from tests.integration_tests.instances import IntegrationInstance from tests.integration_tests.releases import CURRENT_RELEASE, MANTIC from tests.integration_tests.util import ( @@ -71,7 +71,7 @@ def test_clean_log(self, class_client: IntegrationInstance): ) # the deprecation_version is 22.2 in schema for apt_* keys in # user-data. Pass 22.2 in against the client's version_boundary. - if should_log_deprecation("22.2", version_boundary): + if lifecycle.should_log_deprecation("22.2", version_boundary): log_level = "DEPRECATED" else: log_level = "INFO" diff --git a/tests/integration_tests/datasources/test_nocloud.py b/tests/integration_tests/datasources/test_nocloud.py index 6cfe037a448..24aecc0bd8d 100644 --- a/tests/integration_tests/datasources/test_nocloud.py +++ b/tests/integration_tests/datasources/test_nocloud.py @@ -5,8 +5,8 @@ import pytest from pycloudlib.lxd.instance import LXDInstance +from cloudinit import lifecycle from cloudinit.subp import subp -from cloudinit.util import should_log_deprecation from tests.integration_tests.instances import IntegrationInstance from tests.integration_tests.integration_settings import PLATFORM from tests.integration_tests.releases import CURRENT_RELEASE, FOCAL @@ -199,7 +199,7 @@ def test_smbios_seed_network(self, client: IntegrationInstance): client, "DEPRECATION_INFO_BOUNDARY" ) # nocloud-net deprecated in version 24.1 - if should_log_deprecation("24.1", version_boundary): + if lifecycle.should_log_deprecation("24.1", version_boundary): log_level = "DEPRECATED" else: log_level = "INFO" diff --git a/tests/integration_tests/modules/test_combined.py b/tests/integration_tests/modules/test_combined.py index 0bf1b3d49e8..2d8b51ee362 100644 --- a/tests/integration_tests/modules/test_combined.py +++ b/tests/integration_tests/modules/test_combined.py @@ -17,7 +17,8 @@ from pycloudlib.gce.instance import GceInstance import cloudinit.config -from cloudinit.util import is_true, should_log_deprecation +from cloudinit import lifecycle +from cloudinit.util import is_true from tests.integration_tests.decorators import retry from tests.integration_tests.instances import IntegrationInstance from tests.integration_tests.integration_settings import PLATFORM @@ -138,7 +139,7 @@ def test_deprecated_message(self, class_client: IntegrationInstance): ) # the changed_version is 22.2 in schema for user.sudo key in # user-data. Pass 22.2 in against the client's version_boundary. - if should_log_deprecation("22.2", version_boundary): + if lifecycle.should_log_deprecation("22.2", version_boundary): log_level = "DEPRECATED" deprecation_count = 2 else: diff --git a/tests/integration_tests/modules/test_ubuntu_pro.py b/tests/integration_tests/modules/test_ubuntu_pro.py index f4438163425..0f0cb944aec 100644 --- a/tests/integration_tests/modules/test_ubuntu_pro.py +++ b/tests/integration_tests/modules/test_ubuntu_pro.py @@ -5,7 +5,7 @@ import pytest from pycloudlib.cloud import ImageType -from cloudinit.util import should_log_deprecation +from cloudinit import lifecycle from tests.integration_tests.clouds import IntegrationCloud from tests.integration_tests.conftest import get_validated_source from tests.integration_tests.instances import ( @@ -143,7 +143,7 @@ def test_valid_token(self, client: IntegrationInstance): client, "DEPRECATION_INFO_BOUNDARY" ) # ubuntu_advantage key is deprecated in version 24.1 - if should_log_deprecation("24.1", version_boundary): + if lifecycle.should_log_deprecation("24.1", version_boundary): log_level = "DEPRECATED" else: log_level = "INFO" diff --git a/tests/unittests/config/test_cc_ansible.py b/tests/unittests/config/test_cc_ansible.py index 271d9d037ec..b5b25a64286 100644 --- a/tests/unittests/config/test_cc_ansible.py +++ b/tests/unittests/config/test_cc_ansible.py @@ -7,7 +7,7 @@ from pytest import mark, param, raises -from cloudinit import util +from cloudinit import lifecycle from cloudinit.config import cc_ansible from cloudinit.config.schema import ( SchemaValidationError, @@ -292,7 +292,7 @@ def test_required_keys(self, cfg, exception, mocker): mocker.patch(M_PATH + "AnsiblePull.check_deps") mocker.patch( M_PATH + "AnsiblePull.get_version", - return_value=cc_ansible.Version(2, 7, 1), + return_value=cc_ansible.lifecycle.Version(2, 7, 1), ) mocker.patch( M_PATH + "AnsiblePullDistro.is_installed", @@ -415,7 +415,7 @@ def test_parse_version_distro(self, m_subp): """Verify that the expected version is returned""" assert cc_ansible.AnsiblePullDistro( get_cloud().distro - ).get_version() == util.Version(2, 10, 8) + ).get_version() == lifecycle.Version(2, 10, 8) @mock.patch("cloudinit.subp.subp", side_effect=[(pip_version, "")]) def test_parse_version_pip(self, m_subp): @@ -424,7 +424,7 @@ def test_parse_version_pip(self, m_subp): distro.do_as = MagicMock(return_value=(pip_version, "")) pip = cc_ansible.AnsiblePullPip(distro, "root") received = pip.get_version() - expected = util.Version(2, 13, 2) + expected = lifecycle.Version(2, 13, 2) assert received == expected @mock.patch(M_PATH + "subp.subp", return_value=("stdout", "stderr")) diff --git a/tests/unittests/config/test_cc_ssh.py b/tests/unittests/config/test_cc_ssh.py index 49327bb67e5..a49fbf01baf 100644 --- a/tests/unittests/config/test_cc_ssh.py +++ b/tests/unittests/config/test_cc_ssh.py @@ -7,7 +7,7 @@ import pytest -from cloudinit import ssh_util, util +from cloudinit import lifecycle, ssh_util from cloudinit.config import cc_ssh from cloudinit.config.schema import ( SchemaValidationError, @@ -334,7 +334,7 @@ def test_ssh_hostkey_permissions( Otherwise, 600. """ m_gid.return_value = 10 if ssh_keys_group_exists else -1 - m_sshd_version.return_value = util.Version(sshd_version, 0) + m_sshd_version.return_value = lifecycle.Version(sshd_version, 0) key_path = cc_ssh.KEY_FILE_TPL % "rsa" cloud = get_cloud(distro="centos") cc_ssh.handle("name", {"ssh_genkeytypes": ["rsa"]}, cloud, []) diff --git a/tests/unittests/config/test_cc_ubuntu_pro.py b/tests/unittests/config/test_cc_ubuntu_pro.py index df47e7ae41e..40f8035b30d 100644 --- a/tests/unittests/config/test_cc_ubuntu_pro.py +++ b/tests/unittests/config/test_cc_ubuntu_pro.py @@ -7,7 +7,7 @@ import pytest -from cloudinit import subp +from cloudinit import lifecycle, subp from cloudinit.config.cc_ubuntu_pro import ( _attach, _auto_attach, @@ -23,7 +23,6 @@ get_schema, validate_cloudconfig_schema, ) -from cloudinit.util import Version from tests.unittests.helpers import does_not_raise, mock, skipUnlessJsonSchema from tests.unittests.util import get_cloud @@ -452,8 +451,10 @@ class TestUbuntuProSchema: # we're using a high enough version of jsonschema to not need # to skip this test. JSONSCHEMA_SKIP_REASON - if Version.from_str(getattr(jsonschema, "__version__", "999")) - < Version(4) + if lifecycle.Version.from_str( + getattr(jsonschema, "__version__", "999") + ) + < lifecycle.Version(4) else "", id="deprecation_of_ubuntu_advantage_skip_old_json", ), diff --git a/tests/unittests/conftest.py b/tests/unittests/conftest.py index e0baa63b99b..9401f2235ef 100644 --- a/tests/unittests/conftest.py +++ b/tests/unittests/conftest.py @@ -8,7 +8,7 @@ import pytest -from cloudinit import atomic_helper, log, util +from cloudinit import atomic_helper, lifecycle, log, util from cloudinit.cmd.devel import logs from cloudinit.gpg import GPG from tests.hypothesis import HAS_HYPOTHESIS @@ -152,7 +152,7 @@ def clear_deprecation_log(): # Since deprecations are de-duped, the existance (or non-existance) of # a deprecation warning in a previous test can cause the next test to # fail. - setattr(util.deprecate, "log", set()) + setattr(lifecycle.deprecate, "log", set()) PYTEST_VERSION_TUPLE = tuple(map(int, pytest.__version__.split("."))) diff --git a/tests/unittests/distros/test_create_users.py b/tests/unittests/distros/test_create_users.py index 8fa7f0cc092..ebbbb418e8a 100644 --- a/tests/unittests/distros/test_create_users.py +++ b/tests/unittests/distros/test_create_users.py @@ -4,8 +4,7 @@ import pytest -from cloudinit import distros, features, ssh_util -from cloudinit.util import should_log_deprecation +from cloudinit import distros, features, lifecycle, ssh_util from tests.unittests.helpers import mock from tests.unittests.util import abstract_to_concrete @@ -145,7 +144,7 @@ def test_create_groups_with_dict_deprecated( expected_levels = ( ["WARNING", "DEPRECATED"] - if should_log_deprecation( + if lifecycle.should_log_deprecation( "23.1", features.DEPRECATION_INFO_BOUNDARY ) else ["INFO"] @@ -180,7 +179,7 @@ def test_explicit_sudo_false(self, m_subp, dist, caplog): expected_levels = ( ["WARNING", "DEPRECATED"] - if should_log_deprecation( + if lifecycle.should_log_deprecation( "22.2", features.DEPRECATION_INFO_BOUNDARY ) else ["INFO"] diff --git a/tests/unittests/net/test_network_state.py b/tests/unittests/net/test_network_state.py index eaad90dc8e1..a03f60f86f8 100644 --- a/tests/unittests/net/test_network_state.py +++ b/tests/unittests/net/test_network_state.py @@ -5,7 +5,7 @@ import pytest import yaml -from cloudinit import util +from cloudinit import lifecycle from cloudinit.net import network_state from cloudinit.net.netplan import Renderer as NetplanRenderer from cloudinit.net.renderers import NAME_TO_RENDERER @@ -215,7 +215,7 @@ def test_v2_warns_deprecated_gateways( In netplan targets we perform a passthrough and the warning is not needed. """ - util.deprecate.__dict__["log"] = set() + lifecycle.deprecate.__dict__["log"] = set() ncfg = yaml.safe_load( cfg.format( gateway4="gateway4: 10.54.0.1", diff --git a/tests/unittests/sources/test_digitalocean.py b/tests/unittests/sources/test_digitalocean.py index 34a92453335..c111e710ffc 100644 --- a/tests/unittests/sources/test_digitalocean.py +++ b/tests/unittests/sources/test_digitalocean.py @@ -165,7 +165,7 @@ def test_returns_false_not_on_docean(self, m_read_sysinfo): self.assertTrue(m_read_sysinfo.called) @mock.patch("cloudinit.sources.helpers.digitalocean.read_metadata") - @mock.patch("cloudinit.sources.util.deprecate") + @mock.patch("cloudinit.sources.lifecycle.deprecate") def test_deprecation_log_on_init(self, mock_deprecate, _mock_readmd): ds = self.get_ds() self.assertTrue(ds.get_data()) @@ -176,7 +176,7 @@ def test_deprecation_log_on_init(self, mock_deprecate, _mock_readmd): ) @mock.patch("cloudinit.sources.helpers.digitalocean.read_metadata") - @mock.patch("cloudinit.sources.util.deprecate") + @mock.patch("cloudinit.sources.lifecycle.deprecate") def test_deprecation_log_on_unpick(self, mock_deprecate, _mock_readmd): ds = self.get_ds() self.assertTrue(ds.get_data()) diff --git a/tests/unittests/test_log.py b/tests/unittests/test_log.py index 175afc0eb94..d67c3552157 100644 --- a/tests/unittests/test_log.py +++ b/tests/unittests/test_log.py @@ -10,7 +10,7 @@ import pytest -from cloudinit import log, util +from cloudinit import lifecycle, log, util from cloudinit.analyze.dump import CLOUD_INIT_ASCTIME_FMT from tests.unittests.helpers import CiTestCase @@ -112,7 +112,7 @@ def test_deprecate_log_level_based_on_features( "DEPRECATION_INFO_BOUNDARY", deprecation_info_boundary, ) - util.deprecate( + lifecycle.deprecate( deprecated="some key", deprecated_version="19.2", extra_message="dont use it", @@ -125,17 +125,17 @@ def test_deprecate_log_level_based_on_features( def test_log_deduplication(self, caplog): log.define_extra_loggers() - util.deprecate( + lifecycle.deprecate( deprecated="stuff", deprecated_version="19.1", extra_message=":)", ) - util.deprecate( + lifecycle.deprecate( deprecated="stuff", deprecated_version="19.1", extra_message=":)", ) - util.deprecate( + lifecycle.deprecate( deprecated="stuff", deprecated_version="19.1", extra_message=":)", diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py index 2ceed7aa32c..c856f97564f 100644 --- a/tests/unittests/test_util.py +++ b/tests/unittests/test_util.py @@ -22,7 +22,15 @@ import pytest import yaml -from cloudinit import atomic_helper, features, importer, subp, url_helper, util +from cloudinit import ( + atomic_helper, + features, + importer, + lifecycle, + subp, + url_helper, + util, +) from cloudinit.distros import Distro from cloudinit.helpers import Paths from cloudinit.sources import DataSourceHostname @@ -3095,9 +3103,13 @@ class TestVersion: ) def test_eq(self, v1, v2, eq): if eq: - assert util.Version.from_str(v1) == util.Version.from_str(v2) + assert lifecycle.Version.from_str( + v1 + ) == lifecycle.Version.from_str(v2) if not eq: - assert util.Version.from_str(v1) != util.Version.from_str(v2) + assert lifecycle.Version.from_str( + v1 + ) != lifecycle.Version.from_str(v2) @pytest.mark.parametrize( ("v1", "v2", "gt"), @@ -3111,11 +3123,15 @@ def test_eq(self, v1, v2, eq): ) def test_gt(self, v1, v2, gt): if gt: - assert util.Version.from_str(v1) > util.Version.from_str(v2) + assert lifecycle.Version.from_str(v1) > lifecycle.Version.from_str( + v2 + ) if not gt: - assert util.Version.from_str(v1) < util.Version.from_str( + assert lifecycle.Version.from_str(v1) < lifecycle.Version.from_str( v2 - ) or util.Version.from_str(v1) == util.Version.from_str(v2) + ) or lifecycle.Version.from_str(v1) == lifecycle.Version.from_str( + v2 + ) @pytest.mark.parametrize( ("version"), @@ -3129,31 +3145,31 @@ def test_gt(self, v1, v2, gt): ) def test_to_version_and_back_to_str(self, version): """Verify __str__, __iter__, and Version.from_str()""" - assert version == str(util.Version.from_str(version)) + assert version == str(lifecycle.Version.from_str(version)) @pytest.mark.parametrize( ("str_ver", "cls_ver"), ( ( "0.0.0.0", - util.Version(0, 0, 0, 0), + lifecycle.Version(0, 0, 0, 0), ), ( "1.0.0.0", - util.Version(1, 0, 0, 0), + lifecycle.Version(1, 0, 0, 0), ), ( "1.0.2.0", - util.Version(1, 0, 2, 0), + lifecycle.Version(1, 0, 2, 0), ), ( "9.8.2.0", - util.Version(9, 8, 2, 0), + lifecycle.Version(9, 8, 2, 0), ), ), ) def test_from_str(self, str_ver, cls_ver): - assert util.Version.from_str(str_ver) == cls_ver + assert lifecycle.Version.from_str(str_ver) == cls_ver @pytest.mark.allow_dns_lookup