Merge ~chad.smith/cloud-init:bug/1798189-instance-data-fallback into cloud-init:master

Proposed by Chad Smith
Status: Merged
Approved by: Scott Moser
Approved revision: 5900f42a3ae3ed2c5fd90dbbccb037e15d7d5966
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~chad.smith/cloud-init:bug/1798189-instance-data-fallback
Merge into: cloud-init:master
Diff against target: 220 lines (+126/-19)
4 files modified
cloudinit/cmd/devel/render.py (+16/-7)
cloudinit/cmd/devel/tests/test_render.py (+44/-1)
cloudinit/cmd/query.py (+18/-10)
cloudinit/cmd/tests/test_query.py (+48/-1)
Reviewer Review Type Date Requested Status
Scott Moser Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+357793@code.launchpad.net

Commit message

instance-data: fallback to instance-data.json if sensitive is absent.

On cloud-init upgrade path from 18.3 to 18.4 cloud-init changed how
instance-data is written. Cloud-init changes instance-data.json from root
read-only to redacted world-readable content, and provided a separate
unredacted instance-data-sensitive.json which is read-only root.
Since instance-data is only rewritten from cache on
reboot, the query and render tools needed fallback to use the 'old'
instance-data.json if the new sensitive file isn't yet present.

This avoids error messages from tools about an absebt
/run/instance-data-sensitive.json file.

LP: #1798189

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:5900f42a3ae3ed2c5fd90dbbccb037e15d7d5966
https://jenkins.ubuntu.com/server/job/cloud-init-ci/411/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/411/rebuild

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/cmd/devel/render.py b/cloudinit/cmd/devel/render.py
2index 2ba6b68..4d3ec95 100755
3--- a/cloudinit/cmd/devel/render.py
4+++ b/cloudinit/cmd/devel/render.py
5@@ -8,11 +8,10 @@ import sys
6
7 from cloudinit.handlers.jinja_template import render_jinja_payload_from_file
8 from cloudinit import log
9-from cloudinit.sources import INSTANCE_JSON_FILE
10+from cloudinit.sources import INSTANCE_JSON_FILE, INSTANCE_JSON_SENSITIVE_FILE
11 from . import addLogHandlerCLI, read_cfg_paths
12
13 NAME = 'render'
14-DEFAULT_INSTANCE_DATA = '/run/cloud-init/instance-data.json'
15
16 LOG = log.getLogger(NAME)
17
18@@ -47,12 +46,22 @@ def handle_args(name, args):
19 @return 0 on success, 1 on failure.
20 """
21 addLogHandlerCLI(LOG, log.DEBUG if args.debug else log.WARNING)
22- if not args.instance_data:
23- paths = read_cfg_paths()
24- instance_data_fn = os.path.join(
25- paths.run_dir, INSTANCE_JSON_FILE)
26- else:
27+ if args.instance_data:
28 instance_data_fn = args.instance_data
29+ else:
30+ paths = read_cfg_paths()
31+ uid = os.getuid()
32+ redacted_data_fn = os.path.join(paths.run_dir, INSTANCE_JSON_FILE)
33+ if uid == 0:
34+ instance_data_fn = os.path.join(
35+ paths.run_dir, INSTANCE_JSON_SENSITIVE_FILE)
36+ if not os.path.exists(instance_data_fn):
37+ LOG.warning(
38+ 'Missing root-readable %s. Using redacted %s instead.',
39+ instance_data_fn, redacted_data_fn)
40+ instance_data_fn = redacted_data_fn
41+ else:
42+ instance_data_fn = redacted_data_fn
43 if not os.path.exists(instance_data_fn):
44 LOG.error('Missing instance-data.json file: %s', instance_data_fn)
45 return 1
46diff --git a/cloudinit/cmd/devel/tests/test_render.py b/cloudinit/cmd/devel/tests/test_render.py
47index fc5d2c0..988bba0 100644
48--- a/cloudinit/cmd/devel/tests/test_render.py
49+++ b/cloudinit/cmd/devel/tests/test_render.py
50@@ -6,7 +6,7 @@ import os
51 from collections import namedtuple
52 from cloudinit.cmd.devel import render
53 from cloudinit.helpers import Paths
54-from cloudinit.sources import INSTANCE_JSON_FILE
55+from cloudinit.sources import INSTANCE_JSON_FILE, INSTANCE_JSON_SENSITIVE_FILE
56 from cloudinit.tests.helpers import CiTestCase, mock, skipUnlessJinja
57 from cloudinit.util import ensure_dir, write_file
58
59@@ -63,6 +63,49 @@ class TestRender(CiTestCase):
60 'Missing instance-data.json file: %s' % json_file,
61 self.logs.getvalue())
62
63+ def test_handle_args_root_fallback_from_sensitive_instance_data(self):
64+ """When root user defaults to sensitive.json."""
65+ user_data = self.tmp_path('user-data', dir=self.tmp)
66+ run_dir = self.tmp_path('run_dir', dir=self.tmp)
67+ ensure_dir(run_dir)
68+ paths = Paths({'run_dir': run_dir})
69+ self.add_patch('cloudinit.cmd.devel.render.read_cfg_paths', 'm_paths')
70+ self.m_paths.return_value = paths
71+ args = self.args(
72+ user_data=user_data, instance_data=None, debug=False)
73+ with mock.patch('sys.stderr', new_callable=StringIO):
74+ with mock.patch('os.getuid') as m_getuid:
75+ m_getuid.return_value = 0
76+ self.assertEqual(1, render.handle_args('anyname', args))
77+ json_file = os.path.join(run_dir, INSTANCE_JSON_FILE)
78+ json_sensitive = os.path.join(run_dir, INSTANCE_JSON_SENSITIVE_FILE)
79+ self.assertIn(
80+ 'WARNING: Missing root-readable %s. Using redacted %s' % (
81+ json_sensitive, json_file), self.logs.getvalue())
82+ self.assertIn(
83+ 'ERROR: Missing instance-data.json file: %s' % json_file,
84+ self.logs.getvalue())
85+
86+ def test_handle_args_root_uses_sensitive_instance_data(self):
87+ """When root user, and no instance-data arg, use sensitive.json."""
88+ user_data = self.tmp_path('user-data', dir=self.tmp)
89+ write_file(user_data, '##template: jinja\nrendering: {{ my_var }}')
90+ run_dir = self.tmp_path('run_dir', dir=self.tmp)
91+ ensure_dir(run_dir)
92+ json_sensitive = os.path.join(run_dir, INSTANCE_JSON_SENSITIVE_FILE)
93+ write_file(json_sensitive, '{"my-var": "jinja worked"}')
94+ paths = Paths({'run_dir': run_dir})
95+ self.add_patch('cloudinit.cmd.devel.render.read_cfg_paths', 'm_paths')
96+ self.m_paths.return_value = paths
97+ args = self.args(
98+ user_data=user_data, instance_data=None, debug=False)
99+ with mock.patch('sys.stderr', new_callable=StringIO):
100+ with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
101+ with mock.patch('os.getuid') as m_getuid:
102+ m_getuid.return_value = 0
103+ self.assertEqual(0, render.handle_args('anyname', args))
104+ self.assertIn('rendering: jinja worked', m_stdout.getvalue())
105+
106 @skipUnlessJinja()
107 def test_handle_args_renders_instance_data_vars_in_template(self):
108 """If user_data file is a jinja template render instance-data vars."""
109diff --git a/cloudinit/cmd/query.py b/cloudinit/cmd/query.py
110index 7d2d4fe..ff03de9 100644
111--- a/cloudinit/cmd/query.py
112+++ b/cloudinit/cmd/query.py
113@@ -79,22 +79,30 @@ def handle_args(name, args):
114 uid = os.getuid()
115 if not all([args.instance_data, args.user_data, args.vendor_data]):
116 paths = read_cfg_paths()
117- if not args.instance_data:
118+ if args.instance_data:
119+ instance_data_fn = args.instance_data
120+ else:
121+ redacted_data_fn = os.path.join(paths.run_dir, INSTANCE_JSON_FILE)
122 if uid == 0:
123- default_json_fn = INSTANCE_JSON_SENSITIVE_FILE
124+ sensitive_data_fn = os.path.join(
125+ paths.run_dir, INSTANCE_JSON_SENSITIVE_FILE)
126+ if os.path.exists(sensitive_data_fn):
127+ instance_data_fn = sensitive_data_fn
128+ else:
129+ LOG.warning(
130+ 'Missing root-readable %s. Using redacted %s instead.',
131+ sensitive_data_fn, redacted_data_fn)
132+ instance_data_fn = redacted_data_fn
133 else:
134- default_json_fn = INSTANCE_JSON_FILE # World readable
135- instance_data_fn = os.path.join(paths.run_dir, default_json_fn)
136+ instance_data_fn = redacted_data_fn
137+ if args.user_data:
138+ user_data_fn = args.user_data
139 else:
140- instance_data_fn = args.instance_data
141- if not args.user_data:
142 user_data_fn = os.path.join(paths.instance_link, 'user-data.txt')
143+ if args.vendor_data:
144+ vendor_data_fn = args.vendor_data
145 else:
146- user_data_fn = args.user_data
147- if not args.vendor_data:
148 vendor_data_fn = os.path.join(paths.instance_link, 'vendor-data.txt')
149- else:
150- vendor_data_fn = args.vendor_data
151
152 try:
153 instance_json = util.load_file(instance_data_fn)
154diff --git a/cloudinit/cmd/tests/test_query.py b/cloudinit/cmd/tests/test_query.py
155index fb87c6a..241f541 100644
156--- a/cloudinit/cmd/tests/test_query.py
157+++ b/cloudinit/cmd/tests/test_query.py
158@@ -7,7 +7,8 @@ import os
159 from collections import namedtuple
160 from cloudinit.cmd import query
161 from cloudinit.helpers import Paths
162-from cloudinit.sources import REDACT_SENSITIVE_VALUE, INSTANCE_JSON_FILE
163+from cloudinit.sources import (
164+ REDACT_SENSITIVE_VALUE, INSTANCE_JSON_FILE, INSTANCE_JSON_SENSITIVE_FILE)
165 from cloudinit.tests.helpers import CiTestCase, mock
166 from cloudinit.util import ensure_dir, write_file
167
168@@ -76,6 +77,52 @@ class TestQuery(CiTestCase):
169 'ERROR: Missing instance-data.json file: %s' % json_file,
170 m_stderr.getvalue())
171
172+ def test_handle_args_root_fallsback_to_instance_data(self):
173+ """When no instance_data argument, root falls back to redacted json."""
174+ args = self.args(
175+ debug=False, dump_all=True, format=None, instance_data=None,
176+ list_keys=False, user_data=None, vendor_data=None, varname=None)
177+ run_dir = self.tmp_path('run_dir', dir=self.tmp)
178+ ensure_dir(run_dir)
179+ paths = Paths({'run_dir': run_dir})
180+ self.add_patch('cloudinit.cmd.query.read_cfg_paths', 'm_paths')
181+ self.m_paths.return_value = paths
182+ with mock.patch('sys.stderr', new_callable=StringIO) as m_stderr:
183+ with mock.patch('os.getuid') as m_getuid:
184+ m_getuid.return_value = 0
185+ self.assertEqual(1, query.handle_args('anyname', args))
186+ json_file = os.path.join(run_dir, INSTANCE_JSON_FILE)
187+ sensitive_file = os.path.join(run_dir, INSTANCE_JSON_SENSITIVE_FILE)
188+ self.assertIn(
189+ 'WARNING: Missing root-readable %s. Using redacted %s instead.' % (
190+ sensitive_file, json_file),
191+ m_stderr.getvalue())
192+
193+ def test_handle_args_root_uses_instance_sensitive_data(self):
194+ """When no instance_data argument, root uses semsitive json."""
195+ user_data = self.tmp_path('user-data', dir=self.tmp)
196+ vendor_data = self.tmp_path('vendor-data', dir=self.tmp)
197+ write_file(user_data, 'ud')
198+ write_file(vendor_data, 'vd')
199+ run_dir = self.tmp_path('run_dir', dir=self.tmp)
200+ sensitive_file = os.path.join(run_dir, INSTANCE_JSON_SENSITIVE_FILE)
201+ write_file(sensitive_file, '{"my-var": "it worked"}')
202+ ensure_dir(run_dir)
203+ paths = Paths({'run_dir': run_dir})
204+ self.add_patch('cloudinit.cmd.query.read_cfg_paths', 'm_paths')
205+ self.m_paths.return_value = paths
206+ args = self.args(
207+ debug=False, dump_all=True, format=None, instance_data=None,
208+ list_keys=False, user_data=vendor_data, vendor_data=vendor_data,
209+ varname=None)
210+ with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
211+ with mock.patch('os.getuid') as m_getuid:
212+ m_getuid.return_value = 0
213+ self.assertEqual(0, query.handle_args('anyname', args))
214+ self.assertEqual(
215+ '{\n "my_var": "it worked",\n "userdata": "vd",\n '
216+ '"vendordata": "vd"\n}\n', m_stdout.getvalue())
217+
218 def test_handle_args_dumps_all_instance_data(self):
219 """When --all is specified query will dump all instance data vars."""
220 write_file(self.instance_data, '{"my-var": "it worked"}')

Subscribers

People subscribed via source and target branches