diff --git a/config.py b/config.py index 473e544..a07f2d1 100644 --- a/config.py +++ b/config.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from uuid import UUID +from datetime import timedelta # Name des lokalen Pools local_pool_to_backup = "tank" @@ -16,6 +17,8 @@ do_not_backup = [ "tank/docker", ] +recent_scrub_timedelta = timedelta(hours=24) + # überprüfe ob neue Snapshots mit diesen String-Teilen vorliegen snapshot_tag = "znap" snapshot_interval = "monthly" diff --git a/zfs-backup.py b/zfs-backup.py index a0365d7..6e1edf4 100755 --- a/zfs-backup.py +++ b/zfs-backup.py @@ -3,6 +3,7 @@ from __future__ import annotations +from datetime import datetime from functools import cache from os import getuid from pathlib import Path @@ -61,10 +62,10 @@ class Manager: def backup(self): self._external_pool.import_() try: - self._external_pool.scrub() + self._external_pool.scrub(skip_if_recently_scrubbed=True) self._backup_all_datasets() self._external_pool.clean_old_snapshots() - self._external_pool.scrub() + self._external_pool.scrub(skip_if_recently_scrubbed=False) finally: self._external_pool.export() @@ -280,28 +281,78 @@ class ZFS: ) @classmethod - def scrub(cls, pool: Pool, *, wait_for_finish: bool = True) -> None: + def scrub(cls, pool: Pool, *, skip_if_recently_scrubbed: bool, wait_for_finish: bool = True) -> None: if not isinstance(pool, Pool): raise TypeError("Can only scrub pools.") - if not cls._scrub_in_progress(pool): - Command.run([config.ZPOOL, "scrub", pool.name]) + cls._scrub_if_necessary(pool, skip_if_recently_scrubbed) if wait_for_finish: Command.run([config.ZPOOL, "wait", "-t", "scrub", pool.name]) if not cls._healthy(pool): raise IOError(f"Pool {pool.name} is not healthy.") + @classmethod + def _scrub_if_necessary(cls, pool: Pool, skip_if_recently_scrubbed: bool) -> None: + if cls._scrub_in_progress(pool): + return + if not skip_if_recently_scrubbed: + Command.run([config.ZPOOL, "scrub", pool.name]) + else: + if not cls._recently_scrubbed(pool): + Command.run([config.ZPOOL, "scrub", pool.name]) + + @classmethod + def _recently_scrubbed(cls, pool): + return datetime.now() - cls._last_scrub(pool) < config.recent_scrub_timedelta + @classmethod def _scrub_in_progress(cls, pool: Pool) -> bool: - return "scrub in progress" in cls._pool_status_output(pool) + return "scrub in progress" in cls._get_pool_status_output(pool) @classmethod def _healthy(cls, pool: Pool) -> bool: - return "ONLINE" in cls._pool_status_output(pool) + return "ONLINE" in cls._get_pool_status_output(pool) @staticmethod - def _pool_status_output(pool): + def _get_pool_status_output(pool) -> str: return Command.get_output([config.ZFS, "status", pool.name]) + @classmethod + def _last_scrub(cls, pool: Pool) -> datetime: + return Time.get_last_scrub_from_status_output(cls._get_pool_status_output(pool)) + + +class Time: + @staticmethod + def _get_date_format() -> str: + """date format as used by zpool status + + %a: day of week, short version + %b: month, short version + %e: day of month, space-padded + %H, %M, %S: hour, minute, second, zero-padded + %Y: year + """ + return "%a %b %e %H:%M:%S %Y" + + @classmethod + def _extract_datetime_segment(cls, line_with_datetime: str) -> str: + return line_with_datetime.split(" errors on ")[-1] + + @classmethod + def _get_line_with_datetime_from_status_output(cls, output: str) -> str: + lines = output.split("\n") + for line in lines: + if line.startswith(" scan: scrub repaired"): + return line.strip() + else: + raise ValueError("Could not find the correct line") + + @classmethod + def get_last_scrub_from_status_output(cls, output: str) -> datetime: + line_with_datetime = cls._get_line_with_datetime_from_status_output(output) + date_str = cls._extract_datetime_segment(line_with_datetime) + return datetime.strptime(date_str, cls._get_date_format()) + class Disk: def __init__(self, uuid: UUID, name: str): @@ -380,9 +431,9 @@ class ExternalPool(Pool): # TODO cancel scrub if necessary ZFS.export_pool(self._name) - def scrub(self) -> None: + def scrub(self, skip_if_recently_scrubbed: bool) -> None: print(f"Scrubbing {self._name}") - ZFS.scrub(self) + ZFS.scrub(self, skip_if_recently_scrubbed=skip_if_recently_scrubbed) @classmethod def find_from_dict(cls, backup_pools: dict[str, UUID]) -> ExternalPool: