Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/releases/pending/4593.fmf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
description: |
The ``tmt clean`` command now shows the size of each workdir
and a summary of total disk space freed.
3 changes: 3 additions & 0 deletions tests/clean/runs/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ rlJournalStart
rlRun -s "tmt clean runs --dry -v --workdir-root $tmprun"
rlAssertGrep "Would remove workdir '$run1'" "$rlRun_LOG"
rlAssertGrep "Would remove workdir '$run2'" "$rlRun_LOG"
rlAssertGrep "Summary: Would free.*of disk space" "$rlRun_LOG"
rlRun -s "tmt status --workdir-root $tmprun -vv"
rlAssertGrep "(done\s+){1}(todo\s+){6}$run1\s+/plan1" "$rlRun_LOG" -E
rlAssertGrep "(done\s+){1}(todo\s+){6}$run2\s+/plan1" "$rlRun_LOG" -E
Expand All @@ -30,12 +31,14 @@ rlJournalStart
rlPhaseStartTest "Specify ID"
rlRun -s "tmt clean runs -v -i $run1"
rlAssertGrep "Removing workdir '$run1'" "$rlRun_LOG"
rlAssertGrep "Summary: Freed.*of disk space" "$rlRun_LOG"
rlRun -s "tmt status --workdir-root $tmprun -vv"
rlAssertNotGrep "(done\s+){1}(todo\s+){6}$run1\s+/plan1" "$rlRun_LOG" -E
rlAssertGrep "(done\s+){1}(todo\s+){6}$run2\s+/plan1" "$rlRun_LOG" -E

rlRun -s "tmt clean runs -v -l --workdir-root $tmprun"
rlAssertGrep "Removing workdir '$run2'" "$rlRun_LOG"
rlAssertGrep "Summary: Freed.*of disk space" "$rlRun_LOG"
rlRun -s "tmt status --workdir-root $tmprun -vv"
rlAssertNotGrep "(done\s+){1}(todo\s+){6}$run2\s+/plan1" "$rlRun_LOG" -E

Expand Down
36 changes: 29 additions & 7 deletions tmt/base/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import tmt.convert
import tmt.export
import tmt.frameworks
import tmt.hardware
import tmt.identifier
import tmt.lint
import tmt.log
Expand Down Expand Up @@ -3613,6 +3614,11 @@ def show(self) -> None:
CleanCallback = Callable[[], bool]


def _dir_size(path: Path) -> int:
"""Return the total size in bytes of all files under path."""
return sum(f.lstat().st_size for f in path.rglob('*'))


class Clean(tmt.utils.Common):
"""
A class for cleaning up workdirs, guests or images
Expand Down Expand Up @@ -3751,20 +3757,22 @@ def guests(self, run_ids: tuple[str, ...], keep: Optional[int]) -> bool:
successful = False
return successful

def _clean_workdir(self, path: Path) -> bool:
def _clean_workdir(self, path: Path) -> tuple[bool, int]:
"""
Remove a workdir (unless in dry mode)
"""
size = _dir_size(path)
formatted_size = round(tmt.hardware.UNITS(f'{size} bytes').to_compact(), 1)
if self.is_dry_run:
self.verbose(f"Would remove workdir '{path}'.", shift=1)
self.verbose(f"Would remove workdir '{path}' ({formatted_size}).", shift=1)
else:
self.verbose(f"Removing workdir '{path}'.", shift=1)
self.verbose(f"Removing workdir '{path}' ({formatted_size}).", shift=1)
try:
shutil.rmtree(path)
except OSError as error:
self.warn(f"Failed to remove '{path}': {error}.", shift=1)
return False
return True
return False, 0
return True, size

def runs(self, id_: tuple[str, ...], keep: Optional[int]) -> bool:
"""
Expand All @@ -3777,18 +3785,32 @@ def runs(self, id_: tuple[str, ...], keep: Optional[int]) -> bool:
# the correct one.
last_run = Run(logger=self._logger, cli_invocation=self.cli_invocation)
last_run.load_workdir(with_logfiles=False)
return self._clean_workdir(last_run.run_workdir)
success, size = self._clean_workdir(last_run.run_workdir)
self.verbose(
f"Summary: {'Would free' if self.is_dry_run else 'Freed'} "
f"{round(tmt.hardware.UNITS(f'{size} bytes').to_compact(), 1)} of disk space.",
shift=1,
)
return success
all_workdirs = list(tmt.utils.generate_runs(self.workdir_root, id_, all_=True))
if keep is not None:
# Sort by change time of the workdirs and keep the newest workdirs
all_workdirs.sort(key=lambda workdir: workdir.stat().st_ctime, reverse=True)
all_workdirs = all_workdirs[keep:]

successful = True
total_size = 0
for workdir in all_workdirs:
if not self._clean_workdir(workdir):
success, size = self._clean_workdir(workdir)
if not success:
successful = False
total_size += size

self.verbose(
f"Summary: {'Would free' if self.is_dry_run else 'Freed'} "
f"{round(tmt.hardware.UNITS(f'{total_size} bytes').to_compact(), 1)} of disk space.",
shift=1,
)
return successful


Expand Down
13 changes: 11 additions & 2 deletions tmt/steps/provision/testcloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -1589,16 +1589,25 @@ def clean_images(cls, clean: 'tmt.base.core.Clean', dry: bool, workdir_root: Pat
clean.warn(f"Directory '{testcloud_images}' does not exist.", shift=2)
return True
successful = True
total_size = 0
for image in testcloud_images.iterdir():
size = image.stat().st_size
total_size += size
formatted_size = round(tmt.hardware.UNITS(f'{size} bytes').to_compact(), 1)
if dry:
clean.verbose(f"Would remove '{image}'.", shift=2)
clean.verbose(f"Would remove '{image}' ({formatted_size}).", shift=2)
else:
clean.verbose(f"Removing '{image}'.", shift=2)
clean.verbose(f"Removing '{image}' ({formatted_size}).", shift=2)
try:
image.unlink()
except OSError:
clean.fail(f"Failed to remove '{image}'.", shift=2)
successful = False
clean.verbose(
f"Summary: {'Would free' if dry else 'Freed'} "
f"{round(tmt.hardware.UNITS(f'{total_size} bytes').to_compact(), 1)} of disk space.",
shift=2,
)
return successful


Expand Down
Loading