cloud-config annotation throws exception, fix it#1130
Conversation
There was a problem hiding this comment.
While this "fixes" cloud-init schema, this is invalid cloud-config and our fix should probably also either Warn or hard-error noting Invalid #cloud-config in userdata instead of a confusing traceback.
Providing an empty (non-dict) YAML such as #cloud-config\n results in a Traceback anyway on the booted system
csmith@uptown:~$ cat > invalid_cloudconfig.yaml <<EOF
#cloud-config
EOF
csmith@uptown:~$ lxc launch ubuntu-daily:bionic -c user.user-data="$( cat 3.yaml)"
Creating the instance
Instance name is: firm-starfish
Starting firm-starfish
csmith@uptown:~$ lxc exec firm-starfish cat /var/log/cloud-init.log | grep -B 5 -A 10 Traceback
2021-12-03 22:11:12,644 - __init__.py[DEBUG]: Calling handler CloudConfigPartHandler: [['text/cloud-config', 'text/cloud-config-jsonp']] (text/cloud-config, part-001, 3) with frequency once-per-instance
2021-12-03 22:11:12,644 - util.py[DEBUG]: Attempting to load yaml from string of length 14 with allowed root types (<class 'dict'>,)
2021-12-03 22:11:12,644 - util.py[DEBUG]: loaded blob returned None, returning default.
2021-12-03 22:11:12,644 - util.py[WARNING]: Failed at merging in cloud config part from part-001
2021-12-03 22:11:12,644 - util.py[DEBUG]: Failed at merging in cloud config part from part-001
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/cloudinit/handlers/cloud_config.py", line 140, in handle_part
self._merge_part(payload, headers)
File "/usr/lib/python3/dist-packages/cloudinit/handlers/cloud_config.py", line 116, in _merge_part
(payload_yaml, my_mergers) = self._extract_mergers(payload, headers)
File "/usr/lib/python3/dist-packages/cloudinit/handlers/cloud_config.py", line 95, in _extract_mergers
mergers_yaml = mergers.dict_extract_mergers(payload_yaml)
File "/usr/lib/python3/dist-packages/cloudinit/mergers/__init__.py", line 79, in dict_extract_mergers
raw_mergers = config.pop('merge_how', None)
AttributeError: 'NoneType' object has no attribute 'pop'
2021-12-03 22:11:12,645 - __init__.py[DEBUG]: Calling handler CloudConfigPartHandler: [['text/cloud-config', 'text/cloud-config-jsonp']] (__end__, None, 3) with frequency once-per-instance`cloud-init devel schema --annotate` should not throw exception, return something meaningful instead
75130f7 to
f520e5e
Compare
|
Latest update to this PR checks for empty payload and prints something useful: Apparently this code path is executed twice during startup, so we get twice the warning. I'm not familiar enough with merging to know why this is run twice, but at least now it is does something arguably more "useful". |
66d60e7 to
f520e5e
Compare
|
Hello! Thank you for this proposed change to cloud-init. This pull request is now marked as stale as it has not seen any activity in 14 days. If no activity occurs within the next 7 days, this pull request will automatically close. If you are waiting for code review and you are seeing this message, apologies! Please reply, tagging mitechie, and he will ensure that someone takes a look soon. (If the pull request is closed and you would like to continue working on it, please do tag mitechie to reopen it.) |
…nted noise in the logs - elim by checking for nonetype
|
@blackboxsw - I finally got back around to this. The latest iteration drops the following log when an empty cloud config is provided. Assuming tests pass I think this is ready for review. |
blackboxsw
left a comment
There was a problem hiding this comment.
Thanks for the update @holmanb. I'd like to see handle more cases in annotate than just representing "empty" user-data
There is at least one case that still fails.
touch should-error-about-missing-header.yaml
python3 -m cloudinit.cmd.main devel schema -c should-error-about-missing-header.yaml --annotate
# Errors: -------------
Empty cloud-configAn empty user-data file should instead report that it doesn't have a #cloud-config header line.
I also think we could use one or two unittests for this: I've added one in my consolidated diff below if you agree with the suggestions I made inline:
diff --git a/cloudinit/config/schema.py b/cloudinit/config/schema.py
index befad76ca..85cad3bb6 100644
--- a/cloudinit/config/schema.py
+++ b/cloudinit/config/schema.py
@@ -217,21 +217,21 @@ def annotated_cloudconfig_file(cloudconfig, original_content, schema_errors):
if not schema_errors:
return original_content
schemapaths = {}
- if cloudconfig:
- schemapaths = _schemapath_for_cloudconfig(
- cloudconfig, original_content
- )
errors_by_line = defaultdict(list)
error_footer = []
error_header = "# Errors: -------------\n{0}\n\n"
annotated_content = []
lines = original_content.decode().split("\n")
- cloud_config_body = lines[1:]
-
- # Return a meaningful message on empty cloud-config
- if not [line for line in cloud_config_body if line.strip()]:
- return error_header.format("Empty cloud-config")
-
+ if not isinstance(cloudconfig, dict):
+ # Return a meaningful message on empty cloud-config
+ return "\n".join(
+ lines
+ + [error_header.format("# E1: Cloud-config is not a YAML dict.")]
+ )
+ if cloudconfig:
+ schemapaths = _schemapath_for_cloudconfig(
+ cloudconfig, original_content
+ )
for path, msg in schema_errors:
match = re.match(r"format-l(?P<line>\d+)\.c(?P<col>\d+).*", path)
if match:
diff --git a/cloudinit/handlers/cloud_config.py b/cloudinit/handlers/cloud_config.py
index c6b2c3afb..8070c6cb2 100644
--- a/cloudinit/handlers/cloud_config.py
+++ b/cloudinit/handlers/cloud_config.py
@@ -143,7 +143,11 @@ class CloudConfigPartHandler(handlers.Handler):
filename = filename.replace(i, " ")
self.file_names.append(filename.strip())
except ValueError as err:
- LOG.warning("Failed at merging in %s", err)
+ LOG.warning(
+ "Failed at merging in cloud config part from %s: %s",
+ filename,
+ err,
+ )
except Exception:
util.logexc(
LOG, "Failed at merging in cloud config part from %s", filename
diff --git a/tests/unittests/config/test_schema.py b/tests/unittests/config/test_schema.py
index fb5b891d7..416cc46d5 100644
--- a/tests/unittests/config/test_schema.py
+++ b/tests/unittests/config/test_schema.py
@@ -546,6 +546,31 @@ class AnnotatedCloudconfigFileTest(CiTestCase):
content, annotated_cloudconfig_file({}, content, schema_errors=[])
)
+ def test_annotated_cloudconfig_file_with_non_dict_cloud_config(self):
+ """Error when empty non-dict cloud-config is provided.
+
+ OurJSON validation when user-data is None type generates a bunch
+ schema validation errors of the format:
+ ('', "None is not of type 'object'"). Ignore those symptoms and
+ report the general problem instead.
+ """
+ content = b"\n\n\n"
+ expected = "\n".join(
+ [
+ content.decode(),
+ "# Errors: -------------",
+ "# E1: Cloud-config is not a YAML dict.\n\n",
+ ]
+ )
+ self.assertEqual(
+ expected,
+ annotated_cloudconfig_file(
+ None,
+ content,
+ schema_errors=[("", "None is not of type 'object'")],
+ ),
+ )
+
def test_annotated_cloudconfig_file_schema_annotates_and_adds_footer(self):
"""With schema_errors, error lines are annotated and a footer added."""
content = dedent(|
Thanks @blackboxsw. I agree with all of your suggestions. This better handles the root cause over dealing with symptoms. I have applied your suggested changes. I can add further tests if you think it is warranted, but I haven't thought up other cases worth adding. |
|
@blackboxsw when you get a chance I think this is ready. I made the changes mentioned yesterday in IRC and also added a test verifying an error message is produced with the incorrect flag combo. |
blackboxsw
left a comment
There was a problem hiding this comment.
@holmanb This LGTM! Thanks ofr all the related fixes in here.
Let's get this out into Jammy.
Additional Context
the following config:
causes the following exception:
Test Configs
empty-body.yml:
#cloud-configinvalid-not-empty-config.yml:
whilespace-config-has-tabs-spaces.yml:
#cloud-configobserve new error message:
check that annotation still works for
invalid-not-empty-config.ymlChecklist: