Skip to content

Commit dc0be9c

Browse files
blackboxswServer Team CI Bot
authored andcommitted
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
1 parent 532ff0f commit dc0be9c

File tree

4 files changed

+126
-19
lines changed

4 files changed

+126
-19
lines changed

cloudinit/cmd/devel/render.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@
88

99
from cloudinit.handlers.jinja_template import render_jinja_payload_from_file
1010
from cloudinit import log
11-
from cloudinit.sources import INSTANCE_JSON_FILE
11+
from cloudinit.sources import INSTANCE_JSON_FILE, INSTANCE_JSON_SENSITIVE_FILE
1212
from . import addLogHandlerCLI, read_cfg_paths
1313

1414
NAME = 'render'
15-
DEFAULT_INSTANCE_DATA = '/run/cloud-init/instance-data.json'
1615

1716
LOG = log.getLogger(NAME)
1817

@@ -47,12 +46,22 @@ def handle_args(name, args):
4746
@return 0 on success, 1 on failure.
4847
"""
4948
addLogHandlerCLI(LOG, log.DEBUG if args.debug else log.WARNING)
50-
if not args.instance_data:
51-
paths = read_cfg_paths()
52-
instance_data_fn = os.path.join(
53-
paths.run_dir, INSTANCE_JSON_FILE)
54-
else:
49+
if args.instance_data:
5550
instance_data_fn = args.instance_data
51+
else:
52+
paths = read_cfg_paths()
53+
uid = os.getuid()
54+
redacted_data_fn = os.path.join(paths.run_dir, INSTANCE_JSON_FILE)
55+
if uid == 0:
56+
instance_data_fn = os.path.join(
57+
paths.run_dir, INSTANCE_JSON_SENSITIVE_FILE)
58+
if not os.path.exists(instance_data_fn):
59+
LOG.warning(
60+
'Missing root-readable %s. Using redacted %s instead.',
61+
instance_data_fn, redacted_data_fn)
62+
instance_data_fn = redacted_data_fn
63+
else:
64+
instance_data_fn = redacted_data_fn
5665
if not os.path.exists(instance_data_fn):
5766
LOG.error('Missing instance-data.json file: %s', instance_data_fn)
5867
return 1

cloudinit/cmd/devel/tests/test_render.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from collections import namedtuple
77
from cloudinit.cmd.devel import render
88
from cloudinit.helpers import Paths
9-
from cloudinit.sources import INSTANCE_JSON_FILE
9+
from cloudinit.sources import INSTANCE_JSON_FILE, INSTANCE_JSON_SENSITIVE_FILE
1010
from cloudinit.tests.helpers import CiTestCase, mock, skipUnlessJinja
1111
from cloudinit.util import ensure_dir, write_file
1212

@@ -63,6 +63,49 @@ def test_handle_args_defaults_instance_data(self):
6363
'Missing instance-data.json file: %s' % json_file,
6464
self.logs.getvalue())
6565

66+
def test_handle_args_root_fallback_from_sensitive_instance_data(self):
67+
"""When root user defaults to sensitive.json."""
68+
user_data = self.tmp_path('user-data', dir=self.tmp)
69+
run_dir = self.tmp_path('run_dir', dir=self.tmp)
70+
ensure_dir(run_dir)
71+
paths = Paths({'run_dir': run_dir})
72+
self.add_patch('cloudinit.cmd.devel.render.read_cfg_paths', 'm_paths')
73+
self.m_paths.return_value = paths
74+
args = self.args(
75+
user_data=user_data, instance_data=None, debug=False)
76+
with mock.patch('sys.stderr', new_callable=StringIO):
77+
with mock.patch('os.getuid') as m_getuid:
78+
m_getuid.return_value = 0
79+
self.assertEqual(1, render.handle_args('anyname', args))
80+
json_file = os.path.join(run_dir, INSTANCE_JSON_FILE)
81+
json_sensitive = os.path.join(run_dir, INSTANCE_JSON_SENSITIVE_FILE)
82+
self.assertIn(
83+
'WARNING: Missing root-readable %s. Using redacted %s' % (
84+
json_sensitive, json_file), self.logs.getvalue())
85+
self.assertIn(
86+
'ERROR: Missing instance-data.json file: %s' % json_file,
87+
self.logs.getvalue())
88+
89+
def test_handle_args_root_uses_sensitive_instance_data(self):
90+
"""When root user, and no instance-data arg, use sensitive.json."""
91+
user_data = self.tmp_path('user-data', dir=self.tmp)
92+
write_file(user_data, '##template: jinja\nrendering: {{ my_var }}')
93+
run_dir = self.tmp_path('run_dir', dir=self.tmp)
94+
ensure_dir(run_dir)
95+
json_sensitive = os.path.join(run_dir, INSTANCE_JSON_SENSITIVE_FILE)
96+
write_file(json_sensitive, '{"my-var": "jinja worked"}')
97+
paths = Paths({'run_dir': run_dir})
98+
self.add_patch('cloudinit.cmd.devel.render.read_cfg_paths', 'm_paths')
99+
self.m_paths.return_value = paths
100+
args = self.args(
101+
user_data=user_data, instance_data=None, debug=False)
102+
with mock.patch('sys.stderr', new_callable=StringIO):
103+
with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
104+
with mock.patch('os.getuid') as m_getuid:
105+
m_getuid.return_value = 0
106+
self.assertEqual(0, render.handle_args('anyname', args))
107+
self.assertIn('rendering: jinja worked', m_stdout.getvalue())
108+
66109
@skipUnlessJinja()
67110
def test_handle_args_renders_instance_data_vars_in_template(self):
68111
"""If user_data file is a jinja template render instance-data vars."""

cloudinit/cmd/query.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,22 +79,30 @@ def handle_args(name, args):
7979
uid = os.getuid()
8080
if not all([args.instance_data, args.user_data, args.vendor_data]):
8181
paths = read_cfg_paths()
82-
if not args.instance_data:
82+
if args.instance_data:
83+
instance_data_fn = args.instance_data
84+
else:
85+
redacted_data_fn = os.path.join(paths.run_dir, INSTANCE_JSON_FILE)
8386
if uid == 0:
84-
default_json_fn = INSTANCE_JSON_SENSITIVE_FILE
87+
sensitive_data_fn = os.path.join(
88+
paths.run_dir, INSTANCE_JSON_SENSITIVE_FILE)
89+
if os.path.exists(sensitive_data_fn):
90+
instance_data_fn = sensitive_data_fn
91+
else:
92+
LOG.warning(
93+
'Missing root-readable %s. Using redacted %s instead.',
94+
sensitive_data_fn, redacted_data_fn)
95+
instance_data_fn = redacted_data_fn
8596
else:
86-
default_json_fn = INSTANCE_JSON_FILE # World readable
87-
instance_data_fn = os.path.join(paths.run_dir, default_json_fn)
97+
instance_data_fn = redacted_data_fn
98+
if args.user_data:
99+
user_data_fn = args.user_data
88100
else:
89-
instance_data_fn = args.instance_data
90-
if not args.user_data:
91101
user_data_fn = os.path.join(paths.instance_link, 'user-data.txt')
102+
if args.vendor_data:
103+
vendor_data_fn = args.vendor_data
92104
else:
93-
user_data_fn = args.user_data
94-
if not args.vendor_data:
95105
vendor_data_fn = os.path.join(paths.instance_link, 'vendor-data.txt')
96-
else:
97-
vendor_data_fn = args.vendor_data
98106

99107
try:
100108
instance_json = util.load_file(instance_data_fn)

cloudinit/cmd/tests/test_query.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from collections import namedtuple
88
from cloudinit.cmd import query
99
from cloudinit.helpers import Paths
10-
from cloudinit.sources import REDACT_SENSITIVE_VALUE, INSTANCE_JSON_FILE
10+
from cloudinit.sources import (
11+
REDACT_SENSITIVE_VALUE, INSTANCE_JSON_FILE, INSTANCE_JSON_SENSITIVE_FILE)
1112
from cloudinit.tests.helpers import CiTestCase, mock
1213
from cloudinit.util import ensure_dir, write_file
1314

@@ -76,6 +77,52 @@ def test_handle_args_defaults_instance_data(self):
7677
'ERROR: Missing instance-data.json file: %s' % json_file,
7778
m_stderr.getvalue())
7879

80+
def test_handle_args_root_fallsback_to_instance_data(self):
81+
"""When no instance_data argument, root falls back to redacted json."""
82+
args = self.args(
83+
debug=False, dump_all=True, format=None, instance_data=None,
84+
list_keys=False, user_data=None, vendor_data=None, varname=None)
85+
run_dir = self.tmp_path('run_dir', dir=self.tmp)
86+
ensure_dir(run_dir)
87+
paths = Paths({'run_dir': run_dir})
88+
self.add_patch('cloudinit.cmd.query.read_cfg_paths', 'm_paths')
89+
self.m_paths.return_value = paths
90+
with mock.patch('sys.stderr', new_callable=StringIO) as m_stderr:
91+
with mock.patch('os.getuid') as m_getuid:
92+
m_getuid.return_value = 0
93+
self.assertEqual(1, query.handle_args('anyname', args))
94+
json_file = os.path.join(run_dir, INSTANCE_JSON_FILE)
95+
sensitive_file = os.path.join(run_dir, INSTANCE_JSON_SENSITIVE_FILE)
96+
self.assertIn(
97+
'WARNING: Missing root-readable %s. Using redacted %s instead.' % (
98+
sensitive_file, json_file),
99+
m_stderr.getvalue())
100+
101+
def test_handle_args_root_uses_instance_sensitive_data(self):
102+
"""When no instance_data argument, root uses semsitive json."""
103+
user_data = self.tmp_path('user-data', dir=self.tmp)
104+
vendor_data = self.tmp_path('vendor-data', dir=self.tmp)
105+
write_file(user_data, 'ud')
106+
write_file(vendor_data, 'vd')
107+
run_dir = self.tmp_path('run_dir', dir=self.tmp)
108+
sensitive_file = os.path.join(run_dir, INSTANCE_JSON_SENSITIVE_FILE)
109+
write_file(sensitive_file, '{"my-var": "it worked"}')
110+
ensure_dir(run_dir)
111+
paths = Paths({'run_dir': run_dir})
112+
self.add_patch('cloudinit.cmd.query.read_cfg_paths', 'm_paths')
113+
self.m_paths.return_value = paths
114+
args = self.args(
115+
debug=False, dump_all=True, format=None, instance_data=None,
116+
list_keys=False, user_data=vendor_data, vendor_data=vendor_data,
117+
varname=None)
118+
with mock.patch('sys.stdout', new_callable=StringIO) as m_stdout:
119+
with mock.patch('os.getuid') as m_getuid:
120+
m_getuid.return_value = 0
121+
self.assertEqual(0, query.handle_args('anyname', args))
122+
self.assertEqual(
123+
'{\n "my_var": "it worked",\n "userdata": "vd",\n '
124+
'"vendordata": "vd"\n}\n', m_stdout.getvalue())
125+
79126
def test_handle_args_dumps_all_instance_data(self):
80127
"""When --all is specified query will dump all instance data vars."""
81128
write_file(self.instance_data, '{"my-var": "it worked"}')

0 commit comments

Comments
 (0)