From f1fe603084fdf05b70a856ea137a608eb663ccb9 Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Tue, 30 May 2023 15:23:31 +0200 Subject: [PATCH] ec2_metadata_facts - Handle decompression when EC2 instance user-data is compressed (#1575) ec2_metadata_facts - Handle decompression when EC2 instance user-data is compressed SUMMARY Handle decompression when user-data is compressed. The fetch_url method from ansible.module_utils.urls does not decompress the user-data because the header is missing (The API does not set 'Content-Encoding' = 'gzip'). ISSUE TYPE Bugfix Pull Request COMPONENT NAME ec2_metadata_facts ADDITIONAL INFORMATION Reviewed-by: Mike Graves Reviewed-by: Mark Chappell Reviewed-by: Alina Buzachis --- ...data_facts-handle_compressed_user_data.yml | 3 ++ plugins/modules/ec2_metadata_facts.py | 34 ++++++++++++++++ .../modules/test_ec2_metadata_facts.py | 39 +++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 changelogs/fragments/20230526-ec2_mertadata_facts-handle_compressed_user_data.yml diff --git a/changelogs/fragments/20230526-ec2_mertadata_facts-handle_compressed_user_data.yml b/changelogs/fragments/20230526-ec2_mertadata_facts-handle_compressed_user_data.yml new file mode 100644 index 00000000000..2abee986495 --- /dev/null +++ b/changelogs/fragments/20230526-ec2_mertadata_facts-handle_compressed_user_data.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - 'ec2_metadata_facts - Handle decompression when EC2 instance user-data is gzip compressed. The fetch_url method from ansible.module_utils.urls does not decompress the user-data unless the header explicitly contains ``Content-Encoding: gzip`` (https://github.com/ansible-collections/amazon.aws/pull/1575).' diff --git a/plugins/modules/ec2_metadata_facts.py b/plugins/modules/ec2_metadata_facts.py index b8288d2d912..9d043bf218d 100644 --- a/plugins/modules/ec2_metadata_facts.py +++ b/plugins/modules/ec2_metadata_facts.py @@ -438,6 +438,7 @@ import re import socket import time +import zlib from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_text @@ -484,11 +485,42 @@ def __init__( self._token = None self._prefix = "ansible_ec2_%s" + def _decode(self, data): + try: + return data.decode("utf-8") + except UnicodeDecodeError: + # Decoding as UTF-8 failed, return data without raising an error + self.module.warn("Decoding user-data as UTF-8 failed, return data as is ignoring any error") + return data.decode("utf-8", errors="ignore") + + def decode_user_data(self, data): + is_compressed = False + + # Check if data is compressed using zlib header + if data.startswith(b"\x78\x9c") or data.startswith(b"\x1f\x8b"): + is_compressed = True + + if is_compressed: + # Data is compressed, attempt decompression and decode using UTF-8 + try: + decompressed = zlib.decompress(data, zlib.MAX_WBITS | 32) + return self._decode(decompressed) + except zlib.error: + # Unable to decompress, return original data + self.module.warn( + "Unable to decompress user-data using zlib, attempt to decode original user-data as UTF-8" + ) + return self._decode(data) + else: + # Data is not compressed, decode using UTF-8 + return self._decode(data) + def _fetch(self, url): encoded_url = quote(url, safe="%/:=&?~#+!$,;'@()*[]") headers = {} if self._token: headers = {"X-aws-ec2-metadata-token": self._token} + response, info = fetch_url(self.module, encoded_url, headers=headers, force=True) if info.get("status") in (401, 403): @@ -505,6 +537,8 @@ def _fetch(self, url): ) if response and info["status"] < 400: data = response.read() + if "user-data" in encoded_url: + return to_text(self.decode_user_data(data)) else: data = None return to_text(data) diff --git a/tests/unit/plugins/modules/test_ec2_metadata_facts.py b/tests/unit/plugins/modules/test_ec2_metadata_facts.py index e43b00769e3..10c4a341adc 100644 --- a/tests/unit/plugins/modules/test_ec2_metadata_facts.py +++ b/tests/unit/plugins/modules/test_ec2_metadata_facts.py @@ -1,6 +1,7 @@ # This file is part of Ansible # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +import gzip import io import pytest from unittest.mock import MagicMock @@ -59,3 +60,41 @@ def test_fetch_recusive(m_fetch_url, ec2_instance): ] ec2_instance.fetch("http://169.254.169.254/latest/meta-data/") assert ec2_instance._data == {"http://169.254.169.254/latest/meta-data/whatever/my-key": "my-value"} + + +@patch(module_name + ".fetch_url") +def test__fetch_user_data_compressed(m_fetch_url, ec2_instance): + user_data = b"""Content-Type: multipart/mixed; boundary="MIMEBOUNDARY" +MIME-Version: 1.0 + +--MIMEBOUNDARY +Content-Transfer-Encoding: 7bit +Content-Type: text/cloud-config +Mime-Version: 1.0 + +packages: ['httpie'] + +--MIMEBOUNDARY-- +""" + + m_fetch_url.return_value = (io.BytesIO(gzip.compress(user_data)), {"status": 200}) + assert ec2_instance._fetch("http://169.254.169.254/latest/user-data") == user_data.decode("utf-8") + + +@patch(module_name + ".fetch_url") +def test__fetch_user_data_plain(m_fetch_url, ec2_instance): + user_data = b"""Content-Type: multipart/mixed; boundary="MIMEBOUNDARY" +MIME-Version: 1.0 + +--MIMEBOUNDARY +Content-Transfer-Encoding: 7bit +Content-Type: text/cloud-config +Mime-Version: 1.0 + +packages: ['httpie'] + +--MIMEBOUNDARY-- +""" + + m_fetch_url.return_value = (io.BytesIO(user_data), {"status": 200}) + assert ec2_instance._fetch("http://169.254.169.254/latest/user-data") == user_data.decode("utf-8")