|
|
1badc6 |
From 5a3cd50df652e4a70f85ccc712dc11bf9726adda Mon Sep 17 00:00:00 2001
|
|
|
1badc6 |
From: Eduardo Otubo <otubo@redhat.com>
|
|
|
1badc6 |
Date: Wed, 16 Oct 2019 12:10:24 +0200
|
|
|
1badc6 |
Subject: [PATCH] util: json.dumps on python 2.7 will handle UnicodeDecodeError
|
|
|
1badc6 |
on binary
|
|
|
1badc6 |
|
|
|
1badc6 |
RH-Author: Eduardo Otubo <otubo@redhat.com>
|
|
|
1badc6 |
Message-id: <20191016121024.23694-1-otubo@redhat.com>
|
|
|
1badc6 |
Patchwork-id: 91812
|
|
|
1badc6 |
O-Subject: [RHEL-7.8/RHEL-8.1.0 cloud-init PATCH] util: json.dumps on python 2.7 will handle UnicodeDecodeError on binary
|
|
|
1badc6 |
Bugzilla: 1744526
|
|
|
1badc6 |
RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
|
|
|
1badc6 |
RH-Acked-by: Mohammed Gamal <mgamal@redhat.com>
|
|
|
1badc6 |
|
|
|
1badc6 |
commit 067516d7bc917e4921b9f1424b7a64e92cae0ad2
|
|
|
1badc6 |
Author: Chad Smith <chad.smith@canonical.com>
|
|
|
1badc6 |
Date: Fri Sep 27 20:46:00 2019 +0000
|
|
|
1badc6 |
|
|
|
1badc6 |
util: json.dumps on python 2.7 will handle UnicodeDecodeError on binary
|
|
|
1badc6 |
|
|
|
1badc6 |
Since python 2.7 doesn't handle UnicodeDecodeErrors with the default
|
|
|
1badc6 |
handler
|
|
|
1badc6 |
|
|
|
1badc6 |
LP: #1801364
|
|
|
1badc6 |
|
|
|
1badc6 |
Signed-off-by: Eduardo Otubo <otubo@redhat.com>
|
|
|
1badc6 |
Signed-off-by: Miroslav Rezanina <mrezanin@redhat.com>
|
|
|
1badc6 |
---
|
|
|
1badc6 |
cloudinit/sources/tests/test_init.py | 12 +++++-------
|
|
|
1badc6 |
cloudinit/tests/test_util.py | 20 ++++++++++++++++++++
|
|
|
1badc6 |
cloudinit/util.py | 27 +++++++++++++++++++++++++--
|
|
|
1badc6 |
3 files changed, 50 insertions(+), 9 deletions(-)
|
|
|
1badc6 |
|
|
|
1badc6 |
diff --git a/cloudinit/sources/tests/test_init.py b/cloudinit/sources/tests/test_init.py
|
|
|
1badc6 |
index 6378e98..9698261 100644
|
|
|
1badc6 |
--- a/cloudinit/sources/tests/test_init.py
|
|
|
1badc6 |
+++ b/cloudinit/sources/tests/test_init.py
|
|
|
1badc6 |
@@ -457,19 +457,17 @@ class TestDataSource(CiTestCase):
|
|
|
1badc6 |
instance_json['ds']['meta_data'])
|
|
|
1badc6 |
|
|
|
1badc6 |
@skipIf(not six.PY2, "Only python2 hits UnicodeDecodeErrors on non-utf8")
|
|
|
1badc6 |
- def test_non_utf8_encoding_logs_warning(self):
|
|
|
1badc6 |
- """When non-utf-8 values exist in py2 instance-data is not written."""
|
|
|
1badc6 |
+ def test_non_utf8_encoding_gets_b64encoded(self):
|
|
|
1badc6 |
+ """When non-utf-8 values exist in py2 instance-data is b64encoded."""
|
|
|
1badc6 |
tmp = self.tmp_dir()
|
|
|
1badc6 |
datasource = DataSourceTestSubclassNet(
|
|
|
1badc6 |
self.sys_cfg, self.distro, Paths({'run_dir': tmp}),
|
|
|
1badc6 |
custom_metadata={'key1': 'val1', 'key2': {'key2.1': b'ab\xaadef'}})
|
|
|
1badc6 |
self.assertTrue(datasource.get_data())
|
|
|
1badc6 |
json_file = self.tmp_path(INSTANCE_JSON_FILE, tmp)
|
|
|
1badc6 |
- self.assertFalse(os.path.exists(json_file))
|
|
|
1badc6 |
- self.assertIn(
|
|
|
1badc6 |
- "WARNING: Error persisting instance-data.json: 'utf8' codec can't"
|
|
|
1badc6 |
- " decode byte 0xaa in position 2: invalid start byte",
|
|
|
1badc6 |
- self.logs.getvalue())
|
|
|
1badc6 |
+ instance_json = util.load_json(util.load_file(json_file))
|
|
|
1badc6 |
+ key21_value = instance_json['ds']['meta_data']['key2']['key2.1']
|
|
|
1badc6 |
+ self.assertEqual('ci-b64:' + util.b64e(b'ab\xaadef'), key21_value)
|
|
|
1badc6 |
|
|
|
1badc6 |
def test_get_hostname_subclass_support(self):
|
|
|
1badc6 |
"""Validate get_hostname signature on all subclasses of DataSource."""
|
|
|
1badc6 |
diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py
|
|
|
1badc6 |
index e3d2dba..f4f95e9 100644
|
|
|
1badc6 |
--- a/cloudinit/tests/test_util.py
|
|
|
1badc6 |
+++ b/cloudinit/tests/test_util.py
|
|
|
1badc6 |
@@ -2,7 +2,9 @@
|
|
|
1badc6 |
|
|
|
1badc6 |
"""Tests for cloudinit.util"""
|
|
|
1badc6 |
|
|
|
1badc6 |
+import base64
|
|
|
1badc6 |
import logging
|
|
|
1badc6 |
+import json
|
|
|
1badc6 |
import platform
|
|
|
1badc6 |
|
|
|
1badc6 |
import cloudinit.util as util
|
|
|
1badc6 |
@@ -528,6 +530,24 @@ class TestGetLinuxDistro(CiTestCase):
|
|
|
1badc6 |
self.assertEqual(('foo', '1.1', 'aarch64'), dist)
|
|
|
1badc6 |
|
|
|
1badc6 |
|
|
|
1badc6 |
+class TestJsonDumps(CiTestCase):
|
|
|
1badc6 |
+ def test_is_str(self):
|
|
|
1badc6 |
+ """json_dumps should return a string."""
|
|
|
1badc6 |
+ self.assertTrue(isinstance(util.json_dumps({'abc': '123'}), str))
|
|
|
1badc6 |
+
|
|
|
1badc6 |
+ def test_utf8(self):
|
|
|
1badc6 |
+ smiley = '\\ud83d\\ude03'
|
|
|
1badc6 |
+ self.assertEqual(
|
|
|
1badc6 |
+ {'smiley': smiley},
|
|
|
1badc6 |
+ json.loads(util.json_dumps({'smiley': smiley})))
|
|
|
1badc6 |
+
|
|
|
1badc6 |
+ def test_non_utf8(self):
|
|
|
1badc6 |
+ blob = b'\xba\x03Qx-#y\xea'
|
|
|
1badc6 |
+ self.assertEqual(
|
|
|
1badc6 |
+ {'blob': 'ci-b64:' + base64.b64encode(blob).decode('utf-8')},
|
|
|
1badc6 |
+ json.loads(util.json_dumps({'blob': blob})))
|
|
|
1badc6 |
+
|
|
|
1badc6 |
+
|
|
|
1badc6 |
@mock.patch('os.path.exists')
|
|
|
1badc6 |
class TestIsLXD(CiTestCase):
|
|
|
1badc6 |
|
|
|
1badc6 |
diff --git a/cloudinit/util.py b/cloudinit/util.py
|
|
|
1badc6 |
index a84112a..2c9ac66 100644
|
|
|
1badc6 |
--- a/cloudinit/util.py
|
|
|
1badc6 |
+++ b/cloudinit/util.py
|
|
|
1badc6 |
@@ -1590,10 +1590,33 @@ def json_serialize_default(_obj):
|
|
|
1badc6 |
return 'Warning: redacted unserializable type {0}'.format(type(_obj))
|
|
|
1badc6 |
|
|
|
1badc6 |
|
|
|
1badc6 |
+def json_preserialize_binary(data):
|
|
|
1badc6 |
+ """Preserialize any discovered binary values to avoid json.dumps issues.
|
|
|
1badc6 |
+
|
|
|
1badc6 |
+ Used only on python 2.7 where default type handling is not honored for
|
|
|
1badc6 |
+ failure to encode binary data. LP: #1801364.
|
|
|
1badc6 |
+ TODO(Drop this function when py2.7 support is dropped from cloud-init)
|
|
|
1badc6 |
+ """
|
|
|
1badc6 |
+ data = obj_copy.deepcopy(data)
|
|
|
1badc6 |
+ for key, value in data.items():
|
|
|
1badc6 |
+ if isinstance(value, (dict)):
|
|
|
1badc6 |
+ data[key] = json_preserialize_binary(value)
|
|
|
1badc6 |
+ if isinstance(value, bytes):
|
|
|
1badc6 |
+ data[key] = 'ci-b64:{0}'.format(b64e(value))
|
|
|
1badc6 |
+ return data
|
|
|
1badc6 |
+
|
|
|
1badc6 |
+
|
|
|
1badc6 |
def json_dumps(data):
|
|
|
1badc6 |
"""Return data in nicely formatted json."""
|
|
|
1badc6 |
- return json.dumps(data, indent=1, sort_keys=True,
|
|
|
1badc6 |
- separators=(',', ': '), default=json_serialize_default)
|
|
|
1badc6 |
+ try:
|
|
|
1badc6 |
+ return json.dumps(
|
|
|
1badc6 |
+ data, indent=1, sort_keys=True, separators=(',', ': '),
|
|
|
1badc6 |
+ default=json_serialize_default)
|
|
|
1badc6 |
+ except UnicodeDecodeError:
|
|
|
1badc6 |
+ if sys.version_info[:2] == (2, 7):
|
|
|
1badc6 |
+ data = json_preserialize_binary(data)
|
|
|
1badc6 |
+ return json.dumps(data)
|
|
|
1badc6 |
+ raise
|
|
|
1badc6 |
|
|
|
1badc6 |
|
|
|
1badc6 |
def yaml_dumps(obj, explicit_start=True, explicit_end=True):
|
|
|
1badc6 |
--
|
|
|
1badc6 |
1.8.3.1
|
|
|
1badc6 |
|