Skip to content

Commit e9d57b8

Browse files
blackboxswServer Team CI Bot
authored andcommitted
logs: collect-logs ignore instance-data-sensitive.json on non-root user
Since /run/cloud-init/instance-data-sensitive.json is root read-only, ignore this file if non-root user runs collect-logs. If --include-userdata is provided on the command line, exit in error if non-root user attempts this operation. Lastly, update the __main__ to exit based on return value of main. LP: #1805201
1 parent ef0611a commit e9d57b8

2 files changed

Lines changed: 60 additions & 14 deletions

File tree

cloudinit/cmd/devel/logs.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
"""Define 'collect-logs' utility and handler to include in cloud-init cmd."""
66

77
import argparse
8-
from cloudinit.util import (
9-
ProcessExecutionError, chdir, copy, ensure_dir, subp, write_file)
10-
from cloudinit.temp_utils import tempdir
118
from datetime import datetime
129
import os
1310
import shutil
1411
import sys
1512

13+
from cloudinit.sources import INSTANCE_JSON_SENSITIVE_FILE
14+
from cloudinit.temp_utils import tempdir
15+
from cloudinit.util import (
16+
ProcessExecutionError, chdir, copy, ensure_dir, subp, write_file)
17+
1618

1719
CLOUDINIT_LOGS = ['/var/log/cloud-init.log', '/var/log/cloud-init-output.log']
1820
CLOUDINIT_RUN_DIR = '/run/cloud-init'
@@ -46,6 +48,13 @@ def get_parser(parser=None):
4648
return parser
4749

4850

51+
def _copytree_ignore_sensitive_files(curdir, files):
52+
"""Return a list of files to ignore if we are non-root"""
53+
if os.getuid() == 0:
54+
return ()
55+
return (INSTANCE_JSON_SENSITIVE_FILE,) # Ignore root-permissioned files
56+
57+
4958
def _write_command_output_to_file(cmd, filename, msg, verbosity):
5059
"""Helper which runs a command and writes output or error to filename."""
5160
try:
@@ -78,6 +87,11 @@ def collect_logs(tarfile, include_userdata, verbosity=0):
7887
@param tarfile: The path of the tar-gzipped file to create.
7988
@param include_userdata: Boolean, true means include user-data.
8089
"""
90+
if include_userdata and os.getuid() != 0:
91+
sys.stderr.write(
92+
"To include userdata, root user is required."
93+
" Try sudo cloud-init collect-logs\n")
94+
return 1
8195
tarfile = os.path.abspath(tarfile)
8296
date = datetime.utcnow().date().strftime('%Y-%m-%d')
8397
log_dir = 'cloud-init-logs-{0}'.format(date)
@@ -110,29 +124,30 @@ def collect_logs(tarfile, include_userdata, verbosity=0):
110124
ensure_dir(run_dir)
111125
if os.path.exists(CLOUDINIT_RUN_DIR):
112126
shutil.copytree(CLOUDINIT_RUN_DIR,
113-
os.path.join(run_dir, 'cloud-init'))
127+
os.path.join(run_dir, 'cloud-init'),
128+
ignore=_copytree_ignore_sensitive_files)
114129
_debug("collected dir %s\n" % CLOUDINIT_RUN_DIR, 1, verbosity)
115130
else:
116131
_debug("directory '%s' did not exist\n" % CLOUDINIT_RUN_DIR, 1,
117132
verbosity)
118133
with chdir(tmp_dir):
119134
subp(['tar', 'czvf', tarfile, log_dir.replace(tmp_dir + '/', '')])
120135
sys.stderr.write("Wrote %s\n" % tarfile)
136+
return 0
121137

122138

123139
def handle_collect_logs_args(name, args):
124140
"""Handle calls to 'cloud-init collect-logs' as a subcommand."""
125-
collect_logs(args.tarfile, args.userdata, args.verbosity)
141+
return collect_logs(args.tarfile, args.userdata, args.verbosity)
126142

127143

128144
def main():
129145
"""Tool to collect and tar all cloud-init related logs."""
130146
parser = get_parser()
131-
handle_collect_logs_args('collect-logs', parser.parse_args())
132-
return 0
147+
return handle_collect_logs_args('collect-logs', parser.parse_args())
133148

134149

135150
if __name__ == '__main__':
136-
main()
151+
sys.exit(main())
137152

138153
# vi: ts=4 expandtab

cloudinit/cmd/devel/tests/test_logs.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,47 @@
11
# This file is part of cloud-init. See LICENSE file for license information.
22

3-
from cloudinit.cmd.devel import logs
4-
from cloudinit.util import ensure_dir, load_file, subp, write_file
5-
from cloudinit.tests.helpers import FilesystemMockingTestCase, wrap_and_call
63
from datetime import datetime
7-
import mock
84
import os
5+
from six import StringIO
6+
7+
from cloudinit.cmd.devel import logs
8+
from cloudinit.sources import INSTANCE_JSON_SENSITIVE_FILE
9+
from cloudinit.tests.helpers import (
10+
FilesystemMockingTestCase, mock, wrap_and_call)
11+
from cloudinit.util import ensure_dir, load_file, subp, write_file
912

1013

14+
@mock.patch('cloudinit.cmd.devel.logs.os.getuid')
1115
class TestCollectLogs(FilesystemMockingTestCase):
1216

1317
def setUp(self):
1418
super(TestCollectLogs, self).setUp()
1519
self.new_root = self.tmp_dir()
1620
self.run_dir = self.tmp_path('run', self.new_root)
1721

18-
def test_collect_logs_creates_tarfile(self):
22+
def test_collect_logs_with_userdata_requires_root_user(self, m_getuid):
23+
"""collect-logs errors when non-root user collects userdata ."""
24+
m_getuid.return_value = 100 # non-root
25+
output_tarfile = self.tmp_path('logs.tgz')
26+
with mock.patch('sys.stderr', new_callable=StringIO) as m_stderr:
27+
self.assertEqual(
28+
1, logs.collect_logs(output_tarfile, include_userdata=True))
29+
self.assertEqual(
30+
'To include userdata, root user is required.'
31+
' Try sudo cloud-init collect-logs\n',
32+
m_stderr.getvalue())
33+
34+
def test_collect_logs_creates_tarfile(self, m_getuid):
1935
"""collect-logs creates a tarfile with all related cloud-init info."""
36+
m_getuid.return_value = 100
2037
log1 = self.tmp_path('cloud-init.log', self.new_root)
2138
write_file(log1, 'cloud-init-log')
2239
log2 = self.tmp_path('cloud-init-output.log', self.new_root)
2340
write_file(log2, 'cloud-init-output-log')
2441
ensure_dir(self.run_dir)
2542
write_file(self.tmp_path('results.json', self.run_dir), 'results')
43+
write_file(self.tmp_path(INSTANCE_JSON_SENSITIVE_FILE, self.run_dir),
44+
'sensitive')
2645
output_tarfile = self.tmp_path('logs.tgz')
2746

2847
date = datetime.utcnow().date().strftime('%Y-%m-%d')
@@ -59,6 +78,11 @@ def fake_subp(cmd):
5978
# unpack the tarfile and check file contents
6079
subp(['tar', 'zxvf', output_tarfile, '-C', self.new_root])
6180
out_logdir = self.tmp_path(date_logdir, self.new_root)
81+
self.assertFalse(
82+
os.path.exists(
83+
os.path.join(out_logdir, 'run', 'cloud-init',
84+
INSTANCE_JSON_SENSITIVE_FILE)),
85+
'Unexpected file found: %s' % INSTANCE_JSON_SENSITIVE_FILE)
6286
self.assertEqual(
6387
'0.7fake\n',
6488
load_file(os.path.join(out_logdir, 'dpkg-version')))
@@ -82,8 +106,9 @@ def fake_subp(cmd):
82106
os.path.join(out_logdir, 'run', 'cloud-init', 'results.json')))
83107
fake_stderr.write.assert_any_call('Wrote %s\n' % output_tarfile)
84108

85-
def test_collect_logs_includes_optional_userdata(self):
109+
def test_collect_logs_includes_optional_userdata(self, m_getuid):
86110
"""collect-logs include userdata when --include-userdata is set."""
111+
m_getuid.return_value = 0
87112
log1 = self.tmp_path('cloud-init.log', self.new_root)
88113
write_file(log1, 'cloud-init-log')
89114
log2 = self.tmp_path('cloud-init-output.log', self.new_root)
@@ -92,6 +117,8 @@ def test_collect_logs_includes_optional_userdata(self):
92117
write_file(userdata, 'user-data')
93118
ensure_dir(self.run_dir)
94119
write_file(self.tmp_path('results.json', self.run_dir), 'results')
120+
write_file(self.tmp_path(INSTANCE_JSON_SENSITIVE_FILE, self.run_dir),
121+
'sensitive')
95122
output_tarfile = self.tmp_path('logs.tgz')
96123

97124
date = datetime.utcnow().date().strftime('%Y-%m-%d')
@@ -132,4 +159,8 @@ def fake_subp(cmd):
132159
self.assertEqual(
133160
'user-data',
134161
load_file(os.path.join(out_logdir, 'user-data.txt')))
162+
self.assertEqual(
163+
'sensitive',
164+
load_file(os.path.join(out_logdir, 'run', 'cloud-init',
165+
INSTANCE_JSON_SENSITIVE_FILE)))
135166
fake_stderr.write.assert_any_call('Wrote %s\n' % output_tarfile)

0 commit comments

Comments
 (0)