Skip to content

Commit 0686791

Browse files
authored
fix: cloud-init clean --logs should not remove non-files (#6568)
Prevent traceback raised when cloud-init clean --logs encounters character-device paths in log configuration.
1 parent d2bf883 commit 0686791

File tree

2 files changed

+54
-2
lines changed

2 files changed

+54
-2
lines changed

cloudinit/cmd/clean.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import glob
1111
import logging
1212
import os
13+
import stat
1314
import sys
1415

1516
from cloudinit import settings, sources
@@ -42,6 +43,23 @@
4243
]
4344

4445

46+
def should_remove_log_file(log_file: str) -> bool:
47+
"""Check if a log file should be removed.
48+
49+
Avoid tracebacks from attempting to remove device files.
50+
51+
@param log_file: Path to the log file to check.
52+
@returns: True if the file should be removed, False otherwise.
53+
"""
54+
try:
55+
file_stat = os.stat(log_file)
56+
if stat.S_ISBLK(file_stat.st_mode) or stat.S_ISCHR(file_stat.st_mode):
57+
return False
58+
except OSError:
59+
return False
60+
return True
61+
62+
4563
def get_parser(parser=None):
4664
"""Build or extend an arg parser for clean utility.
4765
@@ -126,7 +144,8 @@ def remove_artifacts(init, remove_logs, remove_seed=False, remove_config=None):
126144
init.read_cfg()
127145
if remove_logs:
128146
for log_file in get_config_logfiles(init.cfg):
129-
del_file(log_file)
147+
if should_remove_log_file(log_file):
148+
del_file(log_file)
130149
if remove_config and set(remove_config).intersection(["all", "network"]):
131150
for path in GEN_NET_CONFIG_FILES:
132151
for conf in glob.glob(path):

tests/unittests/cmd/test_clean.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# This file is part of cloud-init. See LICENSE file for license information.
22

33
import os
4+
import stat
45
from collections import namedtuple
6+
from unittest import mock
57

68
import pytest
79

@@ -12,7 +14,7 @@
1214
from cloudinit.sources import DataSource
1315
from cloudinit.stages import Init
1416
from cloudinit.util import del_file, ensure_dir, sym_link, write_file
15-
from tests.unittests.helpers import mock, wrap_and_call
17+
from tests.unittests.helpers import wrap_and_call
1618

1719
MyPaths = namedtuple("MyPaths", "cloud_dir")
1820
CleanPaths = namedtuple(
@@ -574,3 +576,34 @@ def test_status_main(self, clean_paths, init_class):
574576
assert (
575577
clean_paths.log.exists() is False
576578
), f"Unexpected log {clean_paths.log}"
579+
580+
@pytest.mark.parametrize("device_type", (stat.S_IFCHR, stat.S_IFBLK))
581+
@pytest.mark.usefixtures("fake_filesystem")
582+
def test_remove_artifacts_preserves_device_logs(
583+
self, device_type, clean_paths, init_class
584+
):
585+
"""remove_artifacts does not remove log files that are device files."""
586+
original_stat = os.stat
587+
588+
def mock_stat(path):
589+
if path == clean_paths.log.strpath:
590+
st_result = mock.Mock()
591+
st_result.st_mode = device_type | 0o666
592+
return st_result
593+
return original_stat(path)
594+
595+
# Create a regular file and mock its stat
596+
clean_paths.log.write("device-file")
597+
clean_paths.output_log.write("cloud-init-output-log")
598+
with mock.patch("cloudinit.cmd.clean.os.stat", side_effect=mock_stat):
599+
retcode = clean.remove_artifacts(
600+
init=init_class,
601+
remove_logs=True,
602+
)
603+
assert 0 == retcode
604+
assert (
605+
clean_paths.log.exists() is True
606+
), f"Device file {clean_paths.log} should not be removed"
607+
assert (
608+
clean_paths.output_log.exists() is False
609+
), f"Regular log {clean_paths.output_log} should be removed"

0 commit comments

Comments
 (0)