(feature) Add managed rsync verbosity for dry-runs
Add default dry-run rsync output flags so long-running dry-runs expose file-list, progress, stats, and itemized change information in their run-specific log files. Avoid duplicating user-supplied itemize or --info arguments so operators can still tune rsync output from global or host configuration.
This commit is contained in:
@@ -10,6 +10,9 @@ from pathlib import Path
|
|||||||
from typing import Callable, Sequence
|
from typing import Callable, Sequence
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_DRY_RUN_OUTPUT_ARGS = ["--itemize-changes", "--info=flist2,progress2,stats2"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class RsyncResult:
|
class RsyncResult:
|
||||||
exit_code: int
|
exit_code: int
|
||||||
@@ -44,6 +47,8 @@ def build_rsync_command(
|
|||||||
cmd: list[str] = [rsync_binary]
|
cmd: list[str] = [rsync_binary]
|
||||||
|
|
||||||
cmd.extend(list(rsync_args))
|
cmd.extend(list(rsync_args))
|
||||||
|
if dry_run:
|
||||||
|
_append_default_dry_run_output_args(cmd)
|
||||||
|
|
||||||
# includes/excludes: keep it simple for now:
|
# includes/excludes: keep it simple for now:
|
||||||
# - if includes are provided, user is responsible for correct rsync include logic.
|
# - if includes are provided, user is responsible for correct rsync include logic.
|
||||||
@@ -116,3 +121,19 @@ def _terminate_process_group(process: subprocess.Popen) -> None:
|
|||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
os.killpg(process.pid, signal.SIGKILL)
|
os.killpg(process.pid, signal.SIGKILL)
|
||||||
process.wait(timeout=10)
|
process.wait(timeout=10)
|
||||||
|
|
||||||
|
|
||||||
|
def _append_default_dry_run_output_args(command: list[str]) -> None:
|
||||||
|
if not _has_itemize_arg(command):
|
||||||
|
command.append("--itemize-changes")
|
||||||
|
if not any(arg.startswith("--info=") for arg in command):
|
||||||
|
command.append("--info=flist2,progress2,stats2")
|
||||||
|
|
||||||
|
|
||||||
|
def _has_itemize_arg(command: list[str]) -> bool:
|
||||||
|
for arg in command:
|
||||||
|
if arg == "--itemize-changes":
|
||||||
|
return True
|
||||||
|
if arg.startswith("-") and not arg.startswith("--") and "i" in arg:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ class RunScheduledConfigSourceTests(SimpleTestCase):
|
|||||||
def fake_run_rsync(command, log_path, timeout_seconds, cancel_check=None):
|
def fake_run_rsync(command, log_path, timeout_seconds, cancel_check=None):
|
||||||
self.assertEqual(log_path, Path("/tmp/pobsync-dryrun/web-01/run-42/rsync.log"))
|
self.assertEqual(log_path, Path("/tmp/pobsync-dryrun/web-01/run-42/rsync.log"))
|
||||||
self.assertEqual(timeout_seconds, 900)
|
self.assertEqual(timeout_seconds, 900)
|
||||||
|
self.assertIn("--itemize-changes", command)
|
||||||
|
self.assertIn("--info=flist2,progress2,stats2", command)
|
||||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
log_path.write_text("run 42\n", encoding="utf-8")
|
log_path.write_text("run 42\n", encoding="utf-8")
|
||||||
return RsyncResult(exit_code=0, command=command)
|
return RsyncResult(exit_code=0, command=command)
|
||||||
@@ -110,6 +112,30 @@ class RunScheduledConfigSourceTests(SimpleTestCase):
|
|||||||
self.assertEqual(result["log"], "/tmp/pobsync-dryrun/web-01/run-42/rsync.log")
|
self.assertEqual(result["log"], "/tmp/pobsync-dryrun/web-01/run-42/rsync.log")
|
||||||
self.assertEqual(result["timeout_seconds"], 900)
|
self.assertEqual(result["timeout_seconds"], 900)
|
||||||
|
|
||||||
|
def test_dry_run_does_not_duplicate_custom_output_args(self) -> None:
|
||||||
|
config_source = FakeConfigSource()
|
||||||
|
|
||||||
|
def effective_config_for_host(host: str) -> dict:
|
||||||
|
config = FakeConfigSource.effective_config_for_host(config_source, host)
|
||||||
|
config["rsync"]["args_effective"] = ["--archive", "--itemize-changes", "--info=name1,stats2"]
|
||||||
|
return config
|
||||||
|
|
||||||
|
config_source.effective_config_for_host = effective_config_for_host
|
||||||
|
|
||||||
|
with patch("pobsync.commands.run_scheduled.run_rsync") as run_rsync:
|
||||||
|
run_rsync.return_value = RsyncResult(exit_code=0, command=["rsync"])
|
||||||
|
run_scheduled(
|
||||||
|
prefix=Path("/missing-prefix"),
|
||||||
|
host="web-01",
|
||||||
|
dry_run=True,
|
||||||
|
config_source=config_source,
|
||||||
|
)
|
||||||
|
|
||||||
|
command = run_rsync.call_args.args[0]
|
||||||
|
self.assertEqual(command.count("--itemize-changes"), 1)
|
||||||
|
self.assertNotIn("--info=flist2,progress2,stats2", command)
|
||||||
|
self.assertIn("--info=name1,stats2", command)
|
||||||
|
|
||||||
def test_dry_run_reports_cancelled_rsync(self) -> None:
|
def test_dry_run_reports_cancelled_rsync(self) -> None:
|
||||||
def fake_run_rsync(command, log_path, timeout_seconds, cancel_check=None):
|
def fake_run_rsync(command, log_path, timeout_seconds, cancel_check=None):
|
||||||
self.assertTrue(cancel_check())
|
self.assertTrue(cancel_check())
|
||||||
|
|||||||
Reference in New Issue
Block a user