diff options
Diffstat (limited to 'tests/unittests/config/test_schema.py')
-rw-r--r-- | tests/unittests/config/test_schema.py | 318 |
1 files changed, 204 insertions, 114 deletions
diff --git a/tests/unittests/config/test_schema.py b/tests/unittests/config/test_schema.py index 50128f2c..d43af5cc 100644 --- a/tests/unittests/config/test_schema.py +++ b/tests/unittests/config/test_schema.py @@ -9,6 +9,7 @@ import logging import os import re import sys +import unittest from collections import namedtuple from copy import deepcopy from pathlib import Path @@ -17,7 +18,6 @@ from types import ModuleType from typing import List, Optional, Sequence, Set import pytest -import responses from cloudinit import stages from cloudinit.config.schema import ( @@ -147,6 +147,54 @@ class TestVersionedSchemas: ) +class TestCheckSchema(unittest.TestCase): + def test_schema_bools_have_dates(self): + """ensure that new/changed/deprecated keys have an associated + version key + """ + + def check_deprecation_keys(schema, search_key): + if search_key in schema: + assert f"{search_key}_version" in schema + for sub_item in schema.values(): + if isinstance(sub_item, dict): + check_deprecation_keys(sub_item, search_key) + return True + + # ensure that check_deprecation_keys works as expected + assert check_deprecation_keys( + {"changed": True, "changed_version": "22.3"}, "changed" + ) + assert check_deprecation_keys( + {"properties": {"deprecated": True, "deprecated_version": "22.3"}}, + "deprecated", + ) + assert check_deprecation_keys( + { + "properties": { + "properties": {"new": True, "new_version": "22.3"} + } + }, + "new", + ) + with self.assertRaises(AssertionError): + check_deprecation_keys({"changed": True}, "changed") + with self.assertRaises(AssertionError): + check_deprecation_keys( + {"properties": {"deprecated": True}}, "deprecated" + ) + with self.assertRaises(AssertionError): + check_deprecation_keys( + {"properties": {"properties": {"new": True}}}, "new" + ) + + # test the in-repo schema + schema = get_schema() + assert check_deprecation_keys(schema, "new") + assert check_deprecation_keys(schema, "changed") + assert check_deprecation_keys(schema, "deprecated") + + class TestGetSchema: def test_static_schema_file_is_valid(self, caplog): with caplog.at_level(logging.WARNING): @@ -282,7 +330,7 @@ class TestValidateCloudConfigSchema: ((None, 1), ({"properties": {"p1": {"type": "string"}}}, 0)), ) @skipUnlessJsonSchema() - @mock.patch("cloudinit.config.schema.get_schema") + @mock.patch(M_PATH + "get_schema") def test_validateconfig_schema_use_full_schema_when_no_schema_param( self, get_schema, schema, call_count ): @@ -403,13 +451,17 @@ class TestValidateCloudConfigSchema: "a-b": { "type": "string", "deprecated": True, + "deprecated_version": "22.1", + "new": True, + "new_version": "22.1", "description": "<desc>", }, "a_b": {"type": "string", "description": "noop"}, }, }, {"a-b": "asdf"}, - "Deprecated cloud-config provided:\na-b: DEPRECATED: <desc>", + "Deprecated cloud-config provided:\na-b: <desc> " + "Deprecated in version 22.1.", ), ( { @@ -421,6 +473,7 @@ class TestValidateCloudConfigSchema: { "type": "string", "deprecated": True, + "deprecated_version": "22.1", "description": "<desc>", }, ] @@ -428,7 +481,8 @@ class TestValidateCloudConfigSchema: }, }, {"x": "+5"}, - "Deprecated cloud-config provided:\nx: DEPRECATED: <desc>", + "Deprecated cloud-config provided:\nx: <desc> " + "Deprecated in version 22.1.", ), ( { @@ -439,6 +493,8 @@ class TestValidateCloudConfigSchema: {"type": "string", "description": "noop"}, { "deprecated": True, + "deprecated_version": "22.1", + "deprecated_description": "<dep desc>", "description": "<desc>", }, ] @@ -446,7 +502,8 @@ class TestValidateCloudConfigSchema: }, }, {"x": "5"}, - "Deprecated cloud-config provided:\nx: DEPRECATED: <desc>", + "Deprecated cloud-config provided:\nx: <desc> " + "Deprecated in version 22.1. <dep desc>", ), ( { @@ -458,6 +515,7 @@ class TestValidateCloudConfigSchema: { "type": "string", "deprecated": True, + "deprecated_version": "22.1", "description": "<desc>", }, ] @@ -465,7 +523,8 @@ class TestValidateCloudConfigSchema: }, }, {"x": "5"}, - "Deprecated cloud-config provided:\nx: DEPRECATED: <desc>", + "Deprecated cloud-config provided:\nx: <desc> " + "Deprecated in version 22.1.", ), ( { @@ -474,12 +533,14 @@ class TestValidateCloudConfigSchema: "x": { "type": "string", "deprecated": True, + "deprecated_version": "22.1", "description": "<desc>", }, }, }, {"x": "+5"}, - "Deprecated cloud-config provided:\nx: DEPRECATED: <desc>", + "Deprecated cloud-config provided:\nx: <desc> " + "Deprecated in version 22.1.", ), ( { @@ -501,6 +562,7 @@ class TestValidateCloudConfigSchema: "$defs": { "my_ref": { "deprecated": True, + "deprecated_version": "32.3", "description": "<desc>", } }, @@ -514,7 +576,8 @@ class TestValidateCloudConfigSchema: }, }, {"x": "+5"}, - "Deprecated cloud-config provided:\nx: DEPRECATED: <desc>", + "Deprecated cloud-config provided:\nx: <desc> " + "Deprecated in version 32.3.", ), ( { @@ -522,6 +585,7 @@ class TestValidateCloudConfigSchema: "$defs": { "my_ref": { "deprecated": True, + "deprecated_version": "27.2", } }, "properties": { @@ -537,7 +601,8 @@ class TestValidateCloudConfigSchema: }, }, {"x": "+5"}, - "Deprecated cloud-config provided:\nx: DEPRECATED.", + "Deprecated cloud-config provided:\nx: Deprecated in " + "version 27.2.", ), ( { @@ -546,12 +611,14 @@ class TestValidateCloudConfigSchema: "^.+$": { "minItems": 1, "deprecated": True, + "deprecated_version": "27.2", "description": "<desc>", } }, }, {"a-b": "asdf"}, - "Deprecated cloud-config provided:\na-b: DEPRECATED: <desc>", + "Deprecated cloud-config provided:\na-b: <desc> " + "Deprecated in version 27.2.", ), pytest.param( { @@ -560,11 +627,17 @@ class TestValidateCloudConfigSchema: "^.+$": { "minItems": 1, "deprecated": True, + "deprecated_version": "27.2", + "changed": True, + "changed_version": "22.2", + "changed_description": "Drop ballast.", } }, }, {"a-b": "asdf"}, - "Deprecated cloud-config provided:\na-b: DEPRECATED.", + "Deprecated cloud-config provided:\na-b: Deprecated " + "in version 27.2.\na-b: Changed in version 22.2. " + "Drop ballast.", id="deprecated_pattern_property_without_description", ), ], @@ -638,14 +711,6 @@ class TestValidateCloudConfigFile: """Tests for validate_cloudconfig_file.""" @pytest.mark.parametrize("annotate", (True, False)) - def test_validateconfig_file_error_on_absent_file(self, annotate): - """On absent config_path, validate_cloudconfig_file errors.""" - with pytest.raises( - RuntimeError, match="Configfile /not/here does not exist" - ): - validate_cloudconfig_file("/not/here", {}, annotate) - - @pytest.mark.parametrize("annotate", (True, False)) def test_validateconfig_file_error_on_invalid_header( self, annotate, tmpdir ): @@ -708,59 +773,10 @@ class TestValidateCloudConfigFile: validate_cloudconfig_file(config_file.strpath, schema, annotate) @skipUnlessJsonSchema() - @responses.activate - @pytest.mark.parametrize("annotate", (True, False)) - @mock.patch("cloudinit.url_helper.time.sleep") - @mock.patch(M_PATH + "os.getuid", return_value=0) - def test_validateconfig_file_include_validates_schema( - self, m_getuid, m_sleep, annotate, mocker - ): - """validate_cloudconfig_file raises errors on invalid schema - when user-data uses `#include`.""" - schema = {"properties": {"p1": {"type": "string", "format": "string"}}} - included_data = "#cloud-config\np1: -1" - included_url = "http://asdf/user-data" - blob = f"#include {included_url}" - responses.add(responses.GET, included_url, included_data) - - ci = stages.Init() - ci.datasource = FakeDataSource(blob) - mocker.patch(M_PATH + "Init", return_value=ci) - - error_msg = ( - "Cloud config schema errors: p1: -1 is not of type 'string'" - ) - with pytest.raises(SchemaValidationError, match=error_msg): - validate_cloudconfig_file(None, schema, annotate) - - @skipUnlessJsonSchema() - @responses.activate - @pytest.mark.parametrize("annotate", (True, False)) - @mock.patch("cloudinit.url_helper.time.sleep") - @mock.patch(M_PATH + "os.getuid", return_value=0) - def test_validateconfig_file_include_success( - self, m_getuid, m_sleep, annotate, mocker - ): - """validate_cloudconfig_file raises errors on invalid schema - when user-data uses `#include`.""" - schema = {"properties": {"p1": {"type": "string", "format": "string"}}} - included_data = "#cloud-config\np1: asdf" - included_url = "http://asdf/user-data" - blob = f"#include {included_url}" - responses.add(responses.GET, included_url, included_data) - - ci = stages.Init() - ci.datasource = FakeDataSource(blob) - mocker.patch(M_PATH + "Init", return_value=ci) - - validate_cloudconfig_file(None, schema, annotate) - - @skipUnlessJsonSchema() @pytest.mark.parametrize("annotate", (True, False)) @mock.patch("cloudinit.url_helper.time.sleep") - @mock.patch(M_PATH + "os.getuid", return_value=0) def test_validateconfig_file_no_cloud_cfg( - self, m_getuid, m_sleep, annotate, capsys, mocker + self, m_sleep, annotate, capsys, mocker ): """validate_cloudconfig_file does noop with empty user-data.""" schema = {"properties": {"p1": {"type": "string", "format": "string"}}} @@ -769,15 +785,18 @@ class TestValidateCloudConfigFile: ci = stages.Init() ci.datasource = FakeDataSource(blob) mocker.patch(M_PATH + "Init", return_value=ci) + cloud_config_file = ci.paths.get_ipath_cur("cloud_config") + write_file(cloud_config_file, b"") with pytest.raises( SchemaValidationError, match=re.escape( - "Cloud config schema errors: format-l1.c1: File None needs" - ' to begin with "#cloud-config"' + "Cloud config schema errors: format-l1.c1:" + f" File {cloud_config_file} needs to begin with" + ' "#cloud-config"' ), ): - validate_cloudconfig_file(None, schema, annotate) + validate_cloudconfig_file(cloud_config_file, schema, annotate) class TestSchemaDocMarkdown: @@ -847,7 +866,7 @@ class TestSchemaDocMarkdown: **Supported distros:** debian, rhel **Config schema**: - **prop1:** (array of integer) prop-description. + **prop1:** (array of integer) prop-description **Examples**:: @@ -867,12 +886,12 @@ class TestSchemaDocMarkdown: "properties": { "prop1": { "type": "array", - "description": "prop-description", + "description": "prop-description.", "items": {"type": "string"}, }, "prop2": { "type": "boolean", - "description": "prop2-description", + "description": "prop2-description.", }, }, } @@ -947,7 +966,7 @@ class TestSchemaDocMarkdown: "patternProperties": { "^.+$": { "label": "<opaque_label>", - "description": "List of cool strings", + "description": "List of cool strings.", "type": "array", "items": {"type": "string"}, "minItems": 1, @@ -1101,7 +1120,7 @@ class TestSchemaDocMarkdown: "properties": { "prop1": { "type": "array", - "description": "prop-description", + "description": "prop-description.", "items": {"type": "integer"}, } }, @@ -1157,7 +1176,7 @@ class TestSchemaDocMarkdown: - option2 - option3 - The default value is option1. + The default value is option1 """ ) @@ -1268,7 +1287,29 @@ class TestSchemaDocMarkdown: } } }, - "**prop1:** (string/integer) DEPRECATED: <description>", + "**prop1:** (string/integer) <description>\n\n " + "*Deprecated in version <missing deprecated_version " + "key, please file a bug report>.*", + ), + ( + { + "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "prop1": { + "type": ["string", "integer"], + "description": "<description>", + "deprecated": True, + "deprecated_version": "2", + "changed": True, + "changed_version": "1", + "new": True, + "new_version": "1", + }, + }, + }, + "**prop1:** (string/integer) <description>\n\n " + "*Deprecated in version 2.*\n\n *Changed in version" + " 1.*\n\n *New in version 1.*", ), ( { @@ -1278,10 +1319,20 @@ class TestSchemaDocMarkdown: "type": ["string", "integer"], "description": "<description>", "deprecated": True, + "deprecated_version": "2", + "deprecated_description": "dep", + "changed": True, + "changed_version": "1", + "changed_description": "chg", + "new": True, + "new_version": "1", + "new_description": "new", }, }, }, - "**prop1:** (string/integer) DEPRECATED: <description>", + "**prop1:** (string/integer) <description>\n\n " + "*Deprecated in version 2. dep*\n\n *Changed in " + "version 1. chg*\n\n *New in version 1. new*", ), ( { @@ -1299,7 +1350,9 @@ class TestSchemaDocMarkdown: } }, }, - "**prop1:** (string/integer) DEPRECATED: <description>", + "**prop1:** (string/integer) <description>\n\n " + "*Deprecated in version <missing deprecated_version " + "key, please file a bug report>.*", ), ( { @@ -1319,7 +1372,9 @@ class TestSchemaDocMarkdown: } }, }, - "**prop1:** (string/integer) DEPRECATED: <description>", + "**prop1:** (string/integer) <description>\n\n " + "*Deprecated in version <missing deprecated_version " + "key, please file a bug report>.*", ), ( { @@ -1330,14 +1385,17 @@ class TestSchemaDocMarkdown: "anyOf": [ { "type": ["string", "integer"], - "description": "<deprecated_description>", + "description": "<deprecated_description>.", "deprecated": True, }, ], }, }, }, - "**prop1:** (UNDEFINED) <description>. DEPRECATED: <deprecat", + "**prop1:** (UNDEFINED) <description>. " + "<deprecated_description>.\n\n *Deprecated in " + "version <missing deprecated_version key, please " + "file a bug report>.*", ), ( { @@ -1347,7 +1405,7 @@ class TestSchemaDocMarkdown: "anyOf": [ { "type": ["string", "integer"], - "description": "<deprecated_description>", + "description": "<deprecated_description>.", "deprecated": True, }, { @@ -1358,8 +1416,9 @@ class TestSchemaDocMarkdown: }, }, }, - "**prop1:** (number) <description>. DEPRECATED:" - " <deprecated_description>", + "**prop1:** (number) <deprecated_description>.\n\n" + " *Deprecated in version <missing " + "deprecated_version key, please file a bug report>.*", ), ( { @@ -1371,6 +1430,7 @@ class TestSchemaDocMarkdown: "type": ["string", "integer"], "description": "<deprecated_description>", "deprecated": True, + "deprecated_version": "22.1", }, { "type": "string", @@ -1381,8 +1441,9 @@ class TestSchemaDocMarkdown: }, }, }, - "**prop1:** (``none``/``unchanged``/``os``) <description>." - " DEPRECATED: <deprecated_description>.", + "**prop1:** (``none``/``unchanged``/``os``) " + "<description>. <deprecated_description>\n\n " + "*Deprecated in version 22.1.*", ), ( { @@ -1403,8 +1464,9 @@ class TestSchemaDocMarkdown: }, }, }, - "**prop1:** (string/integer/``none``/``unchanged``/``os``)" - " <description_1>. <description>_2.\n", + "**prop1:** (string/integer/``none``/" + "``unchanged``/``os``) <description_1>. " + "<description>_2\n", ), ( { @@ -1425,7 +1487,7 @@ class TestSchemaDocMarkdown: }, }, }, - "**prop1:** (array of object) <desc_1>.\n", + "**prop1:** (array of object) <desc_1>\n", ), ], ) @@ -1582,7 +1644,7 @@ class TestMain: main() assert 1 == context_manager.value.code _out, err = capsys.readouterr() - assert "Error:\nConfigfile NOT_A_FILE does not exist\n" == err + assert "Error: Config file NOT_A_FILE does not exist\n" == err def test_main_invalid_flag_combo(self, capsys): """Main exits non-zero when invalid flag combo used.""" @@ -1614,24 +1676,48 @@ class TestMain: with mock.patch("sys.argv", myargs): assert 0 == main(), "Expected 0 exit code" out, _err = capsys.readouterr() - assert "Valid cloud-config: {0}\n".format(myyaml) == out + assert f"Valid cloud-config: {myyaml}\n" == out @mock.patch(M_PATH + "os.getuid", return_value=0) - def test_main_validates_system_userdata( - self, m_getuid, capsys, mocker, paths + def test_main_validates_system_userdata_and_vendordata( + self, _getuid, capsys, mocker, paths ): """When --system is provided, main validates system userdata.""" m_init = mocker.patch(M_PATH + "Init") m_init.return_value.paths.get_ipath = paths.get_ipath_cur cloud_config_file = paths.get_ipath_cur("cloud_config") write_file(cloud_config_file, b"#cloud-config\nntp:") + vd_file = paths.get_ipath_cur("vendor_cloud_config") + write_file(vd_file, b"#cloud-config\nssh_import_id: [me]") + vd2_file = paths.get_ipath_cur("vendor2_cloud_config") + write_file(vd2_file, b"#cloud-config\nssh_pw_auth: true") myargs = ["mycmd", "--system"] with mock.patch("sys.argv", myargs): - assert 0 == main(), "Expected 0 exit code" + main() out, _err = capsys.readouterr() - assert "Valid cloud-config: system userdata\n" == out - @mock.patch("cloudinit.config.schema.os.getuid", return_value=1000) + expected = dedent( + """\ + Found cloud-config data types: user-data, vendor-data, vendor2-data + + 1. user-data at {ud_file}: + Valid cloud-config: user-data + + 2. vendor-data at {vd_file}: + Valid cloud-config: vendor-data + + 3. vendor2-data at {vd2_file}: + Valid cloud-config: vendor2-data + """ + ) + assert ( + expected.format( + ud_file=cloud_config_file, vd_file=vd_file, vd2_file=vd2_file + ) + == out + ) + + @mock.patch(M_PATH + "os.getuid", return_value=1000) def test_main_system_userdata_requires_root(self, m_getuid, capsys, paths): """Non-root user can't use --system param""" myargs = ["mycmd", "--system"] @@ -1641,8 +1727,8 @@ class TestMain: assert 1 == context_manager.value.code _out, err = capsys.readouterr() expected = ( - "Error:\nUnable to read system userdata as non-root user. " - "Try using sudo\n" + "Error:\nUnable to read system userdata or vendordata as non-root" + " user. Try using sudo.\n" ) assert expected == err @@ -1784,17 +1870,16 @@ class TestHandleSchemaArgs: #cloud-config packages: - htop - apt_update: true # D1 - apt_upgrade: true # D2 - apt_reboot_if_required: true # D3 + apt_update: true # D1 + apt_upgrade: true # D2 + apt_reboot_if_required: true # D3 # Deprecations: ------------- - # D1: DEPRECATED: Dropped after April 2027. Use ``package_update``. Default: ``false`` - # D2: DEPRECATED: Dropped after April 2027. Use ``package_upgrade``. Default: ``false`` - # D3: DEPRECATED: Dropped after April 2027. Use ``package_reboot_if_required``. Default: ``false`` - + # D1: Default: ``false``. Deprecated in version 22.2. Use ``package_update`` instead. + # D2: Default: ``false``. Deprecated in version 22.2. Use ``package_upgrade`` instead. + # D3: Default: ``false``. Deprecated in version 22.2. Use ``package_reboot_if_required`` instead. - Valid cloud-config: {} + Valid cloud-config: {cfg_file} """ # noqa: E501 ), ), @@ -1803,10 +1888,12 @@ class TestHandleSchemaArgs: dedent( """\ Cloud config schema deprecations: \ -apt_reboot_if_required: DEPRECATED: Dropped after April 2027. Use ``package_reboot_if_required``. Default: ``false``, \ -apt_update: DEPRECATED: Dropped after April 2027. Use ``package_update``. Default: ``false``, \ -apt_upgrade: DEPRECATED: Dropped after April 2027. Use ``package_upgrade``. Default: ``false`` - Valid cloud-config: {} +apt_reboot_if_required: Default: ``false``. Deprecated in version 22.2.\ + Use ``package_reboot_if_required`` instead., apt_update: Default: \ +``false``. Deprecated in version 22.2. Use ``package_update`` instead.,\ + apt_upgrade: Default: ``false``. Deprecated in version 22.2. Use \ +``package_upgrade`` instead.\ + Valid cloud-config: {cfg_file} """ # noqa: E501 ), ), @@ -1837,6 +1924,9 @@ apt_upgrade: DEPRECATED: Dropped after April 2027. Use ``package_upgrade``. Defa ) handle_schema_args("unused", args) out, err = capsys.readouterr() - assert expected_output.format(user_data_fn) == out + assert ( + expected_output.format(cfg_file=user_data_fn).split() + == out.split() + ) assert not err assert "deprec" not in caplog.text |