Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ec2_metadata_facts - Handle decompression when EC2 instance user-data is compressed #1575

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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).
tremble marked this conversation as resolved.
Show resolved Hide resolved
34 changes: 34 additions & 0 deletions plugins/modules/ec2_metadata_facts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
tremble marked this conversation as resolved.
Show resolved Hide resolved
# 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):
Expand All @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/plugins/modules/test_ec2_metadata_facts.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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")