diff options
author | Brett Holman <brett.holman@canonical.com> | 2023-02-24 08:00:44 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-24 08:00:44 -0700 |
commit | 6100fda632f63605981636c0900e1e0b8b354979 (patch) | |
tree | dab05196ac0cc299acd0c672053e3506c0ade213 | |
parent | 46fcd03187d70f405c748f7a6cfdb02ecb8c6ee7 (diff) | |
download | cloud-init-git-6100fda632f63605981636c0900e1e0b8b354979.tar.gz |
log: Add standardized deprecation tooling (SC-1312) (#2026)
- Add deprecation log level
- Add deprecation utilities for structured format and messaging
- Update existing deprecation log sites, add deprecated versions
-rwxr-xr-x | cloudinit/cmd/main.py | 10 | ||||
-rw-r--r-- | cloudinit/config/cc_apt_configure.py | 20 | ||||
-rw-r--r-- | cloudinit/config/cc_ca_certs.py | 14 | ||||
-rw-r--r-- | cloudinit/config/cc_growpart.py | 7 | ||||
-rw-r--r-- | cloudinit/config/cc_rsyslog.py | 6 | ||||
-rw-r--r-- | cloudinit/config/cc_set_passwords.py | 22 | ||||
-rw-r--r-- | cloudinit/config/cc_update_etc_hosts.py | 7 | ||||
-rw-r--r-- | cloudinit/config/schema.py | 4 | ||||
-rw-r--r-- | cloudinit/distros/__init__.py | 22 | ||||
-rw-r--r-- | cloudinit/distros/ug_util.py | 15 | ||||
-rw-r--r-- | cloudinit/log.py | 11 | ||||
-rw-r--r-- | cloudinit/net/network_state.py | 9 | ||||
-rw-r--r-- | cloudinit/netinfo.py | 8 | ||||
-rw-r--r-- | cloudinit/stages.py | 9 | ||||
-rw-r--r-- | cloudinit/util.py | 83 | ||||
-rw-r--r-- | doc/rtd/reference/faq.rst | 15 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_combined.py | 12 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_ca_certs.py | 21 | ||||
-rw-r--r-- | tests/unittests/config/test_schema.py | 6 | ||||
-rw-r--r-- | tests/unittests/distros/test_create_users.py | 22 | ||||
-rw-r--r-- | tests/unittests/net/test_network_state.py | 7 | ||||
-rw-r--r-- | tests/unittests/test_log.py | 30 | ||||
-rw-r--r-- | tests/unittests/test_net.py | 3 |
23 files changed, 267 insertions, 96 deletions
diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index f28fda15..f16c59a7 100755 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -219,11 +219,11 @@ def attempt_cmdline_url(path, network=True, cmdline=None) -> Tuple[int, str]: is_cloud_cfg = False if is_cloud_cfg: if cmdline_name == "url": - LOG.warning( - "DEPRECATED: `url` kernel command line key is" - " deprecated for providing cloud-config via URL." - " Please use `cloud-config-url` kernel command line" - " parameter instead" + util.deprecate( + deprecated="The kernel command line key `url`", + deprecated_version="22.3", + extra_message=" Please use `cloud-config-url` " + "kernel command line parameter instead", ) else: if cmdline_name == "cloud-config-url": diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py index 98957f8d..17c2fb58 100644 --- a/cloudinit/config/cc_apt_configure.py +++ b/cloudinit/config/cc_apt_configure.py @@ -614,9 +614,10 @@ def add_apt_sources( def convert_v1_to_v2_apt_format(srclist): """convert v1 apt format to v2 (dict in apt_sources)""" srcdict = {} - LOG.warning( - "DEPRECATION: 'apt_sources' deprecated config key found." - " Use 'apt' instead" + util.deprecate( + deprecated="Config key 'apt_sources'", + deprecated_version="22.1", + extra_message="Use 'apt' instead", ) if isinstance(srclist, list): LOG.debug("apt config: convert V1 to V2 format (source list to dict)") @@ -692,18 +693,17 @@ def convert_v2_to_v3_apt_format(oldcfg): # no old config, so no new one to be created if not needtoconvert: return oldcfg - LOG.warning( - "DEPRECATION apt: converted deprecated config V2 to V3 format for" - " keys '%s'. Use updated config keys.", - ", ".join(needtoconvert), + util.deprecate( + deprecated=f"The following config key(s): {needtoconvert}", + deprecated_version="22.1", ) # if old AND new config are provided, prefer the new one (LP #1616831) newaptcfg = oldcfg.get("apt", None) if newaptcfg is not None: - LOG.warning( - "DEPRECATION: apt config: deprecated V1/2 and V3 format specified," - " preferring V3" + util.deprecate( + deprecated="Support for combined old and new apt module keys", + deprecated_version="22.1", ) for oldkey in needtoconvert: newkey = mapoldkeys[oldkey] diff --git a/cloudinit/config/cc_ca_certs.py b/cloudinit/config/cc_ca_certs.py index 51b8577c..b1c4a2bf 100644 --- a/cloudinit/config/cc_ca_certs.py +++ b/cloudinit/config/cc_ca_certs.py @@ -229,9 +229,10 @@ def handle( @param args: Any module arguments from cloud.cfg """ if "ca-certs" in cfg: - LOG.warning( - "DEPRECATION: key 'ca-certs' is now deprecated. Use 'ca_certs'" - " instead." + util.deprecate( + deprecated="Key 'ca-certs'", + deprecated_version="22.1", + extra_message="Use 'ca_certs' instead.", ) elif "ca_certs" not in cfg: LOG.debug( @@ -251,9 +252,10 @@ def handle( # 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: - LOG.warning( - "DEPRECATION: key 'ca-certs.remove-defaults' is now deprecated." - " Use 'ca_certs.remove_defaults' instead." + util.deprecate( + deprecated="Key 'remove-defaults'", + deprecated_version="22.1", + extra_message="Use 'remove_defaults' instead.", ) if ca_cert_cfg.get( "remove_defaults", ca_cert_cfg.get("remove-defaults", False) diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py index f3acbe2a..2a496820 100644 --- a/cloudinit/config/cc_growpart.py +++ b/cloudinit/config/cc_growpart.py @@ -583,9 +583,10 @@ def handle( mode = mycfg.get("mode", "auto") if util.is_false(mode): if mode != "off": - log.warning( - f"DEPRECATED: growpart mode '{mode}' is deprecated. " - "Use 'off' instead." + util.deprecate( + deprecated="Growpart's 'mode' key with value '{mode}'", + deprecated_version="22.2", + extra_message="Use 'off' instead.", ) log.debug("growpart disabled: mode=%s" % mode) return diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py index 9baaf094..47ade927 100644 --- a/cloudinit/config/cc_rsyslog.py +++ b/cloudinit/config/cc_rsyslog.py @@ -107,9 +107,9 @@ def load_config(cfg: dict) -> dict: mycfg = cfg.get("rsyslog", {}) if isinstance(cfg.get("rsyslog"), list): - LOG.warning( - "DEPRECATION: This rsyslog list format is deprecated and will be " - "removed in a future version of cloud-init. Use documented keys." + util.deprecate( + deprecated="The rsyslog key with value of type 'list'", + deprecated_version="22.2", ) mycfg = {KEYNAME_CONFIGS: cfg.get("rsyslog")} if KEYNAME_LEGACY_FILENAME in cfg: diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py index 3a0b3f5b..bd27d533 100644 --- a/cloudinit/config/cc_set_passwords.py +++ b/cloudinit/config/cc_set_passwords.py @@ -133,10 +133,10 @@ def handle_ssh_pwauth(pw_auth, distro: Distro): cfg_name = "PasswordAuthentication" if isinstance(pw_auth, str): - LOG.warning( - "DEPRECATION: The 'ssh_pwauth' config key should be set to " - "a boolean value. The string format is deprecated and will be " - "removed in a future version of cloud-init." + util.deprecate( + deprecated="Using a string value for the 'ssh_pwauth' key", + deprecated_version="22.2", + extra_message="Use a boolean value with 'ssh_pwauth'.", ) if util.is_true(pw_auth): cfg_val = "yes" @@ -192,17 +192,19 @@ def handle( chfg = cfg["chpasswd"] users_list = util.get_cfg_option_list(chfg, "users", default=[]) if "list" in chfg and chfg["list"]: - log.warning( - "DEPRECATION: key 'lists' is now deprecated. Use 'users'." + util.deprecate( + deprecated="Config key 'lists'", + deprecated_version="22.3", + extra_message="Use 'users' instead.", ) if isinstance(chfg["list"], list): log.debug("Handling input for chpasswd as list.") plist = util.get_cfg_option_list(chfg, "list", plist) else: - log.warning( - "DEPRECATION: The chpasswd multiline string format is " - "deprecated and will be removed from a future version of " - "cloud-init. Use the list format instead." + util.deprecate( + deprecated="The chpasswd multiline string", + deprecated_version="22.2", + extra_message="Use string type instead.", ) log.debug("Handling input for chpasswd as multiline string.") multiline = util.get_cfg_option_str(chfg, "list") diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py index 7bb9dff0..695bc019 100644 --- a/cloudinit/config/cc_update_etc_hosts.py +++ b/cloudinit/config/cc_update_etc_hosts.py @@ -106,9 +106,10 @@ def handle( if util.translate_bool(manage_hosts, addons=["template"]): if manage_hosts == "template": - log.warning( - "DEPRECATED: please use manage_etc_hosts: true instead of" - " 'template'" + util.deprecate( + deprecated="Value 'template' for key 'manage_etc_hosts'", + deprecated_version="22.2", + extra_message="Use 'true' instead.", ) (hostname, fqdn, _) = util.get_hostname_fqdn(cfg, cloud) if not hostname: diff --git a/cloudinit/config/schema.py b/cloudinit/config/schema.py index b0b5fccf..9bccdcec 100644 --- a/cloudinit/config/schema.py +++ b/cloudinit/config/schema.py @@ -476,7 +476,9 @@ def validate_cloudconfig_schema( prefix="Deprecated cloud-config provided:\n", separator="\n", ) - LOG.warning(message) + # This warning doesn't fit the standardized util.deprecated() utility + # format, but it is a deprecation log, so log it directly. + LOG.deprecated(message) # type: ignore if strict and (errors or deprecations): raise SchemaValidationError(errors, deprecations) if errors: diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 940b689e..40789297 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -549,12 +549,12 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): groups = groups.split(",") if isinstance(groups, dict): - LOG.warning( - "DEPRECATED: The user %s has a 'groups' config value of" - " type dict which is deprecated and will be removed in a" - " future version of cloud-init. Use a comma-delimited" - " string or array instead: group1,group2.", - name, + util.deprecate( + deprecated=f"The user {name} has a 'groups' config value " + "of type dict", + deprecated_version="22.3", + extra_message="Use a comma-delimited string or " + "array instead: group1,group2.", ) # remove any white spaces in group names, most likely @@ -682,11 +682,11 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): if kwargs["sudo"]: self.write_sudo_rules(name, kwargs["sudo"]) elif kwargs["sudo"] is False: - LOG.warning( - "DEPRECATED: The user %s has a 'sudo' config value of" - " 'false' which will be dropped after April 2027." - " Use 'null' instead.", - name, + util.deprecate( + deprecated=f"The value of 'false' in user {name}'s " + "'sudo' config", + deprecated_version="22.3", + extra_message="Use 'null' instead.", ) # Import SSH keys diff --git a/cloudinit/distros/ug_util.py b/cloudinit/distros/ug_util.py index e0a4d068..2697527d 100644 --- a/cloudinit/distros/ug_util.py +++ b/cloudinit/distros/ug_util.py @@ -174,9 +174,10 @@ 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} - LOG.warning( - "DEPRECATED: 'user' of type string is deprecated and will" - " be removed in a future release. Use 'users' list instead." + util.deprecate( + deprecated="'user' of type string", + deprecated_version="22.2", + extra_message="Use 'users' list instead.", ) elif not isinstance(old_user, dict): LOG.warning( @@ -206,10 +207,10 @@ def normalize_users_groups(cfg, distro): base_users = cfg.get("users", []) if isinstance(base_users, (dict, str)): - LOG.warning( - "DEPRECATED: 'users' of type %s is deprecated and will be removed" - " in a future release. Use 'users' as a list.", - type(base_users), + util.deprecate( + deprecated=f"'users' of type {type(base_users)}", + deprecated_version="22.2", + extra_message="Use 'users' as a list.", ) elif not isinstance(base_users, (list)): LOG.warning( diff --git a/cloudinit/log.py b/cloudinit/log.py index f40201bb..d9912a50 100644 --- a/cloudinit/log.py +++ b/cloudinit/log.py @@ -63,6 +63,16 @@ def flushLoggers(root): flushLoggers(root.parent) +def defineDeprecationLogger(lvl=35): + logging.addLevelName(lvl, "DEPRECATED") + + def deprecated(self, message, *args, **kwargs): + if self.isEnabledFor(lvl): + self._log(lvl, message, args, **kwargs) + + logging.Logger.deprecated = deprecated + + def setupLogging(cfg=None): # See if the config provides any logging conf... if not cfg: @@ -83,6 +93,7 @@ def setupLogging(cfg=None): log_cfgs.append("\n".join(cfg_str)) else: log_cfgs.append(str(a_cfg)) + defineDeprecationLogger() # See if any of them actually load... am_tried = 0 diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py index f88b1321..790398bc 100644 --- a/cloudinit/net/network_state.py +++ b/cloudinit/net/network_state.py @@ -86,10 +86,11 @@ NET_CONFIG_TO_V2: Dict[str, Dict[str, Any]] = { 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: - LOG.warning( - "DEPRECATED: The use of `gateway4` and `gateway6` is" - " deprecated. For more info check out: " - "https://cloudinit.readthedocs.io/en/latest/topics/network-config-format-v2.html" # noqa: E501 + util.deprecate( + deprecated="The use of `gateway4` and `gateway6`", + deprecated_version="22.4", + extra_message="For more info check out: " + "https://cloudinit.readthedocs.io/en/latest/topics/network-config-format-v2.html", # noqa: E501 ) diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 5eeeb967..a3bca86e 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -94,11 +94,13 @@ def _netdev_info_iproute_json(ipaddr_json): return devs +@util.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.", +) def _netdev_info_iproute(ipaddr_out): """ - DEPRECATED: Only used on distros that don't support ip json output - Use _netdev_info_iproute_json() when possible. - @param ipaddr_out: Output string from 'ip addr show' command. @returns: A dict of device info keyed by network device name containing diff --git a/cloudinit/stages.py b/cloudinit/stages.py index 9494a0bf..a063e778 100644 --- a/cloudinit/stages.py +++ b/cloudinit/stages.py @@ -758,10 +758,11 @@ class Init: return if isinstance(enabled, str): - LOG.debug( - "Use of string '%s' for 'vendor_data:enabled' field " - "is deprecated. Use boolean value instead", - enabled, + util.deprecate( + deprecated=f"Use of string '{enabled}' for " + "'vendor_data:enabled' field", + deprecated_version="23.1", + extra_message="Use boolean value instead.", ) LOG.debug( diff --git a/cloudinit/util.py b/cloudinit/util.py index 8ba3e2b6..aed332ec 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -11,6 +11,7 @@ import contextlib import copy as obj_copy import email +import functools import glob import grp import gzip @@ -36,7 +37,7 @@ from collections import deque, namedtuple from errno import EACCES, ENOENT from functools import lru_cache, total_ordering from pathlib import Path -from typing import Callable, Deque, Dict, List, TypeVar +from typing import Callable, Deque, Dict, List, Optional, TypeVar from urllib import parse from cloudinit import features, importer @@ -3097,3 +3098,83 @@ class Version(namedtuple("Version", ["major", "minor", "patch", "rev"])): if self.rev > other.rev: return 1 return -1 + + +def deprecate( + *, + deprecated: str, + deprecated_version: str, + extra_message: Optional[str] = None, + schedule: int = 5, +): + """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. + + Note: uses keyword-only arguments to improve legibility + """ + if not hasattr(deprecate, "_log"): + deprecate._log = set() # type: ignore + 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) + if dedup not in deprecate._log: # type: ignore + deprecate._log.add(dedup) # type: ignore + deprecate_msg = ( + f"{deprecated} is deprecated in " + f"{deprecated_version} and scheduled to be removed in " + f"{version_removed}. {message}" + ).rstrip() + if hasattr(LOG, "deprecated"): + LOG.deprecated(deprecate_msg) + else: + LOG.warning(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, + ) + return out + + return decorator + + return wrapper diff --git a/doc/rtd/reference/faq.rst b/doc/rtd/reference/faq.rst index 87aade59..ba741741 100644 --- a/doc/rtd/reference/faq.rst +++ b/doc/rtd/reference/faq.rst @@ -235,6 +235,20 @@ to their respective support channels. For Subiquity autoinstaller that is via IRC (``#ubuntu-server`` on Libera) or Discourse. For Juju support see their `discourse page`_. + +Can I use cloud-init as a library? +================================== +Yes, in fact some projects `already do`_. However, ``cloud-init`` does not +currently make any API guarantees to external consumers - current library +users are projects that have close contact with ``cloud-init``, which is why +this model currently works. + +It is worth mentioning for library users that ``cloud-init`` defines a custom +log level. This log level, ``35``, is dedicated to logging info +related to deprecation information. Users of ``cloud-init`` as a library +may wish to ensure that this log level doesn't collide with external +libraries that define their own custom log levels. + Where can I learn more? ======================= @@ -279,6 +293,7 @@ Whitepapers: .. _validate-yaml.py: https://github.com/canonical/cloud-init/blob/main/tools/validate-yaml.py .. _Juju: https://ubuntu.com/blog/topics/juju .. _discourse page: https://discourse.charmhub.io +.. _already do: https://github.com/canonical/ubuntu-advantage-client/blob/9b46480b9e4b88e918bac5ced0d4b8edb3cbbeab/lib/auto_attach.py#L35 .. _cloud-init - The Good Parts: https://www.youtube.com/watch?v=2_m6EUo6VOI .. _Utilising cloud-init on Microsoft Azure (Whitepaper): https://ubuntu.com/engage/azure-cloud-init-whitepaper diff --git a/tests/integration_tests/modules/test_combined.py b/tests/integration_tests/modules/test_combined.py index 647e8728..264e2383 100644 --- a/tests/integration_tests/modules/test_combined.py +++ b/tests/integration_tests/modules/test_combined.py @@ -29,6 +29,10 @@ from tests.integration_tests.util import ( USER_DATA = """\ #cloud-config +users: +- default +- name: craig + sudo: false # make sure craig doesn't get elevated perms apt: primary: - arches: [default] @@ -113,6 +117,14 @@ class TestCombined: assert re.search(expected, log) + def test_deprecated_message(self, class_client: IntegrationInstance): + """Check that deprecated key produces a log warning""" + client = class_client + log = client.read_from_file("/var/log/cloud-init.log") + assert "Deprecated cloud-config provided" in log + assert "The value of 'false' in user craig's 'sudo' config is " in log + assert 2 == log.count("DEPRECATE") + def test_ntp_with_apt(self, class_client: IntegrationInstance): """LP #1628337. diff --git a/tests/unittests/config/test_cc_ca_certs.py b/tests/unittests/config/test_cc_ca_certs.py index 6db17485..adc3609a 100644 --- a/tests/unittests/config/test_cc_ca_certs.py +++ b/tests/unittests/config/test_cc_ca_certs.py @@ -9,7 +9,9 @@ from unittest import mock import pytest -from cloudinit import distros, helpers, subp, util +from cloudinit import distros, helpers +from cloudinit import log as logger +from cloudinit import subp, util from cloudinit.config import cc_ca_certs from cloudinit.config.schema import ( SchemaValidationError, @@ -424,25 +426,23 @@ class TestCACertsSchema: @mock.patch.object(cc_ca_certs, "update_ca_certs") def test_deprecate_key_warnings(self, update_ca_certs, caplog): """Assert warnings are logged for deprecated keys.""" - log = logging.getLogger("CALogTest") + logger.setupLogging() cloud = get_cloud("ubuntu") cc_ca_certs.handle( - "IGNORE", {"ca-certs": {"remove-defaults": False}}, cloud, log, [] + "IGNORE", {"ca-certs": {"remove-defaults": False}}, cloud, None, [] ) expected_warnings = [ - "DEPRECATION: key 'ca-certs' is now deprecated. Use 'ca_certs'" - " instead.", - "DEPRECATION: key 'ca-certs.remove-defaults' is now deprecated." - " Use 'ca_certs.remove_defaults' instead.", + "Key 'ca-certs' is deprecated in", + "Key 'remove-defaults' is deprecated in", ] for warning in expected_warnings: assert warning in caplog.text + assert "DEPRECAT" in caplog.text assert 1 == update_ca_certs.call_count @mock.patch.object(cc_ca_certs, "update_ca_certs") def test_duplicate_keys(self, update_ca_certs, caplog): """Assert warnings are logged for deprecated keys.""" - log = logging.getLogger("CALogTest") cloud = get_cloud("ubuntu") cc_ca_certs.handle( "IGNORE", @@ -451,7 +451,7 @@ class TestCACertsSchema: "ca_certs": {"remove_defaults": False}, }, cloud, - log, + None, [], ) expected_warning = ( @@ -460,6 +460,3 @@ class TestCACertsSchema: ) assert expected_warning in caplog.text assert 1 == update_ca_certs.call_count - - -# vi: ts=4 expandtab diff --git a/tests/unittests/config/test_schema.py b/tests/unittests/config/test_schema.py index d43af5cc..8276511d 100644 --- a/tests/unittests/config/test_schema.py +++ b/tests/unittests/config/test_schema.py @@ -19,7 +19,7 @@ from typing import List, Optional, Sequence, Set import pytest -from cloudinit import stages +from cloudinit import log, stages from cloudinit.config.schema import ( CLOUD_CONFIG_HEADER, VERSIONED_USERDATA_SCHEMA_FILE, @@ -55,6 +55,7 @@ from tests.unittests.helpers import ( from tests.unittests.util import FakeDataSource M_PATH = "cloudinit.config.schema." +DEPRECATED_LOG_LEVEL = 35 def get_schemas() -> dict: @@ -645,6 +646,7 @@ class TestValidateCloudConfigSchema: def test_validateconfig_logs_deprecations( self, schema, config, expected_msg, log_deprecations, caplog ): + log.setupLogging() validate_cloudconfig_schema( config, schema, @@ -653,7 +655,7 @@ class TestValidateCloudConfigSchema: ) if expected_msg is None: return - log_record = (M_PATH[:-1], logging.WARNING, expected_msg) + log_record = (M_PATH[:-1], DEPRECATED_LOG_LEVEL, expected_msg) if log_deprecations: assert log_record == caplog.record_tuples[-1] else: diff --git a/tests/unittests/distros/test_create_users.py b/tests/unittests/distros/test_create_users.py index edc152e1..2a3d85b8 100644 --- a/tests/unittests/distros/test_create_users.py +++ b/tests/unittests/distros/test_create_users.py @@ -2,7 +2,7 @@ import re -from cloudinit import distros, ssh_util +from cloudinit import distros, log, ssh_util from tests.unittests.helpers import CiTestCase, mock from tests.unittests.util import abstract_to_concrete @@ -140,6 +140,7 @@ class TestCreateUser(CiTestCase): self, m_is_group, m_subp, m_is_snappy ): """users.groups supports a dict value, but emit deprecation log.""" + log.setupLogging() user = "foouser" self.dist.create_user(user, groups={"group1": None, "group2": None}) expected = [ @@ -150,10 +151,15 @@ class TestCreateUser(CiTestCase): ] self.assertEqual(m_subp.call_args_list, expected) self.assertIn( - "WARNING: DEPRECATED: The user foouser has a 'groups' config" - " value of type dict which is deprecated and will be removed in a" - " future version of cloud-init. Use a comma-delimited string or" - " array instead: group1,group2.", + "DEPRECAT", + self.logs.getvalue(), + ) + self.assertIn( + "The user foouser has a 'groups' config value of type dict", + self.logs.getvalue(), + ) + self.assertIn( + "Use a comma-delimited", self.logs.getvalue(), ) @@ -182,9 +188,9 @@ class TestCreateUser(CiTestCase): ], ) self.assertIn( - "WARNING: DEPRECATED: The user foouser has a 'sudo' config value" - " of 'false' which will be dropped after April 2027. Use 'null'" - " instead.", + "DEPRECATED: The value of 'false' in user foouser's 'sudo' " + "config is deprecated in 22.3 and scheduled to be removed" + " in 27.3. Use 'null' instead.", self.logs.getvalue(), ) diff --git a/tests/unittests/net/test_network_state.py b/tests/unittests/net/test_network_state.py index 57a4436f..0e1b27b4 100644 --- a/tests/unittests/net/test_network_state.py +++ b/tests/unittests/net/test_network_state.py @@ -4,7 +4,7 @@ from unittest import mock import pytest -from cloudinit import safeyaml +from cloudinit import log, safeyaml, util from cloudinit.net import network_state from cloudinit.net.netplan import Renderer as NetplanRenderer from cloudinit.net.renderers import NAME_TO_RENDERER @@ -214,6 +214,9 @@ class TestNetworkStateParseConfigV2: In netplan targets we perform a passthrough and the warning is not needed. """ + log.setupLogging() + + util.deprecate._log = set() # type: ignore ncfg = safeyaml.load( cfg.format( gateway4="gateway4: 10.54.0.1", @@ -233,7 +236,7 @@ class TestNetworkStateParseConfigV2: else: count = 0 # No deprecation as we passthrough assert count == caplog.text.count( - "DEPRECATED: The use of `gateway4` and `gateway6` is" + "The use of `gateway4` and `gateway6`" ) diff --git a/tests/unittests/test_log.py b/tests/unittests/test_log.py index 87c69dbb..38791ef9 100644 --- a/tests/unittests/test_log.py +++ b/tests/unittests/test_log.py @@ -8,6 +8,7 @@ import logging import time from cloudinit import log as ci_logging +from cloudinit import util from cloudinit.analyze.dump import CLOUD_INIT_ASCTIME_FMT from tests.unittests.helpers import CiTestCase @@ -57,3 +58,32 @@ class TestCloudInitLogger(CiTestCase): self.assertLess(parsed_dt, utc_after) self.assertLess(utc_before, utc_after) self.assertGreater(utc_after, parsed_dt) + + +class TestDeprecatedLogs: + def test_deprecated_log_level(self, caplog): + ci_logging.setupLogging() + log = ci_logging.getLogger() + log.deprecated("deprecated message") + assert "DEPRECATED" == caplog.records[0].levelname + assert "deprecated message" in caplog.text + + def test_log_deduplication(self, caplog): + ci_logging.defineDeprecationLogger() + util.deprecate( + deprecated="stuff", + deprecated_version="19.1", + extra_message=":)", + ) + util.deprecate( + deprecated="stuff", + deprecated_version="19.1", + extra_message=":)", + ) + util.deprecate( + deprecated="stuff", + deprecated_version="19.1", + extra_message=":)", + schedule=6, + ) + assert 2 == len(caplog.records) diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 056aaeb6..832d94fb 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -13,7 +13,7 @@ from typing import Optional import pytest from yaml.serializer import Serializer -from cloudinit import distros, net +from cloudinit import distros, log, net from cloudinit import safeyaml as yaml from cloudinit import subp, temp_utils, util from cloudinit.net import ( @@ -5230,6 +5230,7 @@ USERCTL=no """ # noqa: E501 ), } + log.setupLogging() found = self._render_and_read(network_config=v2_data) self._compare_files_to_expected(expected, found) |