Merge ~raharper/cloud-init:feature/cloudinit-clean-from-write-log into cloud-init:master
- Git
- lp:~raharper/cloud-init
- feature/cloudinit-clean-from-write-log
- Merge into master
Status: | Work in progress |
---|---|
Proposed branch: | ~raharper/cloud-init:feature/cloudinit-clean-from-write-log |
Merge into: | cloud-init:master |
Diff against target: |
328 lines (+154/-7) 5 files modified
cloudinit/cmd/clean.py (+27/-4) cloudinit/cmd/tests/test_clean.py (+28/-3) cloudinit/tests/helpers.py (+2/-0) cloudinit/util.py (+52/-0) tests/unittests/test_util.py (+45/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
cloud-init Commiters | Pending | ||
Review via email: mp+372946@code.launchpad.net |
Commit message
clean: add a write_file audit log and use it to clean artifacts
Cloud-init uses util.write_file() in many places to write files
during cloud-init execution. This branch will create an audit log
in /var/lib/
which captures information about each write.
As a user of this newly created log, cloud-init clean subcommand
reads this data (if present) and augments the list of artifacts to
clean.
Description of the change
Server Team CI bot (server-team-bot) wrote : | # |
- 65b8808... by Ryan Harper
-
Handle write_file_
log_append failures
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:65b88088e1a
https:/
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:/
Scott Moser (smoser) wrote : | # |
Some thoughts:
a.) Why? Why do two writes for every write?
b.) currently atomic write_file is not covered.
Ryan Harper (raharper) wrote : | # |
For (a) what would you suggest instead?
I'll look at (b).
Scott Moser (smoser) wrote : | # |
For (a), I suggest maybe writing exactly one write for every write?
Why do you need a log of files' you've written? Should we have a separate log for:
- files opened?
- files checked for existence?
- commands run?
- log files written?
What is motivating you to have this list of files that cloud-init wrote? Realize that in regard to usefulness, it will likely be incomplete. For example, /var/log/
We already have a log file which I find to be way too verbose, and a "reporting" mechanism. Do we really need more logging?
I suspect that you're after something here, but what?
Ryan Harper (raharper) wrote : | # |
We're looking to have cloud-init clean remove as many of the artifacts that cloud-init is responsible for creating. Since clean already removes things in cloud paths, like /var/lib/cloud but we do touch other files outside of those paths and logging what files we wrote makes clean-up of those files easy.
Scott Moser (smoser) wrote : | # |
I think that this will not really get you what you're after.
Having a list of the files that cloud-init created or appended to or truncated isn't going to get you to your goal. The partial solution comes at the cost of 2 open and writes for every write. Additionally, we're already writing messages with the same information to the log. You could just parse the log to get the same information, possibly making it more machine friendly if you need to.
I think I'd prefer a solution that could at least get us to the goal.
Unmerged commits
- 65b8808... by Ryan Harper
-
Handle write_file_
log_append failures - 6b005ae... by Ryan Harper
-
cmd/clean: leverage write_file log to clean additional files
- 4837140... by Ryan Harper
-
Switch to json format for records, add unittests
- 46f3fe9... by Ryan Harper
-
Write file log intial commit
Preview Diff
1 | diff --git a/cloudinit/cmd/clean.py b/cloudinit/cmd/clean.py | |||
2 | index 30e49de..06f7daa 100644 | |||
3 | --- a/cloudinit/cmd/clean.py | |||
4 | +++ b/cloudinit/cmd/clean.py | |||
5 | @@ -12,7 +12,7 @@ import sys | |||
6 | 12 | from cloudinit.stages import Init | 12 | from cloudinit.stages import Init |
7 | 13 | from cloudinit.util import ( | 13 | from cloudinit.util import ( |
8 | 14 | ProcessExecutionError, del_dir, del_file, get_config_logfiles, | 14 | ProcessExecutionError, del_dir, del_file, get_config_logfiles, |
10 | 15 | is_link, subp) | 15 | is_link, subp, write_file_log_read) |
11 | 16 | 16 | ||
12 | 17 | 17 | ||
13 | 18 | def error(msg): | 18 | def error(msg): |
14 | @@ -45,7 +45,20 @@ def get_parser(parser=None): | |||
15 | 45 | return parser | 45 | return parser |
16 | 46 | 46 | ||
17 | 47 | 47 | ||
19 | 48 | def remove_artifacts(remove_logs, remove_seed=False): | 48 | def get_written_files(filterfn=None): |
20 | 49 | if filterfn is None: | ||
21 | 50 | def _exists(fn): | ||
22 | 51 | return os.path.exists(fn) | ||
23 | 52 | filterfn = _exists | ||
24 | 53 | |||
25 | 54 | for wf in write_file_log_read(): | ||
26 | 55 | fn = wf.get('filename') | ||
27 | 56 | if fn: | ||
28 | 57 | if filterfn(fn): | ||
29 | 58 | yield fn | ||
30 | 59 | |||
31 | 60 | |||
32 | 61 | def remove_artifacts(remove_logs=False, remove_seed=False): | ||
33 | 49 | """Helper which removes artifacts dir and optionally log files. | 62 | """Helper which removes artifacts dir and optionally log files. |
34 | 50 | 63 | ||
35 | 51 | @param: remove_logs: Boolean. Set True to delete the cloud_dir path. False | 64 | @param: remove_logs: Boolean. Set True to delete the cloud_dir path. False |
36 | @@ -56,14 +69,24 @@ def remove_artifacts(remove_logs, remove_seed=False): | |||
37 | 56 | """ | 69 | """ |
38 | 57 | init = Init(ds_deps=[]) | 70 | init = Init(ds_deps=[]) |
39 | 58 | init.read_cfg() | 71 | init.read_cfg() |
40 | 72 | |||
41 | 73 | logfiles = get_config_logfiles(init.cfg) | ||
42 | 59 | if remove_logs: | 74 | if remove_logs: |
44 | 60 | for log_file in get_config_logfiles(init.cfg): | 75 | for log_file in logfiles: |
45 | 61 | del_file(log_file) | 76 | del_file(log_file) |
46 | 62 | 77 | ||
47 | 63 | if not os.path.isdir(init.paths.cloud_dir): | 78 | if not os.path.isdir(init.paths.cloud_dir): |
48 | 64 | return 0 # Artifacts dir already cleaned | 79 | return 0 # Artifacts dir already cleaned |
49 | 65 | seed_path = os.path.join(init.paths.cloud_dir, 'seed') | 80 | seed_path = os.path.join(init.paths.cloud_dir, 'seed') |
51 | 66 | for path in glob.glob('%s/*' % init.paths.cloud_dir): | 81 | |
52 | 82 | def _written_filter(fn): | ||
53 | 83 | if fn not in logfiles: | ||
54 | 84 | if not fn.startswith(init.paths.cloud_dir): | ||
55 | 85 | if os.path.exists(fn): | ||
56 | 86 | return fn | ||
57 | 87 | |||
58 | 88 | files_written = list(get_written_files(filterfn=_written_filter)) | ||
59 | 89 | for path in glob.glob('%s/*' % init.paths.cloud_dir) + files_written: | ||
60 | 67 | if path == seed_path and not remove_seed: | 90 | if path == seed_path and not remove_seed: |
61 | 68 | continue | 91 | continue |
62 | 69 | try: | 92 | try: |
63 | diff --git a/cloudinit/cmd/tests/test_clean.py b/cloudinit/cmd/tests/test_clean.py | |||
64 | index f092ab3..143d955 100644 | |||
65 | --- a/cloudinit/cmd/tests/test_clean.py | |||
66 | +++ b/cloudinit/cmd/tests/test_clean.py | |||
67 | @@ -2,6 +2,7 @@ | |||
68 | 2 | 2 | ||
69 | 3 | from cloudinit.cmd import clean | 3 | from cloudinit.cmd import clean |
70 | 4 | from cloudinit.util import ensure_dir, sym_link, write_file | 4 | from cloudinit.util import ensure_dir, sym_link, write_file |
71 | 5 | from cloudinit import util | ||
72 | 5 | from cloudinit.tests.helpers import CiTestCase, wrap_and_call, mock | 6 | from cloudinit.tests.helpers import CiTestCase, wrap_and_call, mock |
73 | 6 | from collections import namedtuple | 7 | from collections import namedtuple |
74 | 7 | import os | 8 | import os |
75 | @@ -18,6 +19,10 @@ class TestClean(CiTestCase): | |||
76 | 18 | self.artifact_dir = self.tmp_path('artifacts', self.new_root) | 19 | self.artifact_dir = self.tmp_path('artifacts', self.new_root) |
77 | 19 | self.log1 = self.tmp_path('cloud-init.log', self.new_root) | 20 | self.log1 = self.tmp_path('cloud-init.log', self.new_root) |
78 | 20 | self.log2 = self.tmp_path('cloud-init-output.log', self.new_root) | 21 | self.log2 = self.tmp_path('cloud-init-output.log', self.new_root) |
79 | 22 | self.wflog = self.tmp_path('write_file.log', self.artifact_dir) | ||
80 | 23 | os.makedirs(os.path.dirname(self.wflog)) | ||
81 | 24 | util.WRITE_FILE_LOG = self.wflog | ||
82 | 25 | util._ENABLE_WRITE_FILE_LOG = True | ||
83 | 21 | 26 | ||
84 | 22 | class FakeInit(object): | 27 | class FakeInit(object): |
85 | 23 | cfg = {'def_log_file': self.log1, | 28 | cfg = {'def_log_file': self.log1, |
86 | @@ -37,21 +42,31 @@ class TestClean(CiTestCase): | |||
87 | 37 | """remove_artifacts removes logs when remove_logs is True.""" | 42 | """remove_artifacts removes logs when remove_logs is True.""" |
88 | 38 | write_file(self.log1, 'cloud-init-log') | 43 | write_file(self.log1, 'cloud-init-log') |
89 | 39 | write_file(self.log2, 'cloud-init-output-log') | 44 | write_file(self.log2, 'cloud-init-output-log') |
90 | 45 | netplan = self.tmp_path('50-cloud-init.yaml', self.artifact_dir) | ||
91 | 46 | write_file(netplan, 'netplan content') | ||
92 | 47 | # read the write log before it gets cleaned | ||
93 | 48 | with open(self.wflog) as fh: | ||
94 | 49 | contents = fh.readlines() | ||
95 | 50 | self.assertEqual(3, len(contents)) | ||
96 | 40 | 51 | ||
97 | 41 | self.assertFalse( | ||
98 | 42 | os.path.exists(self.artifact_dir), 'Unexpected artifacts dir') | ||
99 | 43 | retcode = wrap_and_call( | 52 | retcode = wrap_and_call( |
100 | 44 | 'cloudinit.cmd.clean', | 53 | 'cloudinit.cmd.clean', |
101 | 45 | {'Init': {'side_effect': self.init_class}}, | 54 | {'Init': {'side_effect': self.init_class}}, |
102 | 46 | clean.remove_artifacts, remove_logs=True) | 55 | clean.remove_artifacts, remove_logs=True) |
103 | 47 | self.assertFalse(os.path.exists(self.log1), 'Unexpected file') | 56 | self.assertFalse(os.path.exists(self.log1), 'Unexpected file') |
104 | 48 | self.assertFalse(os.path.exists(self.log2), 'Unexpected file') | 57 | self.assertFalse(os.path.exists(self.log2), 'Unexpected file') |
105 | 58 | self.assertFalse(os.path.exists(netplan), 'Unexpected file') | ||
106 | 49 | self.assertEqual(0, retcode) | 59 | self.assertEqual(0, retcode) |
107 | 60 | self.assertFalse(os.path.exists(self.wflog)) | ||
108 | 50 | 61 | ||
109 | 51 | def test_remove_artifacts_preserves_logs(self): | 62 | def test_remove_artifacts_preserves_logs(self): |
110 | 52 | """remove_artifacts leaves logs when remove_logs is False.""" | 63 | """remove_artifacts leaves logs when remove_logs is False.""" |
111 | 53 | write_file(self.log1, 'cloud-init-log') | 64 | write_file(self.log1, 'cloud-init-log') |
112 | 54 | write_file(self.log2, 'cloud-init-output-log') | 65 | write_file(self.log2, 'cloud-init-output-log') |
113 | 66 | # read the write log before it gets cleaned | ||
114 | 67 | with open(self.wflog) as fh: | ||
115 | 68 | contents = fh.readlines() | ||
116 | 69 | self.assertEqual(2, len(contents)) | ||
117 | 55 | 70 | ||
118 | 56 | retcode = wrap_and_call( | 71 | retcode = wrap_and_call( |
119 | 57 | 'cloudinit.cmd.clean', | 72 | 'cloudinit.cmd.clean', |
120 | @@ -60,6 +75,7 @@ class TestClean(CiTestCase): | |||
121 | 60 | self.assertTrue(os.path.exists(self.log1), 'Missing expected file') | 75 | self.assertTrue(os.path.exists(self.log1), 'Missing expected file') |
122 | 61 | self.assertTrue(os.path.exists(self.log2), 'Missing expected file') | 76 | self.assertTrue(os.path.exists(self.log2), 'Missing expected file') |
123 | 62 | self.assertEqual(0, retcode) | 77 | self.assertEqual(0, retcode) |
124 | 78 | self.assertFalse(os.path.exists(self.wflog)) | ||
125 | 63 | 79 | ||
126 | 64 | def test_remove_artifacts_removes_unlinks_symlinks(self): | 80 | def test_remove_artifacts_removes_unlinks_symlinks(self): |
127 | 65 | """remove_artifacts cleans artifacts dir unlinking any symlinks.""" | 81 | """remove_artifacts cleans artifacts dir unlinking any symlinks.""" |
128 | @@ -77,6 +93,7 @@ class TestClean(CiTestCase): | |||
129 | 77 | self.assertFalse( | 93 | self.assertFalse( |
130 | 78 | os.path.exists(path), | 94 | os.path.exists(path), |
131 | 79 | 'Unexpected {0} dir'.format(path)) | 95 | 'Unexpected {0} dir'.format(path)) |
132 | 96 | self.assertFalse(os.path.exists(self.wflog)) | ||
133 | 80 | 97 | ||
134 | 81 | def test_remove_artifacts_removes_artifacts_skipping_seed(self): | 98 | def test_remove_artifacts_removes_artifacts_skipping_seed(self): |
135 | 82 | """remove_artifacts cleans artifacts dir with exception of seed dir.""" | 99 | """remove_artifacts cleans artifacts dir with exception of seed dir.""" |
136 | @@ -101,6 +118,7 @@ class TestClean(CiTestCase): | |||
137 | 101 | self.assertFalse( | 118 | self.assertFalse( |
138 | 102 | os.path.exists(deleted_dir), | 119 | os.path.exists(deleted_dir), |
139 | 103 | 'Unexpected {0} dir'.format(deleted_dir)) | 120 | 'Unexpected {0} dir'.format(deleted_dir)) |
140 | 121 | self.assertFalse(os.path.exists(self.wflog)) | ||
141 | 104 | 122 | ||
142 | 105 | def test_remove_artifacts_removes_artifacts_removes_seed(self): | 123 | def test_remove_artifacts_removes_artifacts_removes_seed(self): |
143 | 106 | """remove_artifacts removes seed dir when remove_seed is True.""" | 124 | """remove_artifacts removes seed dir when remove_seed is True.""" |
144 | @@ -124,6 +142,8 @@ class TestClean(CiTestCase): | |||
145 | 124 | os.path.exists(deleted_dir), | 142 | os.path.exists(deleted_dir), |
146 | 125 | 'Unexpected {0} dir'.format(deleted_dir)) | 143 | 'Unexpected {0} dir'.format(deleted_dir)) |
147 | 126 | 144 | ||
148 | 145 | self.assertFalse(os.path.exists(self.wflog)) | ||
149 | 146 | |||
150 | 127 | def test_remove_artifacts_returns_one_on_errors(self): | 147 | def test_remove_artifacts_returns_one_on_errors(self): |
151 | 128 | """remove_artifacts returns non-zero on failure and prints an error.""" | 148 | """remove_artifacts returns non-zero on failure and prints an error.""" |
152 | 129 | ensure_dir(self.artifact_dir) | 149 | ensure_dir(self.artifact_dir) |
153 | @@ -139,6 +159,7 @@ class TestClean(CiTestCase): | |||
154 | 139 | self.assertEqual( | 159 | self.assertEqual( |
155 | 140 | 'ERROR: Could not remove %s/dir1: oops\n' % self.artifact_dir, | 160 | 'ERROR: Could not remove %s/dir1: oops\n' % self.artifact_dir, |
156 | 141 | m_stderr.getvalue()) | 161 | m_stderr.getvalue()) |
157 | 162 | self.assertFalse(os.path.exists(self.wflog)) | ||
158 | 142 | 163 | ||
159 | 143 | def test_handle_clean_args_reboots(self): | 164 | def test_handle_clean_args_reboots(self): |
160 | 144 | """handle_clean_args_reboots when reboot arg is provided.""" | 165 | """handle_clean_args_reboots when reboot arg is provided.""" |
161 | @@ -159,10 +180,15 @@ class TestClean(CiTestCase): | |||
162 | 159 | self.assertEqual(0, retcode) | 180 | self.assertEqual(0, retcode) |
163 | 160 | self.assertEqual( | 181 | self.assertEqual( |
164 | 161 | [(['shutdown', '-r', 'now'], False)], called_cmds) | 182 | [(['shutdown', '-r', 'now'], False)], called_cmds) |
165 | 183 | self.assertFalse(os.path.exists(self.wflog)) | ||
166 | 162 | 184 | ||
167 | 163 | def test_status_main(self): | 185 | def test_status_main(self): |
168 | 164 | '''clean.main can be run as a standalone script.''' | 186 | '''clean.main can be run as a standalone script.''' |
169 | 165 | write_file(self.log1, 'cloud-init-log') | 187 | write_file(self.log1, 'cloud-init-log') |
170 | 188 | # read the write log before it gets cleaned | ||
171 | 189 | with open(self.wflog) as fh: | ||
172 | 190 | contents = fh.readlines() | ||
173 | 191 | self.assertEqual(1, len(contents)) | ||
174 | 166 | with self.assertRaises(SystemExit) as context_manager: | 192 | with self.assertRaises(SystemExit) as context_manager: |
175 | 167 | wrap_and_call( | 193 | wrap_and_call( |
176 | 168 | 'cloudinit.cmd.clean', | 194 | 'cloudinit.cmd.clean', |
177 | @@ -175,5 +201,4 @@ class TestClean(CiTestCase): | |||
178 | 175 | self.assertFalse( | 201 | self.assertFalse( |
179 | 176 | os.path.exists(self.log1), 'Unexpected log {0}'.format(self.log1)) | 202 | os.path.exists(self.log1), 'Unexpected log {0}'.format(self.log1)) |
180 | 177 | 203 | ||
181 | 178 | |||
182 | 179 | # vi: ts=4 expandtab syntax=python | 204 | # vi: ts=4 expandtab syntax=python |
183 | diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py | |||
184 | index 4dad2af..c994a3b 100644 | |||
185 | --- a/cloudinit/tests/helpers.py | |||
186 | +++ b/cloudinit/tests/helpers.py | |||
187 | @@ -96,6 +96,8 @@ class TestCase(unittest2.TestCase): | |||
188 | 96 | util.PROC_CMDLINE = None | 96 | util.PROC_CMDLINE = None |
189 | 97 | util._DNS_REDIRECT_IP = None | 97 | util._DNS_REDIRECT_IP = None |
190 | 98 | util._LSB_RELEASE = {} | 98 | util._LSB_RELEASE = {} |
191 | 99 | util._ENABLE_WRITE_FILE_LOG = False | ||
192 | 100 | util.WRITE_LOG_FILE = '/var/lib/cloud/instance/write_file.log' | ||
193 | 99 | 101 | ||
194 | 100 | def setUp(self): | 102 | def setUp(self): |
195 | 101 | super(TestCase, self).setUp() | 103 | super(TestCase, self).setUp() |
196 | diff --git a/cloudinit/util.py b/cloudinit/util.py | |||
197 | index aa23b3f..950921c 100644 | |||
198 | --- a/cloudinit/util.py | |||
199 | +++ b/cloudinit/util.py | |||
200 | @@ -70,6 +70,8 @@ CONTAINER_TESTS = (['systemd-detect-virt', '--quiet', '--container'], | |||
201 | 70 | ['lxc-is-container']) | 70 | ['lxc-is-container']) |
202 | 71 | 71 | ||
203 | 72 | PROC_CMDLINE = None | 72 | PROC_CMDLINE = None |
204 | 73 | WRITE_FILE_LOG = '/var/lib/cloud/instance/write_file.log' | ||
205 | 74 | _ENABLE_WRITE_FILE_LOG = True | ||
206 | 73 | 75 | ||
207 | 74 | _LSB_RELEASE = {} | 76 | _LSB_RELEASE = {} |
208 | 75 | 77 | ||
209 | @@ -1841,6 +1843,55 @@ def chmod(path, mode): | |||
210 | 1841 | os.chmod(path, real_mode) | 1843 | os.chmod(path, real_mode) |
211 | 1842 | 1844 | ||
212 | 1843 | 1845 | ||
213 | 1846 | def write_file_log_append(filename, omode, target=None): | ||
214 | 1847 | """ Create an audit log of files that cloud-init has written. | ||
215 | 1848 | |||
216 | 1849 | The log is located at: /var/lib/cloud/instance/write_file.log | ||
217 | 1850 | |||
218 | 1851 | The format is JSON dict, one per line | ||
219 | 1852 | {'timestamp': time.time(), 'path': filename, 'mode': omode} | ||
220 | 1853 | |||
221 | 1854 | Example entries: | ||
222 | 1855 | {'filename': '/etc/apt/sources.list', 'mode': 'wb', 'timestamp': ts} | ||
223 | 1856 | |||
224 | 1857 | """ | ||
225 | 1858 | global WRITE_FILE_LOG | ||
226 | 1859 | global _ENABLE_WRITE_FILE_LOG | ||
227 | 1860 | |||
228 | 1861 | if not _ENABLE_WRITE_FILE_LOG: | ||
229 | 1862 | return | ||
230 | 1863 | |||
231 | 1864 | log_file = target_path(target, path=WRITE_FILE_LOG) | ||
232 | 1865 | if not os.path.exists(os.path.dirname(log_file)): | ||
233 | 1866 | return | ||
234 | 1867 | |||
235 | 1868 | record = {'timestamp': time.time(), 'filename': filename, 'mode': omode} | ||
236 | 1869 | content = json.dumps(record, default=json_serialize_default) | ||
237 | 1870 | try: | ||
238 | 1871 | with open(log_file, "ab") as wfl: | ||
239 | 1872 | wfl.write((content + '\n').encode('utf-8')) | ||
240 | 1873 | wfl.flush() | ||
241 | 1874 | except IOError as e: | ||
242 | 1875 | LOG.debug('Failed to append to write file log (%s): %s', log_file, e) | ||
243 | 1876 | |||
244 | 1877 | |||
245 | 1878 | def write_file_log_read(target=None): | ||
246 | 1879 | """ Read the WRITE_FILE_LOG and yield the contents. | ||
247 | 1880 | |||
248 | 1881 | :returns a list of record dicts | ||
249 | 1882 | """ | ||
250 | 1883 | global WRITE_FILE_LOG | ||
251 | 1884 | |||
252 | 1885 | log_file = target_path(target, path=WRITE_FILE_LOG) | ||
253 | 1886 | if os.path.exists(log_file): | ||
254 | 1887 | with open(log_file, "rb") as wfl: | ||
255 | 1888 | contents = wfl.read() | ||
256 | 1889 | for line in contents.splitlines(): | ||
257 | 1890 | record = load_json(line) | ||
258 | 1891 | if record: | ||
259 | 1892 | yield record | ||
260 | 1893 | |||
261 | 1894 | |||
262 | 1844 | def write_file(filename, content, mode=0o644, omode="wb", copy_mode=False): | 1895 | def write_file(filename, content, mode=0o644, omode="wb", copy_mode=False): |
263 | 1845 | """ | 1896 | """ |
264 | 1846 | Writes a file with the given content and sets the file mode as specified. | 1897 | Writes a file with the given content and sets the file mode as specified. |
265 | @@ -1872,6 +1923,7 @@ def write_file(filename, content, mode=0o644, omode="wb", copy_mode=False): | |||
266 | 1872 | mode_r = "%r" % mode | 1923 | mode_r = "%r" % mode |
267 | 1873 | LOG.debug("Writing to %s - %s: [%s] %s %s", | 1924 | LOG.debug("Writing to %s - %s: [%s] %s %s", |
268 | 1874 | filename, omode, mode_r, len(content), write_type) | 1925 | filename, omode, mode_r, len(content), write_type) |
269 | 1926 | write_file_log_append(filename, omode) | ||
270 | 1875 | with SeLinuxGuard(path=filename): | 1927 | with SeLinuxGuard(path=filename): |
271 | 1876 | with open(filename, omode) as fh: | 1928 | with open(filename, omode) as fh: |
272 | 1877 | fh.write(content) | 1929 | fh.write(content) |
273 | diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py | |||
274 | index 0e71db8..82909a7 100644 | |||
275 | --- a/tests/unittests/test_util.py | |||
276 | +++ b/tests/unittests/test_util.py | |||
277 | @@ -186,6 +186,51 @@ class TestWriteFile(helpers.TestCase): | |||
278 | 186 | mockobj.assert_called_once_with('selinux') | 186 | mockobj.assert_called_once_with('selinux') |
279 | 187 | 187 | ||
280 | 188 | 188 | ||
281 | 189 | class TestWriteFileLogAppend(helpers.CiTestCase): | ||
282 | 190 | |||
283 | 191 | idir = 'var/lib/cloud/instance' | ||
284 | 192 | |||
285 | 193 | def test_write_file_log_append(self): | ||
286 | 194 | root_d = self.tmp_dir() | ||
287 | 195 | log_path = os.path.join(root_d, self.idir, 'write_file.log') | ||
288 | 196 | util._ENABLE_WRITE_FILE_LOG = True | ||
289 | 197 | util.WRITE_FILE_LOG = log_path | ||
290 | 198 | helpers.populate_dir(root_d, | ||
291 | 199 | {os.path.join(self.idir, 'dummy'): 'i-foobar'}) | ||
292 | 200 | |||
293 | 201 | content = self.random_string() | ||
294 | 202 | fname = os.path.join(root_d, self.random_string()) | ||
295 | 203 | util.write_file(fname, content, omode='wb') | ||
296 | 204 | |||
297 | 205 | self.assertTrue(os.path.exists(log_path)) | ||
298 | 206 | contents = util.load_file(log_path) | ||
299 | 207 | self.assertEqual(1, len(contents.splitlines())) | ||
300 | 208 | for line in contents.splitlines(): | ||
301 | 209 | record = util.load_json(line) | ||
302 | 210 | self.assertEqual(fname, record['filename']) | ||
303 | 211 | self.assertEqual('wb', record['mode']) | ||
304 | 212 | self.assertIn('timestamp', record) | ||
305 | 213 | |||
306 | 214 | def test_write_file_log_read(self): | ||
307 | 215 | root_d = self.tmp_dir() | ||
308 | 216 | log_path = os.path.join(root_d, self.idir, 'write_file.log') | ||
309 | 217 | util.WRITE_FILE_LOG = log_path | ||
310 | 218 | helpers.populate_dir(root_d, | ||
311 | 219 | {os.path.join(self.idir, 'dummy'): 'i-foobar'}) | ||
312 | 220 | |||
313 | 221 | fname = os.path.join(root_d, self.random_string()) | ||
314 | 222 | |||
315 | 223 | expected_record = {'timestamp': 0, 'filename': fname, 'mode': 'wb'} | ||
316 | 224 | with open(log_path, 'ab') as wfl: | ||
317 | 225 | record = (json.dumps(expected_record) + '\n').encode('utf-8') | ||
318 | 226 | wfl.write(record) | ||
319 | 227 | wfl.flush() | ||
320 | 228 | |||
321 | 229 | records = list(util.write_file_log_read()) | ||
322 | 230 | self.assertEqual(1, len(records)) | ||
323 | 231 | self.assertEqual(expected_record, records[0]) | ||
324 | 232 | |||
325 | 233 | |||
326 | 189 | class TestDeleteDirContents(helpers.TestCase): | 234 | class TestDeleteDirContents(helpers.TestCase): |
327 | 190 | def setUp(self): | 235 | def setUp(self): |
328 | 191 | super(TestDeleteDirContents, self).setUp() | 236 | super(TestDeleteDirContents, self).setUp() |
FAILED: Continuous integration, rev:6b005ae905b 5666a9bcd10865e a95c599a5d7e99 /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 1163/
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
FAILED: Ubuntu LTS: Build
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 1163//rebuild
https:/