From 14b3ae69631e3d0b62d823c1f892d99148f48d18 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 5 Aug 2020 12:04:36 +0200 Subject: [PATCH 001/155] new release cycle --- CHANGES.md | 4 ++++ src/abgleich/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1ca06e6..1e90df8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ # Changes +## 0.0.8 (2020-XX-XX) + +- (TBD) + ## 0.0.7 (2020-08-05) - FIX: `tree` now property checks if source or target is up, depending on what a user wants to see, see #20. diff --git a/src/abgleich/__init__.py b/src/abgleich/__init__.py index adae71e..3f1118f 100644 --- a/src/abgleich/__init__.py +++ b/src/abgleich/__init__.py @@ -28,4 +28,4 @@ # META # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -__version__ = "0.0.7" +__version__ = "0.0.8" From 3250a91546d1f37850013aaf9969a72f8fcdbc9e Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 5 Aug 2020 12:07:31 +0200 Subject: [PATCH 002/155] black --- src/abgleich/core/zpool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 5cefc47..3a414b8 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -273,7 +273,7 @@ def print_table(self): table.append(self._table_row(snapshot)) if len(table) == 0: - print('(empty)') + print("(empty)") return print( @@ -369,9 +369,9 @@ def from_config(cls, side: str, config: ConfigABC,) -> ZpoolABC: output, errors, returncode, exception = Command.on_side( ["zfs", "get", "all", "-r", "-H", "-p", root_dataset,], side, config, - ).run(returncode = True) + ).run(returncode=True) - if returncode != 0 and 'dataset does not exist' in errors: + if returncode != 0 and "dataset does not exist" in errors: return cls(datasets=[], side=side, config=config,) if returncode != 0: raise exception From 1d20e95c44d8739ebde529b1e25d648528825468 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 15:56:38 +0200 Subject: [PATCH 003/155] typing cleanup --- src/abgleich/core/command.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index d3c3ab0..85e80cf 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -29,7 +29,7 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import subprocess -import typing +from typing import List, Tuple, Dict, Union import typeguard @@ -42,7 +42,7 @@ @typeguard.typechecked class Command(CommandABC): - def __init__(self, cmd: typing.List[str]): + def __init__(self, cmd: List[str]): self._cmd = cmd.copy() @@ -52,7 +52,7 @@ def __str__(self) -> str: def run( self, returncode: bool = False - ) -> typing.Union[typing.Tuple[str, str], typing.Tuple[str, str, int, Exception]]: + ) -> Union[Tuple[str, str], Tuple[str, str, int, Exception]]: proc = subprocess.Popen( self.cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE @@ -110,13 +110,13 @@ def run_pipe(self, other: CommandABC): return errors_1, output_2, errors_2 @property - def cmd(self) -> typing.List[str]: + def cmd(self) -> List[str]: return self._cmd.copy() @classmethod def on_side( - cls, cmd: typing.List[str], side: str, config: typing.Dict + cls, cmd: List[str], side: str, config: Dict ) -> CommandABC: if config[side]["host"] == "localhost": @@ -125,7 +125,7 @@ def on_side( @classmethod def with_ssh( - cls, cmd: typing.List[str], side_config: typing.Dict, ssh_config: typing.Dict + cls, cmd: List[str], side_config: Dict, ssh_config: Dict ) -> CommandABC: cmd_str = " ".join([item.replace(" ", "\\ ") for item in cmd]) From 024c7278c1574c21f877407a3fcb88a4a822ce9f Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 15:59:31 +0200 Subject: [PATCH 004/155] typing cleanup (2) --- src/abgleich/core/comparison.py | 70 ++++++++++++++++----------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/abgleich/core/comparison.py b/src/abgleich/core/comparison.py index e9c08b2..6c6c2c9 100644 --- a/src/abgleich/core/comparison.py +++ b/src/abgleich/core/comparison.py @@ -29,7 +29,7 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import itertools -import typing +from typing import Generator, List, Union import typeguard @@ -39,16 +39,16 @@ # TYPING # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -ComparisonParentTypes = typing.Union[ +ComparisonParentTypes = Union[ ZpoolABC, DatasetABC, None, ] -ComparisonMergeTypes = typing.Union[ - typing.Generator[DatasetABC, None, None], typing.Generator[SnapshotABC, None, None], +ComparisonMergeTypes = Union[ + Generator[DatasetABC, None, None], Generator[SnapshotABC, None, None], ] -ComparisonItemType = typing.Union[ +ComparisonItemType = Union[ DatasetABC, SnapshotABC, None, ] -ComparisonStrictItemType = typing.Union[ +ComparisonStrictItemType = Union[ DatasetABC, SnapshotABC, ] @@ -63,7 +63,7 @@ def __init__( self, a: ComparisonParentTypes, b: ComparisonParentTypes, - merged: typing.List[ComparisonItemABC], + merged: List[ComparisonItemABC], ): assert a is not None or b is not None @@ -82,7 +82,7 @@ def a(self) -> ComparisonParentTypes: return self._a @property - def a_head(self) -> typing.List[ComparisonStrictItemType]: + def a_head(self) -> List[ComparisonStrictItemType]: return self._head( source=[item.a for item in self._merged], @@ -90,7 +90,7 @@ def a_head(self) -> typing.List[ComparisonStrictItemType]: ) @property - def a_overlap_tail(self) -> typing.List[ComparisonStrictItemType]: + def a_overlap_tail(self) -> List[ComparisonStrictItemType]: return self._overlap_tail( source=[item.a for item in self._merged], @@ -103,7 +103,7 @@ def b(self) -> ComparisonParentTypes: return self._b @property - def b_head(self) -> typing.List[ComparisonStrictItemType]: + def b_head(self) -> List[ComparisonStrictItemType]: return self._head( source=[item.b for item in self._merged], @@ -111,7 +111,7 @@ def b_head(self) -> typing.List[ComparisonStrictItemType]: ) @property - def b_overlap_tail(self) -> typing.List[ComparisonStrictItemType]: + def b_overlap_tail(self) -> List[ComparisonStrictItemType]: return self._overlap_tail( source=[item.b for item in self._merged], @@ -119,16 +119,16 @@ def b_overlap_tail(self) -> typing.List[ComparisonStrictItemType]: ) @property - def merged(self) -> typing.Generator[ComparisonItemABC, None, None]: + def merged(self) -> Generator[ComparisonItemABC, None, None]: return (item for item in self._merged) @classmethod def _head( cls, - source: typing.List[ComparisonItemType], - target: typing.List[ComparisonItemType], - ) -> typing.List[ComparisonItemType]: + source: List[ComparisonItemType], + target: List[ComparisonItemType], + ) -> List[ComparisonItemType]: """ Returns new elements from source. If target is empty, returns source. @@ -176,9 +176,9 @@ def _head( @classmethod def _overlap_tail( cls, - source: typing.List[ComparisonItemType], - target: typing.List[ComparisonItemType], - ) -> typing.List[ComparisonItemType]: + source: List[ComparisonItemType], + target: List[ComparisonItemType], + ) -> List[ComparisonItemType]: """ Overlap must include first element of source. """ @@ -218,8 +218,8 @@ def _overlap_tail( @classmethod def _strip_none( - cls, elements: typing.List[ComparisonItemType] - ) -> typing.List[ComparisonItemType]: + cls, elements: List[ComparisonItemType] + ) -> List[ComparisonItemType]: elements = cls._left_strip_none(elements) # left strip elements.reverse() # flip into reverse @@ -230,16 +230,16 @@ def _strip_none( @staticmethod def _left_strip_none( - elements: typing.List[ComparisonItemType], - ) -> typing.List[ComparisonItemType]: + elements: List[ComparisonItemType], + ) -> List[ComparisonItemType]: return list(itertools.dropwhile(lambda element: element is None, elements)) @staticmethod def _single_items( - items_a: typing.Union[ComparisonMergeTypes, None], - items_b: typing.Union[ComparisonMergeTypes, None], - ) -> typing.List[ComparisonItemABC]: + items_a: Union[ComparisonMergeTypes, None], + items_b: Union[ComparisonMergeTypes, None], + ) -> List[ComparisonItemABC]: assert items_a is not None or items_b is not None @@ -249,9 +249,9 @@ def _single_items( @staticmethod def _merge_datasets( - items_a: typing.Generator[DatasetABC, None, None], - items_b: typing.Generator[DatasetABC, None, None], - ) -> typing.List[ComparisonItemABC]: + items_a: Generator[DatasetABC, None, None], + items_b: Generator[DatasetABC, None, None], + ) -> List[ComparisonItemABC]: items_a = {item.subname: item for item in items_a} items_b = {item.subname: item for item in items_b} @@ -268,8 +268,8 @@ def _merge_datasets( @classmethod def from_zpools( cls, - zpool_a: typing.Union[ZpoolABC, None], - zpool_b: typing.Union[ZpoolABC, None], + zpool_a: Union[ZpoolABC, None], + zpool_b: Union[ZpoolABC, None], ) -> ComparisonABC: assert zpool_a is not None or zpool_b is not None @@ -295,9 +295,9 @@ def from_zpools( @staticmethod def _merge_snapshots( - items_a: typing.Generator[SnapshotABC, None, None], - items_b: typing.Generator[SnapshotABC, None, None], - ) -> typing.List[ComparisonItemABC]: + items_a: Generator[SnapshotABC, None, None], + items_b: Generator[SnapshotABC, None, None], + ) -> List[ComparisonItemABC]: items_a = list(items_a) items_b = list(items_b) @@ -366,8 +366,8 @@ def _merge_snapshots( @classmethod def from_datasets( cls, - dataset_a: typing.Union[DatasetABC, None], - dataset_b: typing.Union[DatasetABC, None], + dataset_a: Union[DatasetABC, None], + dataset_b: Union[DatasetABC, None], ) -> ComparisonABC: assert dataset_a is not None or dataset_b is not None From ac0b26d827a3848840bc420c3541bc146c6efc52 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 15:59:49 +0200 Subject: [PATCH 005/155] doc string --- src/abgleich/core/comparison.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/abgleich/core/comparison.py b/src/abgleich/core/comparison.py index 6c6c2c9..8ee702b 100644 --- a/src/abgleich/core/comparison.py +++ b/src/abgleich/core/comparison.py @@ -59,6 +59,10 @@ @typeguard.typechecked class Comparison(ComparisonABC): + """ + Immutable. + """ + def __init__( self, a: ComparisonParentTypes, From 3f412b1b8c8d4c6dc9500b93c30782d55e322bba Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:00:09 +0200 Subject: [PATCH 006/155] black --- src/abgleich/core/command.py | 4 +--- src/abgleich/core/comparison.py | 16 ++++------------ 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 85e80cf..a2d826c 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -115,9 +115,7 @@ def cmd(self) -> List[str]: return self._cmd.copy() @classmethod - def on_side( - cls, cmd: List[str], side: str, config: Dict - ) -> CommandABC: + def on_side(cls, cmd: List[str], side: str, config: Dict) -> CommandABC: if config[side]["host"] == "localhost": return cls(cmd) diff --git a/src/abgleich/core/comparison.py b/src/abgleich/core/comparison.py index 8ee702b..df4b8cb 100644 --- a/src/abgleich/core/comparison.py +++ b/src/abgleich/core/comparison.py @@ -129,9 +129,7 @@ def merged(self) -> Generator[ComparisonItemABC, None, None]: @classmethod def _head( - cls, - source: List[ComparisonItemType], - target: List[ComparisonItemType], + cls, source: List[ComparisonItemType], target: List[ComparisonItemType], ) -> List[ComparisonItemType]: """ Returns new elements from source. @@ -179,9 +177,7 @@ def _head( @classmethod def _overlap_tail( - cls, - source: List[ComparisonItemType], - target: List[ComparisonItemType], + cls, source: List[ComparisonItemType], target: List[ComparisonItemType], ) -> List[ComparisonItemType]: """ Overlap must include first element of source. @@ -271,9 +267,7 @@ def _merge_datasets( @classmethod def from_zpools( - cls, - zpool_a: Union[ZpoolABC, None], - zpool_b: Union[ZpoolABC, None], + cls, zpool_a: Union[ZpoolABC, None], zpool_b: Union[ZpoolABC, None], ) -> ComparisonABC: assert zpool_a is not None or zpool_b is not None @@ -369,9 +363,7 @@ def _merge_snapshots( @classmethod def from_datasets( - cls, - dataset_a: Union[DatasetABC, None], - dataset_b: Union[DatasetABC, None], + cls, dataset_a: Union[DatasetABC, None], dataset_b: Union[DatasetABC, None], ) -> ComparisonABC: assert dataset_a is not None or dataset_b is not None From ad7e6170e4d1a67b488135c90feaff1515f14030 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:00:36 +0200 Subject: [PATCH 007/155] doc string --- src/abgleich/core/command.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index a2d826c..284fd59 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -42,6 +42,10 @@ @typeguard.typechecked class Command(CommandABC): + """ + Immutable. + """ + def __init__(self, cmd: List[str]): self._cmd = cmd.copy() From 72a203b17406c78ee0c42b6cced62dbe1ce49f1c Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:01:57 +0200 Subject: [PATCH 008/155] import cleanup --- src/abgleich/core/command.py | 4 ++-- src/abgleich/core/comparison.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 284fd59..ad630dd 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -31,7 +31,7 @@ import subprocess from typing import List, Tuple, Dict, Union -import typeguard +from typeguard import typechecked from .abc import CommandABC @@ -40,7 +40,7 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -@typeguard.typechecked +@typechecked class Command(CommandABC): """ Immutable. diff --git a/src/abgleich/core/comparison.py b/src/abgleich/core/comparison.py index df4b8cb..3ff34fd 100644 --- a/src/abgleich/core/comparison.py +++ b/src/abgleich/core/comparison.py @@ -31,7 +31,7 @@ import itertools from typing import Generator, List, Union -import typeguard +from typeguard import typechecked from .abc import ComparisonABC, ComparisonItemABC, DatasetABC, SnapshotABC, ZpoolABC @@ -57,7 +57,7 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -@typeguard.typechecked +@typechecked class Comparison(ComparisonABC): """ Immutable. @@ -388,7 +388,7 @@ def from_datasets( ) -@typeguard.typechecked +@typechecked class ComparisonItem(ComparisonItemABC): def __init__(self, a: ComparisonItemType, b: ComparisonItemType): From 35901abb4adce0d5186bab41c952524222037a9c Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:02:24 +0200 Subject: [PATCH 009/155] doc string --- src/abgleich/core/dataset.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index ca0de90..59f9b0f 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -54,6 +54,10 @@ @typeguard.typechecked class Dataset(DatasetABC): + """ + Immutable. + """ + def __init__( self, name: str, From aaaec19e982bcbb8061b5cb7d38b43f4ed141e47 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:02:46 +0200 Subject: [PATCH 010/155] import cleanup --- src/abgleich/core/dataset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index 59f9b0f..318fbc0 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -37,7 +37,7 @@ except ImportError: from typing import Dict as DictType -import typeguard +from typeguard import typechecked from .abc import ConfigABC, DatasetABC, PropertyABC, TransactionABC, SnapshotABC from .command import Command @@ -52,7 +52,7 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -@typeguard.typechecked +@typechecked class Dataset(DatasetABC): """ Immutable. From 7d9b8a8790fcd1d24a180f9d01117ec1d50761a2 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:05:08 +0200 Subject: [PATCH 011/155] typing cleanup (3) --- src/abgleich/core/dataset.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index 318fbc0..5fb4262 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -29,13 +29,13 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import datetime -import typing +from typing import Dict, Generator, List, Union # Python <= 3.7.1 "fix" try: from typing import OrderedDict as DictType except ImportError: - from typing import Dict as DictType + DictType = Dict from typeguard import typechecked @@ -61,8 +61,8 @@ class Dataset(DatasetABC): def __init__( self, name: str, - properties: typing.Dict[str, PropertyABC], - snapshots: typing.List[SnapshotABC], + properties: Dict[str, PropertyABC], + snapshots: List[SnapshotABC], side: str, config: ConfigABC, ): @@ -86,7 +86,7 @@ def __len__(self) -> int: return len(self._snapshots) - def __getitem__(self, key: typing.Union[str, int, slice]) -> PropertyABC: + def __getitem__(self, key: Union[str, int, slice]) -> PropertyABC: if isinstance(key, str): return self._properties[key] @@ -94,9 +94,9 @@ def __getitem__(self, key: typing.Union[str, int, slice]) -> PropertyABC: def get( self, - key: typing.Union[str, int, slice], - default: typing.Union[None, PropertyABC] = None, - ) -> typing.Union[None, PropertyABC]: + key: Union[str, int, slice], + default: Union[None, PropertyABC] = None, + ) -> Union[None, PropertyABC]: if isinstance(key, str): return self._properties.get( @@ -147,7 +147,7 @@ def subname(self) -> str: return self._subname @property - def snapshots(self) -> typing.Generator[SnapshotABC, None, None]: + def snapshots(self) -> Generator[SnapshotABC, None, None]: return (snapshot for snapshot in self._snapshots) @@ -215,7 +215,7 @@ def _new_snapshot_name(self) -> str: def from_entities( cls, name: str, - entities: DictType[str, typing.List[typing.List[str]]], + entities: DictType[str, List[List[str]]], side: str, config: ConfigABC, ) -> DatasetABC: From bc6edc2335e533808277c0d187b70de155e7874b Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:05:21 +0200 Subject: [PATCH 012/155] black --- src/abgleich/core/dataset.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index 5fb4262..32ec972 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -93,9 +93,7 @@ def __getitem__(self, key: Union[str, int, slice]) -> PropertyABC: return self._snapshots[key] def get( - self, - key: Union[str, int, slice], - default: Union[None, PropertyABC] = None, + self, key: Union[str, int, slice], default: Union[None, PropertyABC] = None, ) -> Union[None, PropertyABC]: if isinstance(key, str): From 02e063431c3f115e75a48d930b6123fc7fe6b290 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:06:37 +0200 Subject: [PATCH 013/155] import cleanup + doc string --- src/abgleich/core/i18n.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/abgleich/core/i18n.py b/src/abgleich/core/i18n.py index 09b5ec9..a98585e 100644 --- a/src/abgleich/core/i18n.py +++ b/src/abgleich/core/i18n.py @@ -31,7 +31,7 @@ import locale import os -import typeguard +from typeguard import typechecked import yaml try: @@ -49,8 +49,12 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -@typeguard.typechecked +@typechecked class _Lang(dict): + """ + Mutable. + """ + def __init__(self): super().__init__() From 577383a72135863c6585cd2b45901409e0bb4d15 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:07:36 +0200 Subject: [PATCH 014/155] import cleanup --- src/abgleich/core/io.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/abgleich/core/io.py b/src/abgleich/core/io.py index bd2fae0..458f5b8 100644 --- a/src/abgleich/core/io.py +++ b/src/abgleich/core/io.py @@ -28,9 +28,9 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -import typing +from typing import Union -import typeguard +from typeguard import typechecked # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CONSTANTS @@ -57,14 +57,14 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -@typeguard.typechecked +@typechecked def colorize(text: str, col: str) -> str: return c.get(col.upper(), c["GREY"]) + text + c["RESET"] -@typeguard.typechecked +@typechecked def humanize_size( - size: typing.Union[float, int], add_color: bool = False, get_rgb: bool = False + size: Union[float, int], add_color: bool = False, get_rgb: bool = False ) -> str: suffix = "B" From 2c375b30c871549196df35da04ee4670aeafae5c Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:08:41 +0200 Subject: [PATCH 015/155] import cleanup --- src/abgleich/core/lib.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/abgleich/core/lib.py b/src/abgleich/core/lib.py index 3f7b4ad..0738e07 100644 --- a/src/abgleich/core/lib.py +++ b/src/abgleich/core/lib.py @@ -29,9 +29,9 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import re -import typing +from typing import Union -import typeguard +from typeguard import typechecked from .abc import ConfigABC from .command import Command @@ -41,7 +41,7 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -@typeguard.typechecked +@typechecked def is_host_up(side: str, config: ConfigABC) -> bool: assert side in ("source", "target") @@ -53,7 +53,7 @@ def is_host_up(side: str, config: ConfigABC) -> bool: return returncode == 0 -@typeguard.typechecked +@typechecked def join(*args: str) -> str: if len(args) < 2: @@ -67,8 +67,8 @@ def join(*args: str) -> str: return "/".join(args) -@typeguard.typechecked -def root(zpool: str, prefix: typing.Union[str, None]) -> str: +@typechecked +def root(zpool: str, prefix: Union[str, None]) -> str: if prefix is None: return zpool @@ -78,7 +78,7 @@ def root(zpool: str, prefix: typing.Union[str, None]) -> str: _name_re = re.compile("^[A-Za-z0-9_]+$") -@typeguard.typechecked +@typechecked def valid_name(name: str, min_len: int = 1) -> bool: assert min_len >= 0 From cbc90167d4637d72f55d3bb86eedc2f38fefeb1c Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:10:04 +0200 Subject: [PATCH 016/155] import cleanup --- src/abgleich/core/property.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/abgleich/core/property.py b/src/abgleich/core/property.py index ee12bdf..3dc76c8 100644 --- a/src/abgleich/core/property.py +++ b/src/abgleich/core/property.py @@ -28,9 +28,9 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -import typing +from typing import Union -import typeguard +from typeguard import typechecked from .abc import PropertyABC @@ -38,14 +38,14 @@ # TYPING # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -PropertyTypes = typing.Union[str, int, float, None] +PropertyTypes = Union[str, int, float, None] # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -@typeguard.typechecked +@typechecked class Property(PropertyABC): def __init__( self, name: str, value: PropertyTypes, src: PropertyTypes, From b5e3bc2c5c69ee90ec9c89df7dbb586679cd9666 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:11:43 +0200 Subject: [PATCH 017/155] import cleanup + doc string --- src/abgleich/core/snapshot.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 1a5841e..ea46605 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -28,9 +28,9 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -import typing +from typing import Dict, List, Union -import typeguard +from typeguard import typechecked from .abc import ConfigABC, PropertyABC, SnapshotABC, TransactionABC from .command import Command @@ -44,14 +44,18 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -@typeguard.typechecked +@typechecked class Snapshot(SnapshotABC): + """ + Immutable. + """ + def __init__( self, name: str, parent: str, - properties: typing.Dict[str, PropertyABC], - context: typing.List[SnapshotABC], + properties: Dict[str, PropertyABC], + context: List[SnapshotABC], side: str, config: ConfigABC, ): @@ -155,7 +159,7 @@ def subparent(self) -> str: return self._subparent @property - def ancestor(self) -> typing.Union[None, SnapshotABC]: + def ancestor(self) -> Union[None, SnapshotABC]: assert self in self._context self_index = self._context.index(self) @@ -173,8 +177,8 @@ def root(self) -> str: def from_entity( cls, name: str, - entity: typing.List[typing.List[str]], - context: typing.List[SnapshotABC], + entity: List[List[str]], + context: List[SnapshotABC], side: str, config: ConfigABC, ) -> SnapshotABC: From 272640e79811ad04a7e9afe735a64ef1879e59c9 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:12:12 +0200 Subject: [PATCH 018/155] doc string --- src/abgleich/core/property.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/abgleich/core/property.py b/src/abgleich/core/property.py index 3dc76c8..958cd46 100644 --- a/src/abgleich/core/property.py +++ b/src/abgleich/core/property.py @@ -47,6 +47,10 @@ @typechecked class Property(PropertyABC): + """ + Immutable. + """ + def __init__( self, name: str, value: PropertyTypes, src: PropertyTypes, ): From 7d2c2bfba8fd18bbd5a74b750b5daa39375b714b Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:13:00 +0200 Subject: [PATCH 019/155] typeguard cleanup --- src/abgleich/core/transaction.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/abgleich/core/transaction.py b/src/abgleich/core/transaction.py index 3c2e558..90ca1f6 100644 --- a/src/abgleich/core/transaction.py +++ b/src/abgleich/core/transaction.py @@ -31,7 +31,7 @@ import typing from tabulate import tabulate -import typeguard +from typeguard import typechecked from .abc import CommandABC, TransactionABC, TransactionListABC, TransactionMetaABC from .i18n import t @@ -42,7 +42,7 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -@typeguard.typechecked +@typechecked class Transaction(TransactionABC): def __init__( self, meta: TransactionMetaABC, commands: typing.List[CommandABC], @@ -122,7 +122,7 @@ def run(self): MetaNoneTypes = typing.Union[str, int, float, None] -@typeguard.typechecked +@typechecked class TransactionMeta(TransactionMetaABC): def __init__(self, **kwargs: MetaTypes): @@ -152,7 +152,7 @@ def keys(self) -> typing.Generator[str, None, None]: ] -@typeguard.typechecked +@typechecked class TransactionList(TransactionListABC): def __init__(self): From ec0326a8de03e846f62ac02d9690bdeab4631eba Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:14:57 +0200 Subject: [PATCH 020/155] import cleanup --- src/abgleich/core/transaction.py | 36 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/abgleich/core/transaction.py b/src/abgleich/core/transaction.py index 90ca1f6..5e0af0b 100644 --- a/src/abgleich/core/transaction.py +++ b/src/abgleich/core/transaction.py @@ -28,7 +28,7 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -import typing +from typing import Callable, Generator, List, Tuple, Union from tabulate import tabulate from typeguard import typechecked @@ -45,7 +45,7 @@ @typechecked class Transaction(TransactionABC): def __init__( - self, meta: TransactionMetaABC, commands: typing.List[CommandABC], + self, meta: TransactionMetaABC, commands: List[CommandABC], ): assert len(commands) in (1, 2) @@ -59,12 +59,12 @@ def __init__( self._changed = None @property - def changed(self) -> typing.Union[None, typing.Callable]: + def changed(self) -> Union[None, Callable]: return self._changed @changed.setter - def changed(self, value: typing.Union[None, typing.Callable]): + def changed(self, value: Union[None, Callable]): self._changed = value @@ -74,12 +74,12 @@ def complete(self) -> bool: return self._complete @property - def commands(self) -> typing.Tuple[CommandABC]: + def commands(self) -> Tuple[CommandABC]: return self._commands @property - def error(self) -> typing.Union[Exception, None]: + def error(self) -> Union[Exception, None]: return self._error @@ -118,8 +118,8 @@ def run(self): self._changed() -MetaTypes = typing.Union[str, int, float] -MetaNoneTypes = typing.Union[str, int, float, None] +MetaTypes = Union[str, int, float] +MetaNoneTypes = Union[str, int, float, None] @typechecked @@ -140,15 +140,15 @@ def get(self, key: str) -> MetaNoneTypes: return self._meta.get(key, None) - def keys(self) -> typing.Generator[str, None, None]: + def keys(self) -> Generator[str, None, None]: return (key for key in self._meta.keys()) -TransactionIterableTypes = typing.Union[ - typing.Generator[TransactionABC, None, None], - typing.List[TransactionABC], - typing.Tuple[TransactionABC], +TransactionIterableTypes = Union[ + Generator[TransactionABC, None, None], + List[TransactionABC], + Tuple[TransactionABC], ] @@ -168,17 +168,17 @@ def __getitem__(self, index: int) -> TransactionABC: return self._transactions[index] @property - def changed(self) -> typing.Union[None, typing.Callable]: + def changed(self) -> Union[None, Callable]: return self._changed @changed.setter - def changed(self, value: typing.Union[None, typing.Callable]): + def changed(self, value: Union[None, Callable]): self._changed = value @property - def table_columns(self) -> typing.List[str]: + def table_columns(self) -> List[str]: headers = set() for transaction in self._transactions: @@ -199,7 +199,7 @@ def table_columns(self) -> typing.List[str]: return headers @property - def table_rows(self) -> typing.List[str]: + def table_rows(self) -> List[str]: return [f'{t("transaction"):s} #{index:d}' for index in range(1, len(self) + 1)] @@ -261,7 +261,7 @@ def _table_format_cell(header: str, value: MetaNoneTypes) -> str: return FORMAT.get(header, str)(value) @staticmethod - def _table_colalign(headers: typing.List[str]) -> typing.List[str]: + def _table_colalign(headers: List[str]) -> List[str]: RIGHT = (t("written"),) DECIMAL = tuple() From 2556a60d716113eb4160e2ab0e317c9e4c0c1141 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:15:06 +0200 Subject: [PATCH 021/155] black --- src/abgleich/core/transaction.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/abgleich/core/transaction.py b/src/abgleich/core/transaction.py index 5e0af0b..e5398dc 100644 --- a/src/abgleich/core/transaction.py +++ b/src/abgleich/core/transaction.py @@ -146,9 +146,7 @@ def keys(self) -> Generator[str, None, None]: TransactionIterableTypes = Union[ - Generator[TransactionABC, None, None], - List[TransactionABC], - Tuple[TransactionABC], + Generator[TransactionABC, None, None], List[TransactionABC], Tuple[TransactionABC], ] From 014a3ed4092a5f6ac6e514818d3b333413da1f4a Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:16:19 +0200 Subject: [PATCH 022/155] doc strings --- src/abgleich/core/transaction.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/abgleich/core/transaction.py b/src/abgleich/core/transaction.py index e5398dc..c1a5837 100644 --- a/src/abgleich/core/transaction.py +++ b/src/abgleich/core/transaction.py @@ -44,6 +44,10 @@ @typechecked class Transaction(TransactionABC): + """ + Mutable. + """ + def __init__( self, meta: TransactionMetaABC, commands: List[CommandABC], ): @@ -124,6 +128,10 @@ def run(self): @typechecked class TransactionMeta(TransactionMetaABC): + """ + Immutable. + """ + def __init__(self, **kwargs: MetaTypes): self._meta = kwargs @@ -152,6 +160,10 @@ def keys(self) -> Generator[str, None, None]: @typechecked class TransactionList(TransactionListABC): + """ + Mutable. + """ + def __init__(self): self._transactions = [] From 38c9e9ddbb811ef559c7bbb3038d471cb809e4c6 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:16:51 +0200 Subject: [PATCH 023/155] typeguard cleanup --- src/abgleich/core/zpool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 3a414b8..4c082fe 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -32,7 +32,7 @@ import typing from tabulate import tabulate -import typeguard +from typeguard import typechecked from .abc import ( ComparisonItemABC, @@ -57,7 +57,7 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -@typeguard.typechecked +@typechecked class Zpool(ZpoolABC): def __init__( self, datasets: typing.List[DatasetABC], side: str, config: ConfigABC, From ee1c10486b63c7c023986f953deab6976b3127de Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:19:22 +0200 Subject: [PATCH 024/155] cleanups --- src/abgleich/core/zpool.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 4c082fe..9be2d7a 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -29,7 +29,7 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ from collections import OrderedDict -import typing +from typing import Generator, List, Tuple, Union from tabulate import tabulate from typeguard import typechecked @@ -60,7 +60,7 @@ @typechecked class Zpool(ZpoolABC): def __init__( - self, datasets: typing.List[DatasetABC], side: str, config: ConfigABC, + self, datasets: List[DatasetABC], side: str, config: ConfigABC, ): self._datasets = datasets @@ -74,7 +74,7 @@ def __eq__(self, other: ZpoolABC) -> bool: return self.side == other.side @property - def datasets(self) -> typing.Generator[DatasetABC, None, None]: + def datasets(self) -> Generator[DatasetABC, None, None]: return (dataset for dataset in self._datasets) @@ -107,11 +107,11 @@ def get_cleanup_transactions(self, other: ZpoolABC,) -> TransactionListABC: def generate_cleanup_transactions( self, other: ZpoolABC, - ) -> typing.Generator[ - typing.Tuple[ + ) -> Generator[ + Tuple[ int, - typing.Union[ - None, typing.Union[None, typing.Generator[TransactionABC, None, None]] + Union[ + None, Union[None, Generator[TransactionABC, None, None]] ], ], None, @@ -130,7 +130,7 @@ def generate_cleanup_transactions( def _get_cleanup_from_datasetitem( self, dataset_item: ComparisonItemABC, - ) -> typing.Union[None, typing.Generator[TransactionABC, None, None]]: + ) -> Union[None, Generator[TransactionABC, None, None]]: if dataset_item.get_item().subname in self._config["ignore"]: return @@ -162,11 +162,11 @@ def get_backup_transactions(self, other: ZpoolABC,) -> TransactionListABC: def generate_backup_transactions( self, other: ZpoolABC, - ) -> typing.Generator[ - typing.Tuple[ + ) -> Generator[ + Tuple[ int, - typing.Union[ - None, typing.Union[None, typing.Generator[TransactionABC, None, None]] + Union[ + None, Union[None, Generator[TransactionABC, None, None]] ], ], None, @@ -187,7 +187,7 @@ def generate_backup_transactions( def _get_backup_transactions_from_datasetitem( self, other: ZpoolABC, dataset_item: ComparisonItemABC, - ) -> typing.Union[None, typing.Generator[TransactionABC, None, None]]: + ) -> Union[None, Generator[TransactionABC, None, None]]: if dataset_item.get_item().subname in self._config["ignore"]: return @@ -237,8 +237,8 @@ def get_snapshot_transactions(self) -> TransactionListABC: def generate_snapshot_transactions( self, - ) -> typing.Generator[ - typing.Tuple[int, typing.Union[None, TransactionABC]], None, None + ) -> Generator[ + Tuple[int, Union[None, TransactionABC]], None, None ]: assert self._side == "source" @@ -250,7 +250,7 @@ def generate_snapshot_transactions( def _get_snapshot_transactions_from_dataset( self, dataset: DatasetABC - ) -> typing.Union[None, TransactionABC]: + ) -> Union[None, TransactionABC]: if dataset.subname in self._config["ignore"]: return @@ -286,7 +286,7 @@ def print_table(self): ) @staticmethod - def _table_row(entity: typing.Union[SnapshotABC, DatasetABC]) -> typing.List[str]: + def _table_row(entity: Union[SnapshotABC, DatasetABC]) -> List[str]: return [ "- " + colorize(entity.name, "grey") @@ -324,7 +324,7 @@ def print_comparison_table(self, other: ZpoolABC): ) @staticmethod - def _comparison_table_row(item: ComparisonItemABC) -> typing.List[str]: + def _comparison_table_row(item: ComparisonItemABC) -> List[str]: entity = item.get_item() name = entity.name if isinstance(entity, SnapshotABC) else entity.subname From d381bbf9abb8941946c866a7fafde4e5e29b6358 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:20:06 +0200 Subject: [PATCH 025/155] doc string --- src/abgleich/core/zpool.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 9be2d7a..69653f5 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -59,6 +59,10 @@ @typechecked class Zpool(ZpoolABC): + """ + Immutable. + """ + def __init__( self, datasets: List[DatasetABC], side: str, config: ConfigABC, ): From 20c1208ecaf202e18ba24c1ee5879d2e5d83fd21 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:20:26 +0200 Subject: [PATCH 026/155] comment --- src/abgleich/cli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/cli/__init__.py b/src/abgleich/cli/__init__.py index f96b87e..3dee88d 100644 --- a/src/abgleich/cli/__init__.py +++ b/src/abgleich/cli/__init__.py @@ -26,7 +26,7 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# IMPORT +# EXPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ from ._main_ import cli From 0d781f39bf4aa37ee4a671806e6946358a8cedea Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:22:02 +0200 Subject: [PATCH 027/155] import cleanup --- src/abgleich/gui/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/abgleich/gui/lib.py b/src/abgleich/gui/lib.py index 6df61bc..0f189cf 100644 --- a/src/abgleich/gui/lib.py +++ b/src/abgleich/gui/lib.py @@ -28,11 +28,11 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -import typing +from typing import Type import sys from PyQt5.QtWidgets import QApplication, QDialog -import typeguard +from typeguard import typechecked from ..core.abc import ConfigABC @@ -41,8 +41,8 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -@typeguard.typechecked -def run_app(Window: typing.Type[QDialog], config: ConfigABC): +@typechecked +def run_app(Window: Type[QDialog], config: ConfigABC): app = QApplication(sys.argv) window = Window(config) From 6a9f0edd63aff86792e50b40c92eea714ab0caa2 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:22:49 +0200 Subject: [PATCH 028/155] docstring --- src/abgleich/gui/transaction.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/abgleich/gui/transaction.py b/src/abgleich/gui/transaction.py index a94f1d9..5036d7f 100644 --- a/src/abgleich/gui/transaction.py +++ b/src/abgleich/gui/transaction.py @@ -45,6 +45,10 @@ @typeguard.typechecked class TransactionListModel(QAbstractTableModel): + """ + Mutable. + """ + def __init__( self, transactions: TransactionListABC, parent_changed: typing.Callable ): From a4ef1048cf33d6e9791e0009159714224d3b0b5f Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:23:12 +0200 Subject: [PATCH 029/155] typeguard cleanup --- src/abgleich/gui/transaction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/abgleich/gui/transaction.py b/src/abgleich/gui/transaction.py index 5036d7f..54b3ecd 100644 --- a/src/abgleich/gui/transaction.py +++ b/src/abgleich/gui/transaction.py @@ -30,7 +30,7 @@ import typing -import typeguard +from typeguard import typechecked from PyQt5.QtCore import QAbstractTableModel, QModelIndex, Qt from PyQt5.QtGui import QColor @@ -43,7 +43,7 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -@typeguard.typechecked +@typechecked class TransactionListModel(QAbstractTableModel): """ Mutable. From d78729ba42c6a9a4423cd4d55550e835b372045e Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:24:16 +0200 Subject: [PATCH 030/155] import cleanup --- src/abgleich/gui/transaction.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/abgleich/gui/transaction.py b/src/abgleich/gui/transaction.py index 54b3ecd..3c95173 100644 --- a/src/abgleich/gui/transaction.py +++ b/src/abgleich/gui/transaction.py @@ -28,7 +28,7 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -import typing +from typing import Callable, Union from typeguard import typechecked from PyQt5.QtCore import QAbstractTableModel, QModelIndex, Qt @@ -50,7 +50,7 @@ class TransactionListModel(QAbstractTableModel): """ def __init__( - self, transactions: TransactionListABC, parent_changed: typing.Callable + self, transactions: TransactionListABC, parent_changed: Callable ): super().__init__() @@ -63,7 +63,7 @@ def __init__( def data( self, index: QModelIndex, role: int - ) -> typing.Union[None, str, QColor]: # TODO return type + ) -> Union[None, str, QColor]: # TODO return type row, col = index.row(), index.column() col_key = self._cols[col] @@ -98,7 +98,7 @@ def data( def headerData( self, section: int, orientation: Qt.Orientation, role: int - ) -> typing.Union[None, str]: + ) -> Union[None, str]: if role == Qt.DisplayRole: @@ -116,7 +116,7 @@ def columnCount(self, index: QModelIndex) -> int: return len(self._cols) - def _transactions_changed(self, row: typing.Union[None, int] = None): + def _transactions_changed(self, row: Union[None, int] = None): old_rows, old_cols = self._rows, self._cols self._update_labels() From c309874be09621b2adc882c3f9bee31b9e4aa836 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:24:59 +0200 Subject: [PATCH 031/155] docstring --- src/abgleich/gui/wizard_base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/abgleich/gui/wizard_base.py b/src/abgleich/gui/wizard_base.py index f15e1ab..95cb92d 100644 --- a/src/abgleich/gui/wizard_base.py +++ b/src/abgleich/gui/wizard_base.py @@ -46,6 +46,11 @@ @typechecked class WizardUiBase(QDialog): + """ + UI definition. + Mutable. + """ + def __init__(self): super().__init__() # skip WizardUiBaseABC From 925b7dfe76bdf6ff6750e2a839b7eeac18911d88 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:26:33 +0200 Subject: [PATCH 032/155] annotations --- src/abgleich/gui/wizard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/abgleich/gui/wizard.py b/src/abgleich/gui/wizard.py index 5e446b6..7f4d22a 100644 --- a/src/abgleich/gui/wizard.py +++ b/src/abgleich/gui/wizard.py @@ -199,7 +199,7 @@ def _finish_step(self, index: int): self._init_step(index + 1) - def _prepare_snap(self): + def _prepare_snap(self) -> bool: zpool = Zpool.from_config("source", config=self._config) @@ -217,7 +217,7 @@ def _prepare_snap(self): return len(self._transactions) > 0 - def _prepare(self, action: str): + def _prepare(self, action: str) -> bool: source_zpool = Zpool.from_config("source", config=self._config) target_zpool = Zpool.from_config("target", config=self._config) From 8ad2e67356d465f767e50a7452d7d4fbcfdea16d Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:27:10 +0200 Subject: [PATCH 033/155] docstring --- src/abgleich/gui/wizard.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/abgleich/gui/wizard.py b/src/abgleich/gui/wizard.py index 7f4d22a..e09693b 100644 --- a/src/abgleich/gui/wizard.py +++ b/src/abgleich/gui/wizard.py @@ -49,6 +49,11 @@ @typechecked class WizardUi(WizardUiBase): + """ + UI events and logic. + Mutable. + """ + def __init__(self, config: ConfigABC): super().__init__() From 88c9be845e787b764aa5edd857080ecace4d00a5 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:27:24 +0200 Subject: [PATCH 034/155] black --- src/abgleich/core/zpool.py | 18 +++--------------- src/abgleich/gui/transaction.py | 4 +--- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 69653f5..416ed5e 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -112,12 +112,7 @@ def get_cleanup_transactions(self, other: ZpoolABC,) -> TransactionListABC: def generate_cleanup_transactions( self, other: ZpoolABC, ) -> Generator[ - Tuple[ - int, - Union[ - None, Union[None, Generator[TransactionABC, None, None]] - ], - ], + Tuple[int, Union[None, Union[None, Generator[TransactionABC, None, None]]],], None, None, ]: @@ -167,12 +162,7 @@ def get_backup_transactions(self, other: ZpoolABC,) -> TransactionListABC: def generate_backup_transactions( self, other: ZpoolABC, ) -> Generator[ - Tuple[ - int, - Union[ - None, Union[None, Generator[TransactionABC, None, None]] - ], - ], + Tuple[int, Union[None, Union[None, Generator[TransactionABC, None, None]]],], None, None, ]: @@ -241,9 +231,7 @@ def get_snapshot_transactions(self) -> TransactionListABC: def generate_snapshot_transactions( self, - ) -> Generator[ - Tuple[int, Union[None, TransactionABC]], None, None - ]: + ) -> Generator[Tuple[int, Union[None, TransactionABC]], None, None]: assert self._side == "source" diff --git a/src/abgleich/gui/transaction.py b/src/abgleich/gui/transaction.py index 3c95173..15609e5 100644 --- a/src/abgleich/gui/transaction.py +++ b/src/abgleich/gui/transaction.py @@ -49,9 +49,7 @@ class TransactionListModel(QAbstractTableModel): Mutable. """ - def __init__( - self, transactions: TransactionListABC, parent_changed: Callable - ): + def __init__(self, transactions: TransactionListABC, parent_changed: Callable): super().__init__() self._transactions = transactions From 7ed0c15196f57dc98690d64fc4c066d212d62c63 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:28:52 +0200 Subject: [PATCH 035/155] logging changes --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1e90df8..51ee225 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ ## 0.0.8 (2020-XX-XX) -- (TBD) +- FIX: Many cleanups in code base, enabling future developments. ## 0.0.7 (2020-08-05) From 5140ff3fa0e94ff8316a9710ba0efa83b0e518d4 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:32:49 +0200 Subject: [PATCH 036/155] fix abc type --- src/abgleich/core/command.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index ad630dd..436ea3c 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -33,7 +33,7 @@ from typeguard import typechecked -from .abc import CommandABC +from .abc import CommandABC, ConfigABC # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -119,7 +119,7 @@ def cmd(self) -> List[str]: return self._cmd.copy() @classmethod - def on_side(cls, cmd: List[str], side: str, config: Dict) -> CommandABC: + def on_side(cls, cmd: List[str], side: str, config: ConfigABC) -> CommandABC: if config[side]["host"] == "localhost": return cls(cmd) From 4dae9c50413829c4c163e545ad819da1d8913868 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:39:26 +0200 Subject: [PATCH 037/155] docstring --- src/abgleich/core/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/abgleich/core/config.py b/src/abgleich/core/config.py index 045db00..614d380 100644 --- a/src/abgleich/core/config.py +++ b/src/abgleich/core/config.py @@ -49,6 +49,10 @@ @typeguard.typechecked class Config(ConfigABC, dict): + """ + Mutable. TODO make immutable ... + """ + @classmethod def from_fd(cls, fd: typing.TextIO): From 5c97f1985b2082b03d2aa7fd261a4bac0f1c2a17 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 16:43:57 +0200 Subject: [PATCH 038/155] import fix --- src/abgleich/core/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/abgleich/core/config.py b/src/abgleich/core/config.py index 614d380..68811b4 100644 --- a/src/abgleich/core/config.py +++ b/src/abgleich/core/config.py @@ -29,9 +29,9 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -import typing +from typing import Dict, TextIO -import typeguard +from typeguard import typechecked import yaml try: @@ -47,14 +47,14 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -@typeguard.typechecked +@typechecked class Config(ConfigABC, dict): """ Mutable. TODO make immutable ... """ @classmethod - def from_fd(cls, fd: typing.TextIO): + def from_fd(cls, fd: TextIO): ssh_schema = { "compression": lambda v: isinstance(v, bool), @@ -84,7 +84,7 @@ def from_fd(cls, fd: typing.TextIO): return cls(config) @classmethod - def _validate(cls, data: typing.Dict, schema: typing.Dict): + def _validate(cls, data: Dict, schema: Dict): for field, validator in schema.items(): if field not in data.keys(): From 2c56b8470f70df5a141e7d9d15b7f91db63a8118 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 21:15:27 +0200 Subject: [PATCH 039/155] new abc: config field --- src/abgleich/core/abc.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/abgleich/core/abc.py b/src/abgleich/core/abc.py index 58a076a..8cd5f02 100644 --- a/src/abgleich/core/abc.py +++ b/src/abgleich/core/abc.py @@ -28,56 +28,60 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -import abc +from abc import ABC # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASSES # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -class CloneABC(abc.ABC): +class CloneABC(ABC): pass -class CommandABC(abc.ABC): +class CommandABC(ABC): pass -class ComparisonABC(abc.ABC): +class ComparisonABC(ABC): pass -class ComparisonItemABC(abc.ABC): +class ComparisonItemABC(ABC): pass -class ConfigABC(abc.ABC): +class ConfigABC(ABC): pass -class DatasetABC(abc.ABC): +class ConfigFieldABC(ABC): pass -class PropertyABC(abc.ABC): +class DatasetABC(ABC): pass -class SnapshotABC(abc.ABC): +class PropertyABC(ABC): pass -class TransactionABC(abc.ABC): +class SnapshotABC(ABC): pass -class TransactionListABC(abc.ABC): +class TransactionABC(ABC): pass -class TransactionMetaABC(abc.ABC): +class TransactionListABC(ABC): pass -class ZpoolABC(abc.ABC): +class TransactionMetaABC(ABC): + pass + + +class ZpoolABC(ABC): pass From d4d93365ff5dc6be4a827fd061fd755dc4e65efa Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 21:16:15 +0200 Subject: [PATCH 040/155] a configuration field --- src/abgleich/core/configfield.py | 132 +++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/abgleich/core/configfield.py diff --git a/src/abgleich/core/configfield.py b/src/abgleich/core/configfield.py new file mode 100644 index 0000000..edacd21 --- /dev/null +++ b/src/abgleich/core/configfield.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +""" + +ABGLEICH +zfs sync tool +https://github.com/pleiszenburg/abgleich + + src/abgleich/core/configfield.py: Handles configuration fields + + Copyright (C) 2019-2020 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/abgleich/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from typing import Callable, List, Union + +from typeguard import typechecked + +from .abc import ConfigFieldABC + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TYPING +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +ConfigValueTypes = Union[List[str], str, int, float, bool, None] + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +@typechecked +class ConfigField(ConfigFieldABC): + """ + Mutable. + """ + + def __init__(self, + name: str, + validate: Callable, + default: ConfigValueTypes = None, + ): + + self._name = name + self._default = default + self._validate = validate + + if self._default is not None: + if not self._validate(self._default): + raise ValueError(f'invalid default value for {self._name}') + + self._value = None + + def __repr__(self) -> str: + + return ( + '' + ) + + def copy(self) -> ConfigFieldABC: + + return type(self)( + name = self._name, + default = self._default, + validate = self._validate, + ) + + @property + def name(self) -> str: + + return self._name + + @property + def value(self) -> ConfigValueTypes: + + if self._value is not None: + return self._value + + if self._default is None: + raise ValueError(f'required value for {self._name} missing') + + return self._default + + @value.setter + def value(self, value: ConfigValueTypes): + + if self._value is not None: + raise ValueError(f'value for {self._name} has already been set') + if value is None: + return + if not self._validate(value): + raise ValueError(f'invalid value for {self._name}') + + self._value = value + + @property + def valid(self) -> bool: + + return ( + (self._value is not None and self._validate(self._value)) + or + (self._default is not None and self._validate(self._default)) + ) + + @property + def required(self) -> bool: + + return self._default is None + + @property + def set(self) -> bool: + + return self._value is not None From 06dff7989d866050e51f108e5bea1ba347920eef Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 21:16:43 +0200 Subject: [PATCH 041/155] configuration specification based on field class --- src/abgleich/core/configspec.py | 131 ++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/abgleich/core/configspec.py diff --git a/src/abgleich/core/configspec.py b/src/abgleich/core/configspec.py new file mode 100644 index 0000000..f885358 --- /dev/null +++ b/src/abgleich/core/configspec.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- + +""" + +ABGLEICH +zfs sync tool +https://github.com/pleiszenburg/abgleich + + src/abgleich/core/configspec.py: Defines configuration fields + + Copyright (C) 2019-2020 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/abgleich/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from .configfield import ConfigField +from .lib import valid_name + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# SPEC +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +CONFIGSPEC = [ + + ConfigField( + name = "keep_snapshots", + validate = lambda v: isinstance(v, int) and v >= 1, + default = 1, + ), + + ConfigField( + name = "suffix", + validate = lambda v: isinstance(v, str) and valid_name(v, min_len = 0), + default = '', + ), + + ConfigField( + name = "digits", + validate = lambda v: isinstance(v, int) and v >= 1, + default = 2, + ), + + ConfigField( + name = "always_changed", + validate = lambda v: isinstance(v, bool), + default = False, + ), + + ConfigField( + name = "written_threshold", + validate = lambda v: isinstance(v, int) and v > 0, + default = 1024 ** 2, + ), + + ConfigField( + name = "check_diff", + validate = lambda v: isinstance(v, bool), + default = True, + ), + + ConfigField( + name = "ignore", + validate = lambda v: isinstance(v, list) and all((isinstance(item, str) and len(item) > 0 for item in v)), + default = list(), + ), + + ConfigField( + name = "include_root", + validate = lambda v: isinstance(v, bool), + default = True, + ), + + ConfigField( + name = "ssh/compression", + validate = lambda v: isinstance(v, bool), + default = False, + ), + + ConfigField( + name = "ssh/cipher", + validate = lambda v: isinstance(v, str), + default = '', + ), + +] + +for _side in ('source', 'target'): + + CONFIGSPEC.extend([ + + ConfigField( + name = f"{_side}/zpool", + validate = lambda v: isinstance(v, str) and len(v) > 0, + ), + + ConfigField( + name = f"{_side}/prefix", + validate = lambda v: isinstance(v, str), + default = '', + ), + + ConfigField( + name = f"{_side}/host", + validate = lambda v: isinstance(v, str) and len(v) > 0, + default = 'localhost', + ), + + ConfigField( + name = f"{_side}/user", + validate = lambda v: isinstance(v, str), + default = '', + ), + + ]) + +del _side From 30229897dcd72e9e25777979cc088ee77446e32d Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 21:17:58 +0200 Subject: [PATCH 042/155] configuration module rewrite, implementing #28 --- src/abgleich/core/config.py | 100 +++++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 41 deletions(-) diff --git a/src/abgleich/core/config.py b/src/abgleich/core/config.py index 68811b4..a11ecb8 100644 --- a/src/abgleich/core/config.py +++ b/src/abgleich/core/config.py @@ -29,7 +29,7 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -from typing import Dict, TextIO +from typing import Dict, TextIO, Union from typeguard import typechecked import yaml @@ -39,57 +39,75 @@ except ImportError: from yaml import FullLoader as Loader -from .abc import ConfigABC -from .lib import valid_name +from .abc import ConfigABC, ConfigFieldABC +from .configfield import ConfigValueTypes +from .configspec import CONFIGSPEC # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - @typechecked -class Config(ConfigABC, dict): +class Config(ConfigABC): """ - Mutable. TODO make immutable ... + Immutable. """ + def __init__(self, root: Union[str, None] = None, **kwargs: ConfigFieldABC): + + self._root = root + self._fields = kwargs + + def __repr__(self): + + return '' if self._root is None else f'' + + def __getitem__(self, key: str) -> ConfigValueTypes: + + return self._fields[key].value if self._root is None else self._fields[f'{self._root:s}/{key:s}'].value + + def group(self, root: str) -> ConfigABC: + + return type(self)(root = root, **self._fields) + @classmethod - def from_fd(cls, fd: TextIO): - - ssh_schema = { - "compression": lambda v: isinstance(v, bool), - "cipher": lambda v: isinstance(v, str) or v is None, - } - - side_schema = { - "zpool": lambda v: isinstance(v, str) and len(v) > 0, - "prefix": lambda v: isinstance(v, str) or v is None, - "host": lambda v: isinstance(v, str) and len(v) > 0, - "user": lambda v: isinstance(v, str) or v is None, - } - - root_schema = { - "source": lambda v: cls._validate(data=v, schema=side_schema), - "target": lambda v: cls._validate(data=v, schema=side_schema), - "keep_snapshots": lambda v: isinstance(v, int) and v >= 1, - "suffix": lambda v: v is None or (isinstance(v, str) and valid_name(v)), - "digits": lambda v: isinstance(v, int) and v >= 1, - "ignore": lambda v: isinstance(v, list) - and all((isinstance(item, str) and len(item) > 0 for item in v)), - "ssh": lambda v: cls._validate(data=v, schema=ssh_schema), - } - - config = yaml.load(fd.read(), Loader=Loader) - cls._validate(data=config, schema=root_schema) - return cls(config) + def _flatten_dict_tree(cls, data: Dict, root: Union[str, None] = None) -> Dict: + + flat_data = {} + + for key, value in data.items(): + if not isinstance(key, str): + raise TypeError('configuration key is no string', key) + if root is not None: + if len(root) > 0: + key = f'{root:s}/{key:s}' + if isinstance(value, dict): + flat_data.update(cls._flatten_dict_tree(data = value, root = key)) + else: + flat_data[key] = value + + return flat_data @classmethod - def _validate(cls, data: Dict, schema: Dict): + def from_fd(cls, fd: TextIO) -> ConfigABC: + + return cls.from_text(fd.read()) + + @classmethod + def from_text(cls, text: str) -> ConfigABC: + + config = yaml.load(text, Loader=Loader) + + if not isinstance(config, dict): + raise TypeError('config is no dict', config) + + config_fields = {field.name: field.copy() for field in CONFIGSPEC} + for key, value in cls._flatten_dict_tree(data = config).items(): + if key not in config_fields.keys(): + raise ValueError('unknown configuration key', key) + config_fields[key].value = value - for field, validator in schema.items(): - if field not in data.keys(): - raise KeyError(f'missing configuration field "{field:s}"') - if not validator(data[field]): - raise ValueError(f'invalid value in field "{field:s}"') + if any((not field.valid for field in config_fields.values())): + raise ValueError('configuration is not valid') - return True + return cls(**config_fields) From 90e0312799fcd0db4548101b0ab205438ff6e955 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 21:18:32 +0200 Subject: [PATCH 043/155] lib runs on new config --- src/abgleich/core/lib.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/abgleich/core/lib.py b/src/abgleich/core/lib.py index 0738e07..2e83029 100644 --- a/src/abgleich/core/lib.py +++ b/src/abgleich/core/lib.py @@ -45,7 +45,7 @@ def is_host_up(side: str, config: ConfigABC) -> bool: assert side in ("source", "target") - if config[side]["host"] == "localhost": + if config[f'{side:s}/host'] == "localhost": return True _, _, returncode, _ = Command.on_side(["exit"], side, config).run(returncode=True) @@ -85,4 +85,8 @@ def valid_name(name: str, min_len: int = 1) -> bool: if len(name) < min_len: return False + + if min_len == 0 and len(name) == 0: + return True + return bool(_name_re.match(name)) From 2d43693989d3c1d2e3c734b1809829cf8b133997 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 21:19:22 +0200 Subject: [PATCH 044/155] command runs on new config --- src/abgleich/core/command.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 436ea3c..65ad976 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -29,7 +29,7 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import subprocess -from typing import List, Tuple, Dict, Union +from typing import List, Tuple, Union from typeguard import typechecked @@ -121,13 +121,16 @@ def cmd(self) -> List[str]: @classmethod def on_side(cls, cmd: List[str], side: str, config: ConfigABC) -> CommandABC: - if config[side]["host"] == "localhost": + side_config = config.group(side) + + if side_config["host"] == "localhost": return cls(cmd) - return cls.with_ssh(cmd, side_config=config[side], ssh_config=config["ssh"]) + + return cls.with_ssh(cmd, side_config=side_config, ssh_config=config.group('ssh')) @classmethod def with_ssh( - cls, cmd: List[str], side_config: Dict, ssh_config: Dict + cls, cmd: List[str], side_config: ConfigABC, ssh_config: ConfigABC ) -> CommandABC: cmd_str = " ".join([item.replace(" ", "\\ ") for item in cmd]) From 5ccbcb06d669d0774b2adb9a16285978a3eb44d2 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 21:20:09 +0200 Subject: [PATCH 045/155] dataset runs on new config --- src/abgleich/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index 32ec972..47bb34a 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -73,7 +73,7 @@ def __init__( self._side = side self._config = config - self._root = root(config[side]["zpool"], config[side]["prefix"]) + self._root = root(config[f"{side:s}/zpool"], config[f"{side:s}/prefix"]) assert self._name.startswith(self._root) self._subname = self._name[len(self._root) :].strip("/") From 02660d5ddb19e7ebad9d4bf7812ace859e4aa757 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 21:20:40 +0200 Subject: [PATCH 046/155] snapshot runs on new config --- src/abgleich/core/snapshot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index ea46605..13bcc53 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -67,7 +67,7 @@ def __init__( self._side = side self._config = config - self._root = root(config[side]["zpool"], config[side]["prefix"]) + self._root = root(config[f"{side:s}/zpool"], config[f"{side:s}/prefix"]) assert self._parent.startswith(self._root) self._subparent = self._parent[len(self._root) :].strip("/") From 5dd28909d2e2fb8029f9fe5c2610eeb1b2a0dd0d Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 21:22:57 +0200 Subject: [PATCH 047/155] zpool runs on new config --- src/abgleich/core/zpool.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 416ed5e..f73941a 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -71,7 +71,7 @@ def __init__( self._side = side self._config = config - self._root = root(config[side]["zpool"], config[side]["prefix"]) + self._root = root(config[f"{side:s}/zpool"], config[f"{side:s}/prefix"]) def __eq__(self, other: ZpoolABC) -> bool: @@ -346,7 +346,7 @@ def available(side: str, config: ConfigABC,) -> int: "available", "-H", "-p", - root(config[side]["zpool"], config[side]["prefix"]), + root(config[f"{side:s}/zpool"], config[f"{side:s}/prefix"]), ], side, config, @@ -357,7 +357,8 @@ def available(side: str, config: ConfigABC,) -> int: @classmethod def from_config(cls, side: str, config: ConfigABC,) -> ZpoolABC: - root_dataset = root(config[side]["zpool"], config[side]["prefix"]) + side_config = config.group(side) + root_dataset = root(side_config["zpool"], side_config["prefix"]) output, errors, returncode, exception = Command.on_side( ["zfs", "get", "all", "-r", "-H", "-p", root_dataset,], side, config, @@ -375,7 +376,7 @@ def from_config(cls, side: str, config: ConfigABC,) -> ZpoolABC: for line_list in output: entities[line_list[0]].append(line_list[1:]) - if not config.get("include_root", True): + if not config["include_root"]: entities.pop(root_dataset) for name in [ snapshot From f744f8a64733708677af0058b7f774cc65469091 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 21:25:35 +0200 Subject: [PATCH 048/155] log changes --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 51ee225..46a3e8d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## 0.0.8 (2020-XX-XX) +- FEATURE: Configuration module contains default values for parameters, making it much easier to write lightweight configuration files, see #28. The configuration parser now also provides much more useful output. - FIX: Many cleanups in code base, enabling future developments. ## 0.0.7 (2020-08-05) From a819f5f17d39ede272a30b76e4421a15765af1fa Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 26 Aug 2020 21:28:55 +0200 Subject: [PATCH 049/155] black --- src/abgleich/core/command.py | 4 +- src/abgleich/core/config.py | 25 ++++--- src/abgleich/core/configfield.py | 31 ++++----- src/abgleich/core/configspec.py | 113 ++++++++++++------------------- src/abgleich/core/lib.py | 2 +- 5 files changed, 74 insertions(+), 101 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 65ad976..271d6fd 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -126,7 +126,9 @@ def on_side(cls, cmd: List[str], side: str, config: ConfigABC) -> CommandABC: if side_config["host"] == "localhost": return cls(cmd) - return cls.with_ssh(cmd, side_config=side_config, ssh_config=config.group('ssh')) + return cls.with_ssh( + cmd, side_config=side_config, ssh_config=config.group("ssh") + ) @classmethod def with_ssh( diff --git a/src/abgleich/core/config.py b/src/abgleich/core/config.py index a11ecb8..2cb6c2d 100644 --- a/src/abgleich/core/config.py +++ b/src/abgleich/core/config.py @@ -47,6 +47,7 @@ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + @typechecked class Config(ConfigABC): """ @@ -60,15 +61,19 @@ def __init__(self, root: Union[str, None] = None, **kwargs: ConfigFieldABC): def __repr__(self): - return '' if self._root is None else f'' + return "" if self._root is None else f'' def __getitem__(self, key: str) -> ConfigValueTypes: - return self._fields[key].value if self._root is None else self._fields[f'{self._root:s}/{key:s}'].value + return ( + self._fields[key].value + if self._root is None + else self._fields[f"{self._root:s}/{key:s}"].value + ) def group(self, root: str) -> ConfigABC: - return type(self)(root = root, **self._fields) + return type(self)(root=root, **self._fields) @classmethod def _flatten_dict_tree(cls, data: Dict, root: Union[str, None] = None) -> Dict: @@ -77,12 +82,12 @@ def _flatten_dict_tree(cls, data: Dict, root: Union[str, None] = None) -> Dict: for key, value in data.items(): if not isinstance(key, str): - raise TypeError('configuration key is no string', key) + raise TypeError("configuration key is no string", key) if root is not None: if len(root) > 0: - key = f'{root:s}/{key:s}' + key = f"{root:s}/{key:s}" if isinstance(value, dict): - flat_data.update(cls._flatten_dict_tree(data = value, root = key)) + flat_data.update(cls._flatten_dict_tree(data=value, root=key)) else: flat_data[key] = value @@ -99,15 +104,15 @@ def from_text(cls, text: str) -> ConfigABC: config = yaml.load(text, Loader=Loader) if not isinstance(config, dict): - raise TypeError('config is no dict', config) + raise TypeError("config is no dict", config) config_fields = {field.name: field.copy() for field in CONFIGSPEC} - for key, value in cls._flatten_dict_tree(data = config).items(): + for key, value in cls._flatten_dict_tree(data=config).items(): if key not in config_fields.keys(): - raise ValueError('unknown configuration key', key) + raise ValueError("unknown configuration key", key) config_fields[key].value = value if any((not field.valid for field in config_fields.values())): - raise ValueError('configuration is not valid') + raise ValueError("configuration is not valid") return cls(**config_fields) diff --git a/src/abgleich/core/configfield.py b/src/abgleich/core/configfield.py index edacd21..75aef26 100644 --- a/src/abgleich/core/configfield.py +++ b/src/abgleich/core/configfield.py @@ -44,16 +44,15 @@ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + @typechecked class ConfigField(ConfigFieldABC): """ Mutable. """ - def __init__(self, - name: str, - validate: Callable, - default: ConfigValueTypes = None, + def __init__( + self, name: str, validate: Callable, default: ConfigValueTypes = None, ): self._name = name @@ -62,26 +61,24 @@ def __init__(self, if self._default is not None: if not self._validate(self._default): - raise ValueError(f'invalid default value for {self._name}') + raise ValueError(f"invalid default value for {self._name}") self._value = None def __repr__(self) -> str: return ( - '' - ) + ">" + ) def copy(self) -> ConfigFieldABC: return type(self)( - name = self._name, - default = self._default, - validate = self._validate, + name=self._name, default=self._default, validate=self._validate, ) @property @@ -96,7 +93,7 @@ def value(self) -> ConfigValueTypes: return self._value if self._default is None: - raise ValueError(f'required value for {self._name} missing') + raise ValueError(f"required value for {self._name} missing") return self._default @@ -104,21 +101,19 @@ def value(self) -> ConfigValueTypes: def value(self, value: ConfigValueTypes): if self._value is not None: - raise ValueError(f'value for {self._name} has already been set') + raise ValueError(f"value for {self._name} has already been set") if value is None: return if not self._validate(value): - raise ValueError(f'invalid value for {self._name}') + raise ValueError(f"invalid value for {self._name}") self._value = value @property def valid(self) -> bool: - return ( - (self._value is not None and self._validate(self._value)) - or - (self._default is not None and self._validate(self._default)) + return (self._value is not None and self._validate(self._value)) or ( + self._default is not None and self._validate(self._default) ) @property diff --git a/src/abgleich/core/configspec.py b/src/abgleich/core/configspec.py index f885358..5894de6 100644 --- a/src/abgleich/core/configspec.py +++ b/src/abgleich/core/configspec.py @@ -36,96 +36,67 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ CONFIGSPEC = [ - ConfigField( - name = "keep_snapshots", - validate = lambda v: isinstance(v, int) and v >= 1, - default = 1, + name="keep_snapshots", + validate=lambda v: isinstance(v, int) and v >= 1, + default=1, ), - ConfigField( - name = "suffix", - validate = lambda v: isinstance(v, str) and valid_name(v, min_len = 0), - default = '', + name="suffix", + validate=lambda v: isinstance(v, str) and valid_name(v, min_len=0), + default="", ), - ConfigField( - name = "digits", - validate = lambda v: isinstance(v, int) and v >= 1, - default = 2, + name="digits", validate=lambda v: isinstance(v, int) and v >= 1, default=2, ), - ConfigField( - name = "always_changed", - validate = lambda v: isinstance(v, bool), - default = False, + name="always_changed", validate=lambda v: isinstance(v, bool), default=False, ), - ConfigField( - name = "written_threshold", - validate = lambda v: isinstance(v, int) and v > 0, - default = 1024 ** 2, + name="written_threshold", + validate=lambda v: isinstance(v, int) and v > 0, + default=1024 ** 2, ), - ConfigField( - name = "check_diff", - validate = lambda v: isinstance(v, bool), - default = True, + name="check_diff", validate=lambda v: isinstance(v, bool), default=True, ), - ConfigField( - name = "ignore", - validate = lambda v: isinstance(v, list) and all((isinstance(item, str) and len(item) > 0 for item in v)), - default = list(), + name="ignore", + validate=lambda v: isinstance(v, list) + and all((isinstance(item, str) and len(item) > 0 for item in v)), + default=list(), ), - ConfigField( - name = "include_root", - validate = lambda v: isinstance(v, bool), - default = True, + name="include_root", validate=lambda v: isinstance(v, bool), default=True, ), - ConfigField( - name = "ssh/compression", - validate = lambda v: isinstance(v, bool), - default = False, + name="ssh/compression", validate=lambda v: isinstance(v, bool), default=False, ), - - ConfigField( - name = "ssh/cipher", - validate = lambda v: isinstance(v, str), - default = '', - ), - + ConfigField(name="ssh/cipher", validate=lambda v: isinstance(v, str), default="",), ] -for _side in ('source', 'target'): - - CONFIGSPEC.extend([ - - ConfigField( - name = f"{_side}/zpool", - validate = lambda v: isinstance(v, str) and len(v) > 0, - ), - - ConfigField( - name = f"{_side}/prefix", - validate = lambda v: isinstance(v, str), - default = '', - ), - - ConfigField( - name = f"{_side}/host", - validate = lambda v: isinstance(v, str) and len(v) > 0, - default = 'localhost', - ), - - ConfigField( - name = f"{_side}/user", - validate = lambda v: isinstance(v, str), - default = '', - ), - - ]) +for _side in ("source", "target"): + + CONFIGSPEC.extend( + [ + ConfigField( + name=f"{_side}/zpool", + validate=lambda v: isinstance(v, str) and len(v) > 0, + ), + ConfigField( + name=f"{_side}/prefix", + validate=lambda v: isinstance(v, str), + default="", + ), + ConfigField( + name=f"{_side}/host", + validate=lambda v: isinstance(v, str) and len(v) > 0, + default="localhost", + ), + ConfigField( + name=f"{_side}/user", validate=lambda v: isinstance(v, str), default="", + ), + ] + ) del _side diff --git a/src/abgleich/core/lib.py b/src/abgleich/core/lib.py index 2e83029..678b180 100644 --- a/src/abgleich/core/lib.py +++ b/src/abgleich/core/lib.py @@ -45,7 +45,7 @@ def is_host_up(side: str, config: ConfigABC) -> bool: assert side in ("source", "target") - if config[f'{side:s}/host'] == "localhost": + if config[f"{side:s}/host"] == "localhost": return True _, _, returncode, _ = Command.on_side(["exit"], side, config).run(returncode=True) From cd5abc99c39b750fdc88cb6e7ef4c9ecfd48ab8c Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 09:41:43 +0200 Subject: [PATCH 050/155] ssh port on source and target becomes configurable, implementing #22 --- CHANGES.md | 1 + README.md | 2 +- src/abgleich/core/command.py | 4 +++- src/abgleich/core/configspec.py | 5 +++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 46a3e8d..55cc6ad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## 0.0.8 (2020-XX-XX) +- FEATURE: `ssh`-port on source and target becomes configurable, see #22. - FEATURE: Configuration module contains default values for parameters, making it much easier to write lightweight configuration files, see #28. The configuration parser now also provides much more useful output. - FIX: Many cleanups in code base, enabling future developments. diff --git a/README.md b/README.md index c8f1a1c..b59b4f8 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ ssh: cipher: aes256-gcm@openssh.com ``` -The prefix can be empty on either side. If a `host` is set to `localhost`, the `user` field can be left empty. Both source and target can be remote hosts or localhost at the same time. `include_root` indicates whether `{zpool}{/{prefix}}` should be included in all operations. `keep_snapshots` is an integer and must be greater or equal to `1`. It specifies the number of snapshots that are kept per dataset on the source side when a cleanup operation is triggered. `suffix` contains the name suffix for new snapshots. Setting `always_changed` to `yes` causes `abgleich` to beliefe that all datasets have always changed since the last snapshot, completely ignoring what ZFS actually reports. No diff will be produced & checked for values of `written` lower than `written_threshold`. Checking diffs can be completely deactivated by setting `check_diff` to `no`. `digits` specifies how many digits are used for a decimal number describing the n-th snapshot per dataset per day as part of the name of new snapshots. `ignore` lists stuff underneath the `prefix` which will be ignored by this tool, i.e. no snapshots, backups or cleanups. `ssh` allows to fine-tune the speed of backups. In fast local networks, it is best to set `compression` to `no` because the compression is usually slowing down the transfer. However, for low-bandwidth transmissions, it makes sense to set it to `yes`. For significantly better speed in fast local networks, make sure that both the source and the target system support a common cipher, which is accelerated by [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) on both ends. +The prefix can be empty on either side. If a `host` is set to `localhost`, the `user` field can be left empty. Both source and target can be remote hosts or localhost at the same time. `include_root` indicates whether `{zpool}{/{prefix}}` should be included in all operations. `keep_snapshots` is an integer and must be greater or equal to `1`. It specifies the number of snapshots that are kept per dataset on the source side when a cleanup operation is triggered. `suffix` contains the name suffix for new snapshots. Setting `always_changed` to `yes` causes `abgleich` to beliefe that all datasets have always changed since the last snapshot, completely ignoring what ZFS actually reports. No diff will be produced & checked for values of `written` lower than `written_threshold`. Checking diffs can be completely deactivated by setting `check_diff` to `no`. `digits` specifies how many digits are used for a decimal number describing the n-th snapshot per dataset per day as part of the name of new snapshots. `ignore` lists stuff underneath the `prefix` which will be ignored by this tool, i.e. no snapshots, backups or cleanups. `ssh` allows to fine-tune the speed of backups. In fast local networks, it is best to set `compression` to `no` because the compression is usually slowing down the transfer. However, for low-bandwidth transmissions, it makes sense to set it to `yes`. For significantly better speed in fast local networks, make sure that both the source and the target system support a common cipher, which is accelerated by [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) on both ends. The `ssh` port can be specified per side, i.e. `source` and/or `target`. ## USAGE diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 271d6fd..e83abbb 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -139,7 +139,9 @@ def with_ssh( cmd = [ "ssh", "-T", # Disable pseudo-terminal allocation - "-o", + "-p", # Port parameter + f'{side_config["port"]:d}', + "-o", # Option parameter "Compression=yes" if ssh_config["compression"] else "Compression=no", ] if ssh_config["cipher"] is not None: diff --git a/src/abgleich/core/configspec.py b/src/abgleich/core/configspec.py index 5894de6..223174b 100644 --- a/src/abgleich/core/configspec.py +++ b/src/abgleich/core/configspec.py @@ -96,6 +96,11 @@ ConfigField( name=f"{_side}/user", validate=lambda v: isinstance(v, str), default="", ), + ConfigField( + name=f"{_side}/port", + validate=lambda v: isinstance(v, int) and v > 0, + default=22, + ), ] ) From 6df0e0e63e7da65e2d2009298b7b190f18e7e2cd Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 09:49:48 +0200 Subject: [PATCH 051/155] fix: allow empty prefix --- src/abgleich/core/lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/abgleich/core/lib.py b/src/abgleich/core/lib.py index 678b180..f1e829b 100644 --- a/src/abgleich/core/lib.py +++ b/src/abgleich/core/lib.py @@ -72,6 +72,8 @@ def root(zpool: str, prefix: Union[str, None]) -> str: if prefix is None: return zpool + if len(prefix) == 0: + return zpool return join(zpool, prefix) From 93812a1d6ce4686b4b8b92facc45907a19c4cb99 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 12:50:48 +0200 Subject: [PATCH 052/155] new command: handles infinite number of pipes, uses shlex for joins and escapes --- src/abgleich/core/command.py | 114 ++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index e83abbb..deccd5e 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -28,8 +28,9 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -import subprocess +from subprocess import Popen, PIPE from typing import List, Tuple, Union +import shlex from typeguard import typechecked @@ -46,77 +47,78 @@ class Command(CommandABC): Immutable. """ - def __init__(self, cmd: List[str]): + def __init__(self, cmd: List[List[str]]): - self._cmd = cmd.copy() + self._cmd = [fragment.copy() for fragment in cmd] + + def __repr__(self) -> str: + + return "" def __str__(self) -> str: - return " ".join([item.replace(" ", "\\ ") for item in self._cmd]) + return " | ".join([shlex.join(fragment) for fragment in self._cmd]) + + def __len__(self) -> int: + + return len(self._cmd) + + def __or__(self, other: CommandABC) -> CommandABC: # pipe + + return type(self)(self.cmd + other.cmd) + + @staticmethod + def _com_to_str(com: Union[str, bytes, None]) -> str: + + if com is None: + return "" + + if isinstance(com, bytes): + return com.decode("utf-8") + + return com def run( self, returncode: bool = False - ) -> Union[Tuple[str, str], Tuple[str, str, int, Exception]]: + ) -> Union[ + Tuple[List[str], List[str], List[int], Exception], Tuple[List[str], List[str]] + ]: - proc = subprocess.Popen( - self.cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - output, errors = proc.communicate() - status = not bool(proc.returncode) - output, errors = output.decode("utf-8"), errors.decode("utf-8") + procs = [] # all processes, connected with pipes + + for index, fragment in enumerate(self._cmd): # create & connect processes + + stdin = None if index == 0 else procs[-1].stdout # output of last process + proc = Popen(fragment, stdout=PIPE, stderr=PIPE, stdin=stdin,) + procs.append(proc) + + output, errors, status = [], [], [] + + for proc in procs[::-1]: # inverse order, last process first + + out, err = proc.communicate() + output.append(self._com_to_str(out)) + errors.append(self._com_to_str(err)) + status.append(int(proc.returncode)) + + output.reverse() + errors.reverse() + status.reverse() exception = SystemError("command failed", str(self), output, errors) if returncode: - return output, errors, int(proc.returncode), exception + return output, errors, status, exception - if not status or len(errors.strip()) > 0: + if any((code != 0 for code in status)): # some fragment failed: raise exception return output, errors - def run_pipe(self, other: CommandABC): - - proc_1 = subprocess.Popen( - self.cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - proc_2 = subprocess.Popen( - other.cmd, - stdin=proc_1.stdout, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - output_2, errors_2 = proc_2.communicate() - status_2 = not bool(proc_2.returncode) - _, errors_1 = proc_1.communicate() - status_1 = not bool(proc_1.returncode) - - errors_1 = errors_1.decode("utf-8") - output_2, errors_2 = output_2.decode("utf-8"), errors_2.decode("utf-8") - - if any( - ( - not status_1, - len(errors_1.strip()) > 0, - not status_2, - len(errors_2.strip()) > 0, - ) - ): - raise SystemError( - "command pipe failed", - f"{str(self):s} | {str(other):s}", - errors_1, - output_2, - errors_2, - ) - - return errors_1, output_2, errors_2 - @property - def cmd(self) -> List[str]: + def cmd(self) -> List[List[str]]: - return self._cmd.copy() + return [fragment.copy() for fragment in self._cmd] @classmethod def on_side(cls, cmd: List[str], side: str, config: ConfigABC) -> CommandABC: @@ -124,7 +126,7 @@ def on_side(cls, cmd: List[str], side: str, config: ConfigABC) -> CommandABC: side_config = config.group(side) if side_config["host"] == "localhost": - return cls(cmd) + return cls([cmd]) return cls.with_ssh( cmd, side_config=side_config, ssh_config=config.group("ssh") @@ -135,7 +137,7 @@ def with_ssh( cls, cmd: List[str], side_config: ConfigABC, ssh_config: ConfigABC ) -> CommandABC: - cmd_str = " ".join([item.replace(" ", "\\ ") for item in cmd]) + cmd_str = shlex.join(cmd) cmd = [ "ssh", "-T", # Disable pseudo-terminal allocation @@ -148,4 +150,4 @@ def with_ssh( cmd.extend(("-c", ssh_config["cipher"])) cmd.extend([f'{side_config["user"]:s}@{side_config["host"]:s}', cmd_str]) - return cls(cmd) + return cls([cmd]) From a183e77660d03360da02da326894638a0f7d5c8f Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 12:51:07 +0200 Subject: [PATCH 053/155] uses new command run --- src/abgleich/core/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/abgleich/core/lib.py b/src/abgleich/core/lib.py index f1e829b..0cf0716 100644 --- a/src/abgleich/core/lib.py +++ b/src/abgleich/core/lib.py @@ -49,8 +49,8 @@ def is_host_up(side: str, config: ConfigABC) -> bool: return True _, _, returncode, _ = Command.on_side(["exit"], side, config).run(returncode=True) - assert returncode in (0, 255) - return returncode == 0 + assert returncode[0] in (0, 255) + return returncode[0] == 0 @typechecked From 5eae892a91f22cd9a231c5fbd373ed3152e6edbd Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 12:52:22 +0200 Subject: [PATCH 054/155] transaction runs on new command class, does not care about number of commands between pipes, no more list of comands --- src/abgleich/core/transaction.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/abgleich/core/transaction.py b/src/abgleich/core/transaction.py index c1a5837..e145cf3 100644 --- a/src/abgleich/core/transaction.py +++ b/src/abgleich/core/transaction.py @@ -49,12 +49,10 @@ class Transaction(TransactionABC): """ def __init__( - self, meta: TransactionMetaABC, commands: List[CommandABC], + self, meta: TransactionMetaABC, command: CommandABC, ): - assert len(commands) in (1, 2) - - self._meta, self._commands = meta, commands + self._meta, self._command = meta, command self._complete = False self._running = False @@ -78,9 +76,9 @@ def complete(self) -> bool: return self._complete @property - def commands(self) -> Tuple[CommandABC]: + def command(self) -> CommandABC: - return self._commands + return self._command @property def error(self) -> Union[Exception, None]: @@ -107,12 +105,7 @@ def run(self): self._changed() try: - if len(self._commands) == 1: - output, errors = self._commands[0].run() - else: - errors_1, output_2, errors_2 = self._commands[0].run_pipe( - self._commands[1] - ) + _, _ = self._command.run() except SystemError as error: self._error = error finally: @@ -293,7 +286,7 @@ def run(self): print( f'({colorize(transaction.meta[t("type")], "white"):s}) ' - f'{colorize(" | ".join([str(command) for command in transaction.commands]), "yellow"):s}' + f'{colorize(str(transaction.command), "yellow"):s}' ) assert not transaction.running From 6bc4b267876837afacfee15f5feffe78128caecc Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 12:52:55 +0200 Subject: [PATCH 055/155] dataset runs on new command class --- src/abgleich/core/dataset.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index 47bb34a..adf57d2 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -132,7 +132,7 @@ def changed(self) -> bool: self._side, self._config, ).run() - return len(output.strip(" \t\n")) > 0 + return len(output[0].strip(" \t\n")) > 0 @property def name(self) -> str: @@ -159,7 +159,7 @@ def get_snapshot_transaction(self) -> TransactionABC: snapshot_name = self._new_snapshot_name() return Transaction( - TransactionMeta( + meta=TransactionMeta( **{ t("type"): t("snapshot"), t("dataset_subname"): self._subname, @@ -167,13 +167,11 @@ def get_snapshot_transaction(self) -> TransactionABC: t("written"): self._properties["written"].value, } ), - [ - Command.on_side( - ["zfs", "snapshot", f"{self._name:s}@{snapshot_name:s}"], - self._side, - self._config, - ) - ], + command=Command.on_side( + ["zfs", "snapshot", f"{self._name:s}@{snapshot_name:s}"], + self._side, + self._config, + ), ) def _new_snapshot_name(self) -> str: From c1972299c034b931ed80f3e761487e33c3d54beb Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 12:54:08 +0200 Subject: [PATCH 056/155] snapshot runs on new command class --- src/abgleich/core/snapshot.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 13bcc53..33077c6 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -92,13 +92,11 @@ def get_cleanup_transaction(self) -> TransactionABC: t("snapshot_name"): self._name, } ), - commands=[ - Command.on_side( - ["zfs", "destroy", f"{self._parent:s}@{self._name:s}"], - self._side, - self._config, - ) - ], + command=Command.on_side( + ["zfs", "destroy", f"{self._parent:s}@{self._name:s}"], + self._side, + self._config, + ), ) def get_backup_transaction( @@ -109,8 +107,7 @@ def get_backup_transaction( ancestor = self.ancestor - commands = [ - Command.on_side( + command = Command.on_side( ["zfs", "send", "-c", f"{source_dataset:s}@{self.name:s}",] if ancestor is None else [ @@ -123,11 +120,9 @@ def get_backup_transaction( ], "source", self._config, - ), - Command.on_side( + ) | Command.on_side( ["zfs", "receive", f"{target_dataset:s}"], "target", self._config - ), - ] + ) return Transaction( meta=TransactionMeta( @@ -140,7 +135,7 @@ def get_backup_transaction( t("snapshot_name"): self.name, } ), - commands=commands, + command=command, ) @property From b6d9268885c93cb508acc3396011df61e23df4cf Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 12:54:34 +0200 Subject: [PATCH 057/155] zpool runs on new command class --- src/abgleich/core/zpool.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index f73941a..f6bfc01 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -352,7 +352,7 @@ def available(side: str, config: ConfigABC,) -> int: config, ).run() - return Property.from_params(*output.strip().split("\t")[1:]).value + return Property.from_params(*output[0].strip().split("\t")[1:]).value @classmethod def from_config(cls, side: str, config: ConfigABC,) -> ZpoolABC: @@ -364,13 +364,13 @@ def from_config(cls, side: str, config: ConfigABC,) -> ZpoolABC: ["zfs", "get", "all", "-r", "-H", "-p", root_dataset,], side, config, ).run(returncode=True) - if returncode != 0 and "dataset does not exist" in errors: + if returncode[0] != 0 and "dataset does not exist" in errors[0]: return cls(datasets=[], side=side, config=config,) - if returncode != 0: + if returncode[0] != 0: raise exception output = [ - line.split("\t") for line in output.split("\n") if len(line.strip()) > 0 + line.split("\t") for line in output[0].split("\n") if len(line.strip()) > 0 ] entities = OrderedDict((line[0], []) for line in output) for line_list in output: From b2532ce3a14ced979f006f109fbfd291dc10ff7b Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 12:56:27 +0200 Subject: [PATCH 058/155] log changes --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 55cc6ad..3725b7d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ - FEATURE: `ssh`-port on source and target becomes configurable, see #22. - FEATURE: Configuration module contains default values for parameters, making it much easier to write lightweight configuration files, see #28. The configuration parser now also provides much more useful output. +- FEATURE: Significantly more flexible shell command wrapper and, as a result, cleaned up transaction handling. - FIX: Many cleanups in code base, enabling future developments. ## 0.0.7 (2020-08-05) From 5b05a3cb57a9ec4563ca43f3414378242818c8fc Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 12:58:22 +0200 Subject: [PATCH 059/155] black --- src/abgleich/core/snapshot.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 33077c6..c996484 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -108,21 +108,21 @@ def get_backup_transaction( ancestor = self.ancestor command = Command.on_side( - ["zfs", "send", "-c", f"{source_dataset:s}@{self.name:s}",] - if ancestor is None - else [ - "zfs", - "send", - "-c", - "-i", - f"{source_dataset:s}@{ancestor.name:s}", - f"{source_dataset:s}@{self.name:s}", - ], - "source", - self._config, - ) | Command.on_side( - ["zfs", "receive", f"{target_dataset:s}"], "target", self._config - ) + ["zfs", "send", "-c", f"{source_dataset:s}@{self.name:s}",] + if ancestor is None + else [ + "zfs", + "send", + "-c", + "-i", + f"{source_dataset:s}@{ancestor.name:s}", + f"{source_dataset:s}@{self.name:s}", + ], + "source", + self._config, + ) | Command.on_side( + ["zfs", "receive", f"{target_dataset:s}"], "target", self._config + ) return Transaction( meta=TransactionMeta( From 727043c984de2274cbc3f20be8a265c0fad5e3f2 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 13:32:36 +0200 Subject: [PATCH 060/155] on_side generates new command from command --- src/abgleich/core/command.py | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index deccd5e..52b3613 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -115,30 +115,15 @@ def run( return output, errors - @property - def cmd(self) -> List[List[str]]: + def on_side(self, side: str, config: ConfigABC) -> CommandABC: - return [fragment.copy() for fragment in self._cmd] - - @classmethod - def on_side(cls, cmd: List[str], side: str, config: ConfigABC) -> CommandABC: + if config[f'{side:s}/host'] == "localhost": + return self side_config = config.group(side) + ssh_config = config.group("ssh") - if side_config["host"] == "localhost": - return cls([cmd]) - - return cls.with_ssh( - cmd, side_config=side_config, ssh_config=config.group("ssh") - ) - - @classmethod - def with_ssh( - cls, cmd: List[str], side_config: ConfigABC, ssh_config: ConfigABC - ) -> CommandABC: - - cmd_str = shlex.join(cmd) - cmd = [ + cmd_ssh = [ "ssh", "-T", # Disable pseudo-terminal allocation "-p", # Port parameter @@ -147,7 +132,22 @@ def with_ssh( "Compression=yes" if ssh_config["compression"] else "Compression=no", ] if ssh_config["cipher"] is not None: - cmd.extend(("-c", ssh_config["cipher"])) - cmd.extend([f'{side_config["user"]:s}@{side_config["host"]:s}', cmd_str]) + cmd_ssh.extend(("-c", ssh_config["cipher"])) + cmd_ssh.extend([f'{side_config["user"]:s}@{side_config["host"]:s}', str(self)]) + + return type(self)([cmd_ssh]) + + @property + def cmd(self) -> List[List[str]]: + + return [fragment.copy() for fragment in self._cmd] + + @classmethod + def from_str(cls, cmd: str) -> CommandABC: + + return cls.from_list(shlex.split(cmd)) + + @classmethod + def from_list(cls, cmd: List[str]) -> CommandABC: return cls([cmd]) From 14ffd31a37f224594e713716ee22117133059f1c Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 13:33:01 +0200 Subject: [PATCH 061/155] new on_side --- src/abgleich/core/dataset.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index adf57d2..cd627a9 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -127,11 +127,9 @@ def changed(self) -> bool: if not self._config["check_diff"]: return True - output, _ = Command.on_side( - ["zfs", "diff", f"{self._name:s}@{self._snapshots[-1].name:s}"], - self._side, - self._config, - ).run() + output, _ = Command.from_list( + ["zfs", "diff", f"{self._name:s}@{self._snapshots[-1].name:s}"] + ).on_side(side = self._side, config = self._config).run() return len(output[0].strip(" \t\n")) > 0 @property @@ -167,11 +165,9 @@ def get_snapshot_transaction(self) -> TransactionABC: t("written"): self._properties["written"].value, } ), - command=Command.on_side( - ["zfs", "snapshot", f"{self._name:s}@{snapshot_name:s}"], - self._side, - self._config, - ), + command=Command.from_list( + ["zfs", "snapshot", f"{self._name:s}@{snapshot_name:s}"] + ).on_side(side = self._side, config = self._config), ) def _new_snapshot_name(self) -> str: From a4a4ff7bd3776ec540e84adc726786e10855d885 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 13:33:18 +0200 Subject: [PATCH 062/155] new on_side --- src/abgleich/core/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/core/lib.py b/src/abgleich/core/lib.py index 0cf0716..8fc9f90 100644 --- a/src/abgleich/core/lib.py +++ b/src/abgleich/core/lib.py @@ -48,7 +48,7 @@ def is_host_up(side: str, config: ConfigABC) -> bool: if config[f"{side:s}/host"] == "localhost": return True - _, _, returncode, _ = Command.on_side(["exit"], side, config).run(returncode=True) + _, _, returncode, _ = Command.from_list(["exit"]).on_side(side = side, config = config).run(returncode=True) assert returncode[0] in (0, 255) return returncode[0] == 0 From f49c785453dcbb38f4ef2db5afd48919ac3ecfb6 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 14:48:27 +0200 Subject: [PATCH 063/155] new on_side --- src/abgleich/core/snapshot.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index c996484..0fa891d 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -92,11 +92,9 @@ def get_cleanup_transaction(self) -> TransactionABC: t("snapshot_name"): self._name, } ), - command=Command.on_side( - ["zfs", "destroy", f"{self._parent:s}@{self._name:s}"], - self._side, - self._config, - ), + command=Command.from_list( + ["zfs", "destroy", f"{self._parent:s}@{self._name:s}"] + ).on_side(side = self._side, config = self._config), ) def get_backup_transaction( @@ -107,7 +105,7 @@ def get_backup_transaction( ancestor = self.ancestor - command = Command.on_side( + command = Command.from_list( ["zfs", "send", "-c", f"{source_dataset:s}@{self.name:s}",] if ancestor is None else [ @@ -117,12 +115,10 @@ def get_backup_transaction( "-i", f"{source_dataset:s}@{ancestor.name:s}", f"{source_dataset:s}@{self.name:s}", - ], - "source", - self._config, - ) | Command.on_side( - ["zfs", "receive", f"{target_dataset:s}"], "target", self._config - ) + ] + ).on_side(side = "source", config = self._config) | Command.from_list( + ["zfs", "receive", f"{target_dataset:s}"] + ).on_side(side = "target", config = self._config) return Transaction( meta=TransactionMeta( From 27c764596b75c7a35d054b223a018517596b5227 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 14:48:42 +0200 Subject: [PATCH 064/155] new on_side --- src/abgleich/core/zpool.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index f6bfc01..8433887 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -339,7 +339,7 @@ def _comparison_table_row(item: ComparisonItemABC) -> List[str]: @staticmethod def available(side: str, config: ConfigABC,) -> int: - output, _ = Command.on_side( + output, _ = Command.from_list( [ "zfs", "get", @@ -347,10 +347,8 @@ def available(side: str, config: ConfigABC,) -> int: "-H", "-p", root(config[f"{side:s}/zpool"], config[f"{side:s}/prefix"]), - ], - side, - config, - ).run() + ] + ).on_side(side = side, config = config).run() return Property.from_params(*output[0].strip().split("\t")[1:]).value @@ -360,9 +358,9 @@ def from_config(cls, side: str, config: ConfigABC,) -> ZpoolABC: side_config = config.group(side) root_dataset = root(side_config["zpool"], side_config["prefix"]) - output, errors, returncode, exception = Command.on_side( - ["zfs", "get", "all", "-r", "-H", "-p", root_dataset,], side, config, - ).run(returncode=True) + output, errors, returncode, exception = Command.from_list( + ["zfs", "get", "all", "-r", "-H", "-p", root_dataset,] + ).on_side(side = side, config = config).run(returncode=True) if returncode[0] != 0 and "dataset does not exist" in errors[0]: return cls(datasets=[], side=side, config=config,) From fdb7824e4fc1b5d65044ab911a13e3c70110c9ee Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 14:50:49 +0200 Subject: [PATCH 065/155] black --- src/abgleich/core/command.py | 2 +- src/abgleich/core/dataset.py | 12 ++++++++---- src/abgleich/core/lib.py | 6 +++++- src/abgleich/core/snapshot.py | 8 +++++--- src/abgleich/core/zpool.py | 32 +++++++++++++++++++------------- 5 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 52b3613..5716152 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -117,7 +117,7 @@ def run( def on_side(self, side: str, config: ConfigABC) -> CommandABC: - if config[f'{side:s}/host'] == "localhost": + if config[f"{side:s}/host"] == "localhost": return self side_config = config.group(side) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index cd627a9..34fdfda 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -127,9 +127,13 @@ def changed(self) -> bool: if not self._config["check_diff"]: return True - output, _ = Command.from_list( - ["zfs", "diff", f"{self._name:s}@{self._snapshots[-1].name:s}"] - ).on_side(side = self._side, config = self._config).run() + output, _ = ( + Command.from_list( + ["zfs", "diff", f"{self._name:s}@{self._snapshots[-1].name:s}"] + ) + .on_side(side=self._side, config=self._config) + .run() + ) return len(output[0].strip(" \t\n")) > 0 @property @@ -167,7 +171,7 @@ def get_snapshot_transaction(self) -> TransactionABC: ), command=Command.from_list( ["zfs", "snapshot", f"{self._name:s}@{snapshot_name:s}"] - ).on_side(side = self._side, config = self._config), + ).on_side(side=self._side, config=self._config), ) def _new_snapshot_name(self) -> str: diff --git a/src/abgleich/core/lib.py b/src/abgleich/core/lib.py index 8fc9f90..d34eb70 100644 --- a/src/abgleich/core/lib.py +++ b/src/abgleich/core/lib.py @@ -48,7 +48,11 @@ def is_host_up(side: str, config: ConfigABC) -> bool: if config[f"{side:s}/host"] == "localhost": return True - _, _, returncode, _ = Command.from_list(["exit"]).on_side(side = side, config = config).run(returncode=True) + _, _, returncode, _ = ( + Command.from_list(["exit"]) + .on_side(side=side, config=config) + .run(returncode=True) + ) assert returncode[0] in (0, 255) return returncode[0] == 0 diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 0fa891d..4ea938a 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -94,7 +94,7 @@ def get_cleanup_transaction(self) -> TransactionABC: ), command=Command.from_list( ["zfs", "destroy", f"{self._parent:s}@{self._name:s}"] - ).on_side(side = self._side, config = self._config), + ).on_side(side=self._side, config=self._config), ) def get_backup_transaction( @@ -116,9 +116,11 @@ def get_backup_transaction( f"{source_dataset:s}@{ancestor.name:s}", f"{source_dataset:s}@{self.name:s}", ] - ).on_side(side = "source", config = self._config) | Command.from_list( + ).on_side(side="source", config=self._config) | Command.from_list( ["zfs", "receive", f"{target_dataset:s}"] - ).on_side(side = "target", config = self._config) + ).on_side( + side="target", config=self._config + ) return Transaction( meta=TransactionMeta( diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 8433887..b470f02 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -339,16 +339,20 @@ def _comparison_table_row(item: ComparisonItemABC) -> List[str]: @staticmethod def available(side: str, config: ConfigABC,) -> int: - output, _ = Command.from_list( - [ - "zfs", - "get", - "available", - "-H", - "-p", - root(config[f"{side:s}/zpool"], config[f"{side:s}/prefix"]), - ] - ).on_side(side = side, config = config).run() + output, _ = ( + Command.from_list( + [ + "zfs", + "get", + "available", + "-H", + "-p", + root(config[f"{side:s}/zpool"], config[f"{side:s}/prefix"]), + ] + ) + .on_side(side=side, config=config) + .run() + ) return Property.from_params(*output[0].strip().split("\t")[1:]).value @@ -358,9 +362,11 @@ def from_config(cls, side: str, config: ConfigABC,) -> ZpoolABC: side_config = config.group(side) root_dataset = root(side_config["zpool"], side_config["prefix"]) - output, errors, returncode, exception = Command.from_list( - ["zfs", "get", "all", "-r", "-H", "-p", root_dataset,] - ).on_side(side = side, config = config).run(returncode=True) + output, errors, returncode, exception = ( + Command.from_list(["zfs", "get", "all", "-r", "-H", "-p", root_dataset,]) + .on_side(side=side, config=config) + .run(returncode=True) + ) if returncode[0] != 0 and "dataset does not exist" in errors[0]: return cls(datasets=[], side=side, config=config,) From 2b74be27f27711a557f257277d8d9e0ffe1364bb Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 15:26:19 +0200 Subject: [PATCH 066/155] splitting lists --- src/abgleich/core/lib.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/abgleich/core/lib.py b/src/abgleich/core/lib.py index d34eb70..33a54fd 100644 --- a/src/abgleich/core/lib.py +++ b/src/abgleich/core/lib.py @@ -28,8 +28,9 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +import itertools import re -from typing import Union +from typing import Any, List, Union from typeguard import typechecked @@ -81,6 +82,16 @@ def root(zpool: str, prefix: Union[str, None]) -> str: return join(zpool, prefix) +@typechecked +def split_list(data: List, delimiter: Any) -> List[List]: + + return [ + list(sub_list) + for is_delimiter, sub_list in itertools.groupby(data, lambda item: item == delimiter) + if not is_delimiter + ] + + _name_re = re.compile("^[A-Za-z0-9_]+$") From bcb6952a32b461f17d6c2da59b62f822c1637a79 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 15:26:37 +0200 Subject: [PATCH 067/155] allow pipes in command string import --- src/abgleich/core/command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 5716152..84b88cb 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -35,6 +35,7 @@ from typeguard import typechecked from .abc import CommandABC, ConfigABC +from .lib import split_list # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -145,7 +146,7 @@ def cmd(self) -> List[List[str]]: @classmethod def from_str(cls, cmd: str) -> CommandABC: - return cls.from_list(shlex.split(cmd)) + return cls(split_list(shlex.split(cmd), '|')) @classmethod def from_list(cls, cmd: List[str]) -> CommandABC: From 3a82afad2ca17d14350e5537bf79e48b78e27b3a Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 15:28:42 +0200 Subject: [PATCH 068/155] new field for pre/post processing commands for zfs transfers --- src/abgleich/core/configspec.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/abgleich/core/configspec.py b/src/abgleich/core/configspec.py index 223174b..7f933e4 100644 --- a/src/abgleich/core/configspec.py +++ b/src/abgleich/core/configspec.py @@ -101,6 +101,11 @@ validate=lambda v: isinstance(v, int) and v > 0, default=22, ), + ConfigField( + name=f"{_side}/processing", + validate=lambda v: isinstance(v, str), + default="", + ), ] ) From e6bc702284d05b978cc54d24835e44ed843600d6 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 15:32:23 +0200 Subject: [PATCH 069/155] move split list over to command --- src/abgleich/core/lib.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/abgleich/core/lib.py b/src/abgleich/core/lib.py index 33a54fd..d34eb70 100644 --- a/src/abgleich/core/lib.py +++ b/src/abgleich/core/lib.py @@ -28,9 +28,8 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -import itertools import re -from typing import Any, List, Union +from typing import Union from typeguard import typechecked @@ -82,16 +81,6 @@ def root(zpool: str, prefix: Union[str, None]) -> str: return join(zpool, prefix) -@typechecked -def split_list(data: List, delimiter: Any) -> List[List]: - - return [ - list(sub_list) - for is_delimiter, sub_list in itertools.groupby(data, lambda item: item == delimiter) - if not is_delimiter - ] - - _name_re = re.compile("^[A-Za-z0-9_]+$") From 646cf9714c151ba0b0ce4891a8b6d32436d8663b Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 15:33:01 +0200 Subject: [PATCH 070/155] move split list over to command (2) --- src/abgleich/core/command.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 84b88cb..6e59f7f 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -28,6 +28,7 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +import itertools from subprocess import Popen, PIPE from typing import List, Tuple, Union import shlex @@ -35,7 +36,6 @@ from typeguard import typechecked from .abc import CommandABC, ConfigABC -from .lib import split_list # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -79,6 +79,15 @@ def _com_to_str(com: Union[str, bytes, None]) -> str: return com + @staticmethod + def _split_list(data: List, delimiter: str) -> List[List]: + + return [ + list(sub_list) + for is_delimiter, sub_list in itertools.groupby(data, lambda item: item == delimiter) + if not is_delimiter + ] + def run( self, returncode: bool = False ) -> Union[ @@ -146,7 +155,7 @@ def cmd(self) -> List[List[str]]: @classmethod def from_str(cls, cmd: str) -> CommandABC: - return cls(split_list(shlex.split(cmd), '|')) + return cls(cls._split_list(shlex.split(cmd), '|')) @classmethod def from_list(cls, cmd: List[str]) -> CommandABC: From 961a8b23977a1e99f1da9536ac016823e7993233 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 15:33:59 +0200 Subject: [PATCH 071/155] pre/post processing commands for zfs transfers, implementing #23 --- src/abgleich/core/snapshot.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 4ea938a..6571d24 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -105,7 +105,7 @@ def get_backup_transaction( ancestor = self.ancestor - command = Command.from_list( + send = Command.from_list( ["zfs", "send", "-c", f"{source_dataset:s}@{self.name:s}",] if ancestor is None else [ @@ -116,12 +116,18 @@ def get_backup_transaction( f"{source_dataset:s}@{ancestor.name:s}", f"{source_dataset:s}@{self.name:s}", ] - ).on_side(side="source", config=self._config) | Command.from_list( + ) + receive = Command.from_list( ["zfs", "receive", f"{target_dataset:s}"] - ).on_side( - side="target", config=self._config ) + if self._config['source/processing'].set: + send = send | Command.from_str(self._config['source/processing'].value) + if self._config['target/processing'].set: + receive = Command.from_str(self._config['target/processing'].value) | receive + + command = send.on_side(side="source", config=self._config) | receive.on_side(side="target", config=self._config) + return Transaction( meta=TransactionMeta( **{ From 5aa53a4c98ce0a93654d6f9fcc52624014314336 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 15:40:10 +0200 Subject: [PATCH 072/155] log changes --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 3725b7d..4d7b651 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ ## 0.0.8 (2020-XX-XX) - FEATURE: `ssh`-port on source and target becomes configurable, see #22. +- FEATURE: New configuration fields for `source` and `target` each: `processing`. They can carry shell commands for pre- and post-processing of data before and after it is transferred via ssh. This enables the use of e.g. `lzma` or `bzip2` as a custom transfer compression beyond the compression capabilities of `ssh` itself. See #23. - FEATURE: Configuration module contains default values for parameters, making it much easier to write lightweight configuration files, see #28. The configuration parser now also provides much more useful output. - FEATURE: Significantly more flexible shell command wrapper and, as a result, cleaned up transaction handling. - FIX: Many cleanups in code base, enabling future developments. From 784eb220ee92a94a16a2a10f73412e0507ccf887 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 15:45:21 +0200 Subject: [PATCH 073/155] documented processing option --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b59b4f8..3491bff 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ ssh: cipher: aes256-gcm@openssh.com ``` -The prefix can be empty on either side. If a `host` is set to `localhost`, the `user` field can be left empty. Both source and target can be remote hosts or localhost at the same time. `include_root` indicates whether `{zpool}{/{prefix}}` should be included in all operations. `keep_snapshots` is an integer and must be greater or equal to `1`. It specifies the number of snapshots that are kept per dataset on the source side when a cleanup operation is triggered. `suffix` contains the name suffix for new snapshots. Setting `always_changed` to `yes` causes `abgleich` to beliefe that all datasets have always changed since the last snapshot, completely ignoring what ZFS actually reports. No diff will be produced & checked for values of `written` lower than `written_threshold`. Checking diffs can be completely deactivated by setting `check_diff` to `no`. `digits` specifies how many digits are used for a decimal number describing the n-th snapshot per dataset per day as part of the name of new snapshots. `ignore` lists stuff underneath the `prefix` which will be ignored by this tool, i.e. no snapshots, backups or cleanups. `ssh` allows to fine-tune the speed of backups. In fast local networks, it is best to set `compression` to `no` because the compression is usually slowing down the transfer. However, for low-bandwidth transmissions, it makes sense to set it to `yes`. For significantly better speed in fast local networks, make sure that both the source and the target system support a common cipher, which is accelerated by [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) on both ends. The `ssh` port can be specified per side, i.e. `source` and/or `target`. +The prefix can be empty on either side. If a `host` is set to `localhost`, the `user` field can be left empty. Both source and target can be remote hosts or localhost at the same time. `include_root` indicates whether `{zpool}{/{prefix}}` should be included in all operations. `keep_snapshots` is an integer and must be greater or equal to `1`. It specifies the number of snapshots that are kept per dataset on the source side when a cleanup operation is triggered. `suffix` contains the name suffix for new snapshots. Setting `always_changed` to `yes` causes `abgleich` to beliefe that all datasets have always changed since the last snapshot, completely ignoring what ZFS actually reports. No diff will be produced & checked for values of `written` lower than `written_threshold`. Checking diffs can be completely deactivated by setting `check_diff` to `no`. `digits` specifies how many digits are used for a decimal number describing the n-th snapshot per dataset per day as part of the name of new snapshots. `ignore` lists stuff underneath the `prefix` which will be ignored by this tool, i.e. no snapshots, backups or cleanups. `ssh` allows to fine-tune the speed of backups. In fast local networks, it is best to set `compression` to `no` because the compression is usually slowing down the transfer. However, for low-bandwidth transmissions, it makes sense to set it to `yes`. For significantly better speed in fast local networks, make sure that both the source and the target system support a common cipher, which is accelerated by [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) on both ends. The `ssh` port can be specified per side via the `port` configuration option, i.e. for `source` and/or `target`. Custom pre- and post-processing can be applied after `send` and before `receive` per side via shell commands specified in the `processing` configuration option. This can be useful for a custom transfer compression based on e.g. `lzma` or `bzip2`. ## USAGE From d59437ec281acce7a48f1c7452f5e949cb87355f Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 15:53:10 +0200 Subject: [PATCH 074/155] black (update) --- setup.py | 30 ++++++++++-- src/abgleich/core/command.py | 13 ++++-- src/abgleich/core/comparison.py | 30 ++++++++---- src/abgleich/core/configfield.py | 9 +++- src/abgleich/core/configspec.py | 30 +++++++++--- src/abgleich/core/dataset.py | 13 ++++-- src/abgleich/core/property.py | 11 ++++- src/abgleich/core/snapshot.py | 29 ++++++++---- src/abgleich/core/transaction.py | 13 ++++-- src/abgleich/core/zpool.py | 78 ++++++++++++++++++++++++++------ 10 files changed, 198 insertions(+), 58 deletions(-) diff --git a/setup.py b/setup.py index c56a060..b2e77d2 100644 --- a/setup.py +++ b/setup.py @@ -73,8 +73,16 @@ def get_version(code): # Requirements extras_require = { - "dev": ["black", "python-language-server[all]", "setuptools", "twine", "wheel",], - "gui": ["pyqt5",], + "dev": [ + "black", + "python-language-server[all]", + "setuptools", + "twine", + "wheel", + ], + "gui": [ + "pyqt5", + ], } extras_require["all"] = list( {rq for target in extras_require.keys() for rq in extras_require[target]} @@ -95,15 +103,27 @@ def get_version(code): download_url="https://github.com/pleiszenburg/abgleich/archive/v%s.tar.gz" % __version__, license="LGPLv2", - keywords=["zfs", "ssh",], + keywords=[ + "zfs", + "ssh", + ], scripts=[], include_package_data=True, python_requires=">=3.{MINOR:d}".format(MINOR=python_minor_min), setup_requires=[], - install_requires=["click", "tabulate", "pyyaml", "typeguard",], + install_requires=[ + "click", + "tabulate", + "pyyaml", + "typeguard", + ], extras_require=extras_require, zip_safe=False, - entry_points={"console_scripts": ["abgleich = abgleich.cli:cli",],}, + entry_points={ + "console_scripts": [ + "abgleich = abgleich.cli:cli", + ], + }, classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console", diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 6e59f7f..662ab29 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -84,7 +84,9 @@ def _split_list(data: List, delimiter: str) -> List[List]: return [ list(sub_list) - for is_delimiter, sub_list in itertools.groupby(data, lambda item: item == delimiter) + for is_delimiter, sub_list in itertools.groupby( + data, lambda item: item == delimiter + ) if not is_delimiter ] @@ -99,7 +101,12 @@ def run( for index, fragment in enumerate(self._cmd): # create & connect processes stdin = None if index == 0 else procs[-1].stdout # output of last process - proc = Popen(fragment, stdout=PIPE, stderr=PIPE, stdin=stdin,) + proc = Popen( + fragment, + stdout=PIPE, + stderr=PIPE, + stdin=stdin, + ) procs.append(proc) output, errors, status = [], [], [] @@ -155,7 +162,7 @@ def cmd(self) -> List[List[str]]: @classmethod def from_str(cls, cmd: str) -> CommandABC: - return cls(cls._split_list(shlex.split(cmd), '|')) + return cls(cls._split_list(shlex.split(cmd), "|")) @classmethod def from_list(cls, cmd: List[str]) -> CommandABC: diff --git a/src/abgleich/core/comparison.py b/src/abgleich/core/comparison.py index 3ff34fd..c6b6845 100644 --- a/src/abgleich/core/comparison.py +++ b/src/abgleich/core/comparison.py @@ -40,16 +40,22 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ComparisonParentTypes = Union[ - ZpoolABC, DatasetABC, None, + ZpoolABC, + DatasetABC, + None, ] ComparisonMergeTypes = Union[ - Generator[DatasetABC, None, None], Generator[SnapshotABC, None, None], + Generator[DatasetABC, None, None], + Generator[SnapshotABC, None, None], ] ComparisonItemType = Union[ - DatasetABC, SnapshotABC, None, + DatasetABC, + SnapshotABC, + None, ] ComparisonStrictItemType = Union[ - DatasetABC, SnapshotABC, + DatasetABC, + SnapshotABC, ] # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -129,7 +135,9 @@ def merged(self) -> Generator[ComparisonItemABC, None, None]: @classmethod def _head( - cls, source: List[ComparisonItemType], target: List[ComparisonItemType], + cls, + source: List[ComparisonItemType], + target: List[ComparisonItemType], ) -> List[ComparisonItemType]: """ Returns new elements from source. @@ -177,7 +185,9 @@ def _head( @classmethod def _overlap_tail( - cls, source: List[ComparisonItemType], target: List[ComparisonItemType], + cls, + source: List[ComparisonItemType], + target: List[ComparisonItemType], ) -> List[ComparisonItemType]: """ Overlap must include first element of source. @@ -267,7 +277,9 @@ def _merge_datasets( @classmethod def from_zpools( - cls, zpool_a: Union[ZpoolABC, None], zpool_b: Union[ZpoolABC, None], + cls, + zpool_a: Union[ZpoolABC, None], + zpool_b: Union[ZpoolABC, None], ) -> ComparisonABC: assert zpool_a is not None or zpool_b is not None @@ -363,7 +375,9 @@ def _merge_snapshots( @classmethod def from_datasets( - cls, dataset_a: Union[DatasetABC, None], dataset_b: Union[DatasetABC, None], + cls, + dataset_a: Union[DatasetABC, None], + dataset_b: Union[DatasetABC, None], ) -> ComparisonABC: assert dataset_a is not None or dataset_b is not None diff --git a/src/abgleich/core/configfield.py b/src/abgleich/core/configfield.py index 75aef26..c0a5287 100644 --- a/src/abgleich/core/configfield.py +++ b/src/abgleich/core/configfield.py @@ -52,7 +52,10 @@ class ConfigField(ConfigFieldABC): """ def __init__( - self, name: str, validate: Callable, default: ConfigValueTypes = None, + self, + name: str, + validate: Callable, + default: ConfigValueTypes = None, ): self._name = name @@ -78,7 +81,9 @@ def __repr__(self) -> str: def copy(self) -> ConfigFieldABC: return type(self)( - name=self._name, default=self._default, validate=self._validate, + name=self._name, + default=self._default, + validate=self._validate, ) @property diff --git a/src/abgleich/core/configspec.py b/src/abgleich/core/configspec.py index 7f933e4..6a3d301 100644 --- a/src/abgleich/core/configspec.py +++ b/src/abgleich/core/configspec.py @@ -47,10 +47,14 @@ default="", ), ConfigField( - name="digits", validate=lambda v: isinstance(v, int) and v >= 1, default=2, + name="digits", + validate=lambda v: isinstance(v, int) and v >= 1, + default=2, ), ConfigField( - name="always_changed", validate=lambda v: isinstance(v, bool), default=False, + name="always_changed", + validate=lambda v: isinstance(v, bool), + default=False, ), ConfigField( name="written_threshold", @@ -58,7 +62,9 @@ default=1024 ** 2, ), ConfigField( - name="check_diff", validate=lambda v: isinstance(v, bool), default=True, + name="check_diff", + validate=lambda v: isinstance(v, bool), + default=True, ), ConfigField( name="ignore", @@ -67,12 +73,20 @@ default=list(), ), ConfigField( - name="include_root", validate=lambda v: isinstance(v, bool), default=True, + name="include_root", + validate=lambda v: isinstance(v, bool), + default=True, + ), + ConfigField( + name="ssh/compression", + validate=lambda v: isinstance(v, bool), + default=False, ), ConfigField( - name="ssh/compression", validate=lambda v: isinstance(v, bool), default=False, + name="ssh/cipher", + validate=lambda v: isinstance(v, str), + default="", ), - ConfigField(name="ssh/cipher", validate=lambda v: isinstance(v, str), default="",), ] for _side in ("source", "target"): @@ -94,7 +108,9 @@ default="localhost", ), ConfigField( - name=f"{_side}/user", validate=lambda v: isinstance(v, str), default="", + name=f"{_side}/user", + validate=lambda v: isinstance(v, str), + default="", ), ConfigField( name=f"{_side}/port", diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index 34fdfda..ac4eb3a 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -93,12 +93,15 @@ def __getitem__(self, key: Union[str, int, slice]) -> PropertyABC: return self._snapshots[key] def get( - self, key: Union[str, int, slice], default: Union[None, PropertyABC] = None, + self, + key: Union[str, int, slice], + default: Union[None, PropertyABC] = None, ) -> Union[None, PropertyABC]: if isinstance(key, str): return self._properties.get( - key, Property(key, None, None) if default is None else default, + key, + Property(key, None, None) if default is None else default, ) assert isinstance(key, int) or isinstance(key, slice) @@ -226,7 +229,11 @@ def from_entities( snapshots.extend( ( Snapshot.from_entity( - snapshot_name, entities[snapshot_name], snapshots, side, config, + snapshot_name, + entities[snapshot_name], + snapshots, + side, + config, ) for snapshot_name in entities.keys() ) diff --git a/src/abgleich/core/property.py b/src/abgleich/core/property.py index 958cd46..c14495c 100644 --- a/src/abgleich/core/property.py +++ b/src/abgleich/core/property.py @@ -52,7 +52,10 @@ class Property(PropertyABC): """ def __init__( - self, name: str, value: PropertyTypes, src: PropertyTypes, + self, + name: str, + value: PropertyTypes, + src: PropertyTypes, ): self._name = name @@ -92,4 +95,8 @@ def _convert(cls, value: str) -> PropertyTypes: @classmethod def from_params(cls, name, value, src) -> PropertyABC: - return cls(name=name, value=cls._convert(value), src=cls._convert(src),) + return cls( + name=name, + value=cls._convert(value), + src=cls._convert(src), + ) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 6571d24..db61afa 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -98,7 +98,9 @@ def get_cleanup_transaction(self) -> TransactionABC: ) def get_backup_transaction( - self, source_dataset: str, target_dataset: str, + self, + source_dataset: str, + target_dataset: str, ) -> TransactionABC: assert self._side == "source" @@ -106,7 +108,12 @@ def get_backup_transaction( ancestor = self.ancestor send = Command.from_list( - ["zfs", "send", "-c", f"{source_dataset:s}@{self.name:s}",] + [ + "zfs", + "send", + "-c", + f"{source_dataset:s}@{self.name:s}", + ] if ancestor is None else [ "zfs", @@ -117,16 +124,18 @@ def get_backup_transaction( f"{source_dataset:s}@{self.name:s}", ] ) - receive = Command.from_list( - ["zfs", "receive", f"{target_dataset:s}"] - ) + receive = Command.from_list(["zfs", "receive", f"{target_dataset:s}"]) - if self._config['source/processing'].set: - send = send | Command.from_str(self._config['source/processing'].value) - if self._config['target/processing'].set: - receive = Command.from_str(self._config['target/processing'].value) | receive + if self._config["source/processing"].set: + send = send | Command.from_str(self._config["source/processing"].value) + if self._config["target/processing"].set: + receive = ( + Command.from_str(self._config["target/processing"].value) | receive + ) - command = send.on_side(side="source", config=self._config) | receive.on_side(side="target", config=self._config) + command = send.on_side(side="source", config=self._config) | receive.on_side( + side="target", config=self._config + ) return Transaction( meta=TransactionMeta( diff --git a/src/abgleich/core/transaction.py b/src/abgleich/core/transaction.py index e145cf3..9705044 100644 --- a/src/abgleich/core/transaction.py +++ b/src/abgleich/core/transaction.py @@ -49,7 +49,9 @@ class Transaction(TransactionABC): """ def __init__( - self, meta: TransactionMetaABC, command: CommandABC, + self, + meta: TransactionMetaABC, + command: CommandABC, ): self._meta, self._command = meta, command @@ -147,7 +149,9 @@ def keys(self) -> Generator[str, None, None]: TransactionIterableTypes = Union[ - Generator[TransactionABC, None, None], List[TransactionABC], Tuple[TransactionABC], + Generator[TransactionABC, None, None], + List[TransactionABC], + Tuple[TransactionABC], ] @@ -250,7 +254,10 @@ def print_table(self): print( tabulate( - table, headers=table_columns, tablefmt="github", colalign=colalign, + table, + headers=table_columns, + tablefmt="github", + colalign=colalign, ) ) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index b470f02..f3eee57 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -64,7 +64,10 @@ class Zpool(ZpoolABC): """ def __init__( - self, datasets: List[DatasetABC], side: str, config: ConfigABC, + self, + datasets: List[DatasetABC], + side: str, + config: ConfigABC, ): self._datasets = datasets @@ -92,7 +95,10 @@ def root(self) -> str: return self._root - def get_cleanup_transactions(self, other: ZpoolABC,) -> TransactionListABC: + def get_cleanup_transactions( + self, + other: ZpoolABC, + ) -> TransactionListABC: assert self.side == "source" assert other.side == "target" @@ -110,9 +116,13 @@ def get_cleanup_transactions(self, other: ZpoolABC,) -> TransactionListABC: return transactions def generate_cleanup_transactions( - self, other: ZpoolABC, + self, + other: ZpoolABC, ) -> Generator[ - Tuple[int, Union[None, Union[None, Generator[TransactionABC, None, None]]],], + Tuple[ + int, + Union[None, Union[None, Generator[TransactionABC, None, None]]], + ], None, None, ]: @@ -128,7 +138,8 @@ def generate_cleanup_transactions( yield index, self._get_cleanup_from_datasetitem(dataset_item) def _get_cleanup_from_datasetitem( - self, dataset_item: ComparisonItemABC, + self, + dataset_item: ComparisonItemABC, ) -> Union[None, Generator[TransactionABC, None, None]]: if dataset_item.get_item().subname in self._config["ignore"]: @@ -141,7 +152,10 @@ def _get_cleanup_from_datasetitem( return (snapshot.get_cleanup_transaction() for snapshot in snapshots) - def get_backup_transactions(self, other: ZpoolABC,) -> TransactionListABC: + def get_backup_transactions( + self, + other: ZpoolABC, + ) -> TransactionListABC: assert self.side == "source" assert other.side == "target" @@ -160,9 +174,13 @@ def get_backup_transactions(self, other: ZpoolABC,) -> TransactionListABC: return transactions def generate_backup_transactions( - self, other: ZpoolABC, + self, + other: ZpoolABC, ) -> Generator[ - Tuple[int, Union[None, Union[None, Generator[TransactionABC, None, None]]],], + Tuple[ + int, + Union[None, Union[None, Generator[TransactionABC, None, None]]], + ], None, None, ]: @@ -180,7 +198,9 @@ def generate_backup_transactions( ) def _get_backup_transactions_from_datasetitem( - self, other: ZpoolABC, dataset_item: ComparisonItemABC, + self, + other: ZpoolABC, + dataset_item: ComparisonItemABC, ) -> Union[None, Generator[TransactionABC, None, None]]: if dataset_item.get_item().subname in self._config["ignore"]: @@ -211,7 +231,10 @@ def _get_backup_transactions_from_datasetitem( ) return ( - snapshot.get_backup_transaction(source_dataset, target_dataset,) + snapshot.get_backup_transaction( + source_dataset, + target_dataset, + ) for snapshot in snapshots ) @@ -337,7 +360,10 @@ def _comparison_table_row(item: ComparisonItemABC) -> List[str]: ] @staticmethod - def available(side: str, config: ConfigABC,) -> int: + def available( + side: str, + config: ConfigABC, + ) -> int: output, _ = ( Command.from_list( @@ -357,19 +383,37 @@ def available(side: str, config: ConfigABC,) -> int: return Property.from_params(*output[0].strip().split("\t")[1:]).value @classmethod - def from_config(cls, side: str, config: ConfigABC,) -> ZpoolABC: + def from_config( + cls, + side: str, + config: ConfigABC, + ) -> ZpoolABC: side_config = config.group(side) root_dataset = root(side_config["zpool"], side_config["prefix"]) output, errors, returncode, exception = ( - Command.from_list(["zfs", "get", "all", "-r", "-H", "-p", root_dataset,]) + Command.from_list( + [ + "zfs", + "get", + "all", + "-r", + "-H", + "-p", + root_dataset, + ] + ) .on_side(side=side, config=config) .run(returncode=True) ) if returncode[0] != 0 and "dataset does not exist" in errors[0]: - return cls(datasets=[], side=side, config=config,) + return cls( + datasets=[], + side=side, + config=config, + ) if returncode[0] != 0: raise exception @@ -405,4 +449,8 @@ def from_config(cls, side: str, config: ConfigABC,) -> ZpoolABC: ] datasets.sort(key=lambda dataset: dataset.name) - return cls(datasets=datasets, side=side, config=config,) + return cls( + datasets=datasets, + side=side, + config=config, + ) From d7cb8fe1b76171acb302e63f5528b9526a8b1a81 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 16:44:22 +0200 Subject: [PATCH 075/155] prepare target side cleanup --- src/abgleich/cli/cleanup.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/abgleich/cli/cleanup.py b/src/abgleich/cli/cleanup.py index 68803fd..24e7408 100644 --- a/src/abgleich/cli/cleanup.py +++ b/src/abgleich/cli/cleanup.py @@ -47,20 +47,29 @@ @click.command(short_help="cleanup older snapshots") @click.argument("configfile", type=click.File("r", encoding="utf-8")) -def cleanup(configfile): +@click.argument("side", default="source", type=str) +def cleanup(configfile, side): config = Config.from_fd(configfile) - for side in ("source", "target"): + assert side in ('source', 'target') + + cleanup_side = side + control_side = 'target' if cleanup_side == 'source' else 'source' + + if cleanup_side == 'target': + click.confirm(t("DANGER ZONE: You are about to clean the target. Do you want to continue?"), abort=True) + + for side in (cleanup_side, control_side): if not is_host_up(side, config): print(f'{t("host is not up"):s}: {side:s}') sys.exit(1) - source_zpool = Zpool.from_config("source", config=config) - target_zpool = Zpool.from_config("target", config=config) - available_before = Zpool.available("source", config=config) + cleanup_zpool = Zpool.from_config(cleanup_side, config=config) + control_zpool = Zpool.from_config(control_side, config=config) + available_before = Zpool.available(cleanup_side, config=config) - transactions = source_zpool.get_cleanup_transactions(target_zpool) + transactions = cleanup_zpool.get_cleanup_transactions(control_zpool) if len(transactions) == 0: print(t("nothing to do")) @@ -74,7 +83,8 @@ def cleanup(configfile): WAIT = 10 print(f"waiting {WAIT:d} seconds ...") time.sleep(WAIT) - available_after = Zpool.available("source", config=config) - print( - f"{humanize_size(available_after, add_color = True):s} available, {humanize_size(available_after - available_before, add_color = True):s} freed" - ) + available_after = Zpool.available(cleanup_side, config=config) + print(( + f"{humanize_size(available_after, add_color = True):s} available, " + f"{humanize_size(available_after - available_before, add_color = True):s} freed" + )) From a09ca10ce0880033ff2e4e58bf239c1a6267e94f Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 16:44:57 +0200 Subject: [PATCH 076/155] keep_snapshots can be specified per side --- src/abgleich/core/configspec.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/abgleich/core/configspec.py b/src/abgleich/core/configspec.py index 6a3d301..2d3092d 100644 --- a/src/abgleich/core/configspec.py +++ b/src/abgleich/core/configspec.py @@ -122,6 +122,11 @@ validate=lambda v: isinstance(v, str), default="", ), + ConfigField( + name=f"{_side}/keep_snapshots", + validate=lambda v: isinstance(v, int) and v >= 1, + default=1, + ), ] ) From 7ae2c10721330cc2f60f26bd43a2d22f684905a7 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 16:51:56 +0200 Subject: [PATCH 077/155] black --- src/abgleich/cli/cleanup.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/abgleich/cli/cleanup.py b/src/abgleich/cli/cleanup.py index 24e7408..c7a0a4b 100644 --- a/src/abgleich/cli/cleanup.py +++ b/src/abgleich/cli/cleanup.py @@ -52,13 +52,18 @@ def cleanup(configfile, side): config = Config.from_fd(configfile) - assert side in ('source', 'target') + assert side in ("source", "target") cleanup_side = side - control_side = 'target' if cleanup_side == 'source' else 'source' + control_side = "target" if cleanup_side == "source" else "source" - if cleanup_side == 'target': - click.confirm(t("DANGER ZONE: You are about to clean the target. Do you want to continue?"), abort=True) + if cleanup_side == "target": + click.confirm( + t( + "DANGER ZONE: You are about to clean the target. Do you want to continue?" + ), + abort=True, + ) for side in (cleanup_side, control_side): if not is_host_up(side, config): @@ -84,7 +89,9 @@ def cleanup(configfile, side): print(f"waiting {WAIT:d} seconds ...") time.sleep(WAIT) available_after = Zpool.available(cleanup_side, config=config) - print(( - f"{humanize_size(available_after, add_color = True):s} available, " - f"{humanize_size(available_after - available_before, add_color = True):s} freed" - )) + print( + ( + f"{humanize_size(available_after, add_color = True):s} available, " + f"{humanize_size(available_after - available_before, add_color = True):s} freed" + ) + ) From 64b334c45684ae0070fe473027885e42d558e3c0 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 16:58:29 +0200 Subject: [PATCH 078/155] cleanup tasks handle per-side keep_snapshots; added todos for target cleanup --- src/abgleich/core/zpool.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index f3eee57..050586f 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -127,19 +127,27 @@ def generate_cleanup_transactions( None, ]: - assert self.side == "source" - assert other.side == "target" + assert self.side != other.side + assert self.side in ("source", "target") + assert other.side in ("source", "target") + + keep_snapshots = self._config["keep_snapshots"].value + if keep_snapshots < self._config[f"{self.side:s}/keep_snapshots"].value: + keep_snapshots = self._config[f"{self.side:s}/keep_snapshots"].value zpool_comparison = Comparison.from_zpools(self, other) yield len(zpool_comparison), None for index, dataset_item in enumerate(zpool_comparison.merged): - yield index, self._get_cleanup_from_datasetitem(dataset_item) + yield index, self._get_cleanup_from_datasetitem( + dataset_item, keep_snapshots + ) def _get_cleanup_from_datasetitem( self, dataset_item: ComparisonItemABC, + keep_snapshots: int, ) -> Union[None, Generator[TransactionABC, None, None]]: if dataset_item.get_item().subname in self._config["ignore"]: @@ -147,8 +155,14 @@ def _get_cleanup_from_datasetitem( if dataset_item.a is None or dataset_item.b is None: return - dataset_comparison = Comparison.from_datasets(dataset_item.a, dataset_item.b) - snapshots = dataset_comparison.a_overlap_tail[: -self._config["keep_snapshots"]] + dataset_comparison = Comparison.from_datasets( + dataset_item.a, dataset_item.b + ) # TODO namespace + + # if self.side == 'source': + snapshots = dataset_comparison.a_overlap_tail[:-keep_snapshots] + # else: # target + # TODO target tail ... return (snapshot.get_cleanup_transaction() for snapshot in snapshots) From eaf50d40213981e3ef258864f8101c09f2a47b46 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 17:18:41 +0200 Subject: [PATCH 079/155] zpool todos --- src/abgleich/core/zpool.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 050586f..971c3a0 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -159,10 +159,10 @@ def _get_cleanup_from_datasetitem( dataset_item.a, dataset_item.b ) # TODO namespace - # if self.side == 'source': - snapshots = dataset_comparison.a_overlap_tail[:-keep_snapshots] - # else: # target - # TODO target tail ... + if self.side == 'source': + snapshots = dataset_comparison.a_overlap_tail[:-keep_snapshots] + else: # target + snapshots = [] # TODO return (snapshot.get_cleanup_transaction() for snapshot in snapshots) From 37cf1be9faade46383982b8ec0de12dd8a584869 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 17:26:23 +0200 Subject: [PATCH 080/155] new module for comparison item --- src/abgleich/core/comparisonitem.py | 89 +++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/abgleich/core/comparisonitem.py diff --git a/src/abgleich/core/comparisonitem.py b/src/abgleich/core/comparisonitem.py new file mode 100644 index 0000000..7c637e5 --- /dev/null +++ b/src/abgleich/core/comparisonitem.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +""" + +ABGLEICH +zfs sync tool +https://github.com/pleiszenburg/abgleich + + src/abgleich/core/comparisonitem.py: ZFS comparison item + + Copyright (C) 2019-2020 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/abgleich/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from typing import Union + +from typeguard import typechecked + +from .abc import ComparisonItemABC, DatasetABC, SnapshotABC + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TYPING +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +ComparisonItemType = Union[ + DatasetABC, + SnapshotABC, + None, +] +ComparisonStrictItemType = Union[ + DatasetABC, + SnapshotABC, +] + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +@typechecked +class ComparisonItem(ComparisonItemABC): + """ + Immutable. + """ + + def __init__(self, a: ComparisonItemType, b: ComparisonItemType): + + assert a is not None or b is not None + if a is not None and b is not None: + assert type(a) == type(b) + + self._a, self._b = a, b + + def get_item(self) -> ComparisonStrictItemType: + + if self._a is not None: + return self._a + return self._b + + @property + def complete(self) -> bool: + + return self._a is not None and self._b is not None + + @property + def a(self) -> ComparisonItemType: + + return self._a + + @property + def b(self) -> ComparisonItemType: + + return self._b From 4b81d7a29da617e1e3240c3a6937973df1c0d164 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 17:26:36 +0200 Subject: [PATCH 081/155] removed comparison item code --- src/abgleich/core/comparison.py | 42 +-------------------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/src/abgleich/core/comparison.py b/src/abgleich/core/comparison.py index c6b6845..3fa5395 100644 --- a/src/abgleich/core/comparison.py +++ b/src/abgleich/core/comparison.py @@ -34,6 +34,7 @@ from typeguard import typechecked from .abc import ComparisonABC, ComparisonItemABC, DatasetABC, SnapshotABC, ZpoolABC +from .comparisonitem import ComparisonItem, ComparisonItemType, ComparisonStrictItemType # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # TYPING @@ -48,15 +49,6 @@ Generator[DatasetABC, None, None], Generator[SnapshotABC, None, None], ] -ComparisonItemType = Union[ - DatasetABC, - SnapshotABC, - None, -] -ComparisonStrictItemType = Union[ - DatasetABC, - SnapshotABC, -] # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -400,35 +392,3 @@ def from_datasets( b=dataset_b, merged=cls._merge_snapshots(dataset_a.snapshots, dataset_b.snapshots), ) - - -@typechecked -class ComparisonItem(ComparisonItemABC): - def __init__(self, a: ComparisonItemType, b: ComparisonItemType): - - assert a is not None or b is not None - if a is not None and b is not None: - assert type(a) == type(b) - - self._a, self._b = a, b - - def get_item(self) -> ComparisonStrictItemType: - - if self._a is not None: - return self._a - return self._b - - @property - def complete(self) -> bool: - - return self._a is not None and self._b is not None - - @property - def a(self) -> ComparisonItemType: - - return self._a - - @property - def b(self) -> ComparisonItemType: - - return self._b From a18cfe8d09d955876bf36cb7fd8ac46781b9691d Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 17:36:36 +0200 Subject: [PATCH 082/155] move and cleaned single item comparison --- src/abgleich/core/comparison.py | 24 ++++-------------------- src/abgleich/core/comparisonitem.py | 20 +++++++++++++++++++- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/abgleich/core/comparison.py b/src/abgleich/core/comparison.py index 3fa5395..3f9b3e5 100644 --- a/src/abgleich/core/comparison.py +++ b/src/abgleich/core/comparison.py @@ -45,10 +45,6 @@ DatasetABC, None, ] -ComparisonMergeTypes = Union[ - Generator[DatasetABC, None, None], - Generator[SnapshotABC, None, None], -] # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -237,18 +233,6 @@ def _left_strip_none( return list(itertools.dropwhile(lambda element: element is None, elements)) - @staticmethod - def _single_items( - items_a: Union[ComparisonMergeTypes, None], - items_b: Union[ComparisonMergeTypes, None], - ) -> List[ComparisonItemABC]: - - assert items_a is not None or items_b is not None - - if items_a is None: - return [ComparisonItem(None, item) for item in items_b] - return [ComparisonItem(item, None) for item in items_a] - @staticmethod def _merge_datasets( items_a: Generator[DatasetABC, None, None], @@ -276,11 +260,11 @@ def from_zpools( assert zpool_a is not None or zpool_b is not None - if zpool_a is None or zpool_b is None: + if (zpool_a is None) ^ (zpool_b is None): return cls( a=zpool_a, b=zpool_b, - merged=cls._single_items( + merged=ComparisonItem.list_from_singles( getattr(zpool_a, "datasets", None), getattr(zpool_b, "datasets", None), ), @@ -374,11 +358,11 @@ def from_datasets( assert dataset_a is not None or dataset_b is not None - if dataset_a is None or dataset_b is None: + if (dataset_a is None) ^ (dataset_b is None): return cls( a=dataset_a, b=dataset_b, - merged=cls._single_items( + merged=ComparisonItem.list_from_singles( getattr(dataset_a, "snapshots", None), getattr(dataset_b, "snapshots", None), ), diff --git a/src/abgleich/core/comparisonitem.py b/src/abgleich/core/comparisonitem.py index 7c637e5..24adb63 100644 --- a/src/abgleich/core/comparisonitem.py +++ b/src/abgleich/core/comparisonitem.py @@ -29,7 +29,7 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -from typing import Union +from typing import Generator, List, Union from typeguard import typechecked @@ -39,6 +39,11 @@ # TYPING # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +ComparisonGeneratorType = Union[ + Generator[DatasetABC, None, None], + Generator[SnapshotABC, None, None], + None, +] ComparisonItemType = Union[ DatasetABC, SnapshotABC, @@ -87,3 +92,16 @@ def a(self) -> ComparisonItemType: def b(self) -> ComparisonItemType: return self._b + + @classmethod + def list_from_singles( + cls, + items_a: ComparisonGeneratorType, + items_b: ComparisonGeneratorType, + ) -> List[ComparisonItemABC]: + + assert (items_a is not None) ^ (items_b is not None) + + if items_a is None: + return [cls(None, item) for item in items_b] + return [cls(item, None) for item in items_a] From fb7d515f8533e4be8fe47423918026003fecab10 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 18:15:30 +0200 Subject: [PATCH 083/155] cleanup merge --- src/abgleich/core/comparison.py | 90 +++++++++++++++++---------------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/src/abgleich/core/comparison.py b/src/abgleich/core/comparison.py index 3f9b3e5..bb61858 100644 --- a/src/abgleich/core/comparison.py +++ b/src/abgleich/core/comparison.py @@ -233,6 +233,33 @@ def _left_strip_none( return list(itertools.dropwhile(lambda element: element is None, elements)) + @staticmethod + def _test_alternations(items: List[SnapshotABC, None]): + + alternations = 0 + state = False # None + + for item in items: + + new_state = item is not None + + if new_state == state: + continue + + alternations += 1 + state = new_state + + if alternations > 2: + raise ValueError("gap in snapshot series") + + @staticmethod + def _test_names(items: List[ComparisonItemABC]): + + for item in items: + if item.a is not None and item.b is not None: + if item.a.name != item.b.name: + raise ValueError("inconsistent snapshot names") + @staticmethod def _merge_datasets( items_a: Generator[DatasetABC, None, None], @@ -279,14 +306,15 @@ def from_zpools( merged=cls._merge_datasets(zpool_a.datasets, zpool_b.datasets), ) - @staticmethod + @classmethod def _merge_snapshots( + cls, items_a: Generator[SnapshotABC, None, None], items_b: Generator[SnapshotABC, None, None], ) -> List[ComparisonItemABC]: - items_a = list(items_a) - items_b = list(items_b) + items_a, items_b = list(items_a), list(items_b) + names_a = [item.name for item in items_a] names_b = [item.name for item in items_b] @@ -300,52 +328,28 @@ def _merge_snapshots( if len(items_b) == 0: return [ComparisonItem(item, None) for item in items_a] - try: - start_b = names_a.index(names_b[0]) - except ValueError: - start_b = None - try: - start_a = names_b.index(names_a[0]) - except ValueError: - start_a = None + start_b = names_a.index(names_b[0]) if names_b[0] in names_a else None + start_a = names_b.index(names_a[0]) if names_a[0] in names_b else None assert start_a is not None or start_b is not None # overlap - prefix_a = [] if start_a is None else [None for _ in range(start_a)] - prefix_b = [] if start_b is None else [None for _ in range(start_b)] - items_a = prefix_a + items_a - items_b = prefix_b + items_b - suffix_a = ( - [] - if len(items_a) >= len(items_b) - else [None for _ in range(len(items_b) - len(items_a))] - ) - suffix_b = ( - [] - if len(items_b) >= len(items_a) - else [None for _ in range(len(items_a) - len(items_b))] - ) - items_a = items_a + suffix_a - items_b = items_b + suffix_b + if start_a is not None: # fill prefix + items_a = [None for _ in range(start_a)] + items_a + if start_b is not None: # fill prefix + items_b = [None for _ in range(start_b)] + items_b + if len(items_a) < len(items_b): # fill suffix + items_a = items_a + [None for _ in range(len(items_b) - len(items_a))] + if len(items_b) < len(items_a): # fill suffix + items_b = items_b + [None for _ in range(len(items_a) - len(items_b))] assert len(items_a) == len(items_b) - alt_a, alt_b, state_a, state_b = 0, 0, False, False - merged = [] - for item_a, item_b in zip(items_a, items_b): - new_state_a, new_state_b = item_a is not None, item_b is not None - if new_state_a != state_a: - alt_a, state_a = alt_a + 1, new_state_a - if alt_a > 2: - raise ValueError("gap in snapshot series") - if new_state_b != state_b: - alt_b, state_b = alt_b + 1, new_state_b - if alt_b > 2: - raise ValueError("gap in snapshot series") - if state_a and state_b: - if item_a.name != item_b.name: - raise ValueError("inconsistent snapshot names") - merged.append(ComparisonItem(item_a, item_b)) + cls._test_alternations(items_a) + cls._test_alternations(items_b) + + merged = [ComparisonItem(item_a, item_b) for item_a, item_b in zip(items_a, items_b)] + + cls._test_names(merged) return merged From 0f47942e14e9a6f933191703d3983a10468a3c68 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 18:20:20 +0200 Subject: [PATCH 084/155] black --- src/abgleich/core/comparison.py | 4 +++- src/abgleich/core/comparisonitem.py | 1 + src/abgleich/core/zpool.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/abgleich/core/comparison.py b/src/abgleich/core/comparison.py index bb61858..de8d562 100644 --- a/src/abgleich/core/comparison.py +++ b/src/abgleich/core/comparison.py @@ -347,7 +347,9 @@ def _merge_snapshots( cls._test_alternations(items_a) cls._test_alternations(items_b) - merged = [ComparisonItem(item_a, item_b) for item_a, item_b in zip(items_a, items_b)] + merged = [ + ComparisonItem(item_a, item_b) for item_a, item_b in zip(items_a, items_b) + ] cls._test_names(merged) diff --git a/src/abgleich/core/comparisonitem.py b/src/abgleich/core/comparisonitem.py index 24adb63..d7e217c 100644 --- a/src/abgleich/core/comparisonitem.py +++ b/src/abgleich/core/comparisonitem.py @@ -58,6 +58,7 @@ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + @typechecked class ComparisonItem(ComparisonItemABC): """ diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 971c3a0..738b3b4 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -159,7 +159,7 @@ def _get_cleanup_from_datasetitem( dataset_item.a, dataset_item.b ) # TODO namespace - if self.side == 'source': + if self.side == "source": snapshots = dataset_comparison.a_overlap_tail[:-keep_snapshots] else: # target snapshots = [] # TODO From 84f368a00c6fdaf0317c2669b4b23361ee820b4e Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 18:57:53 +0200 Subject: [PATCH 085/155] name clarification --- src/abgleich/core/comparison.py | 10 +++++----- src/abgleich/core/zpool.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/abgleich/core/comparison.py b/src/abgleich/core/comparison.py index de8d562..c5f2115 100644 --- a/src/abgleich/core/comparison.py +++ b/src/abgleich/core/comparison.py @@ -80,9 +80,9 @@ def a(self) -> ComparisonParentTypes: return self._a @property - def a_head(self) -> List[ComparisonStrictItemType]: + def a_disjoint_head(self) -> List[ComparisonStrictItemType]: - return self._head( + return self._disjoint_head( source=[item.a for item in self._merged], target=[item.b for item in self._merged], ) @@ -101,9 +101,9 @@ def b(self) -> ComparisonParentTypes: return self._b @property - def b_head(self) -> List[ComparisonStrictItemType]: + def b_disjoint_head(self) -> List[ComparisonStrictItemType]: - return self._head( + return self._disjoint_head( source=[item.b for item in self._merged], target=[item.a for item in self._merged], ) @@ -122,7 +122,7 @@ def merged(self) -> Generator[ComparisonItemABC, None, None]: return (item for item in self._merged) @classmethod - def _head( + def _disjoint_head( cls, source: List[ComparisonItemType], target: List[ComparisonItemType], diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 738b3b4..b8998c3 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -228,7 +228,7 @@ def _get_backup_transactions_from_datasetitem( dataset_comparison = Comparison.from_datasets( dataset_item.a, dataset_item.b ) - snapshots = dataset_comparison.a_head + snapshots = dataset_comparison.a_disjoint_head if len(snapshots) == 0: return From 4a15b36b270c098decea83badd40708eecbb68a6 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 19:13:17 +0200 Subject: [PATCH 086/155] grammar --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3491bff..f799a33 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ ssh: cipher: aes256-gcm@openssh.com ``` -The prefix can be empty on either side. If a `host` is set to `localhost`, the `user` field can be left empty. Both source and target can be remote hosts or localhost at the same time. `include_root` indicates whether `{zpool}{/{prefix}}` should be included in all operations. `keep_snapshots` is an integer and must be greater or equal to `1`. It specifies the number of snapshots that are kept per dataset on the source side when a cleanup operation is triggered. `suffix` contains the name suffix for new snapshots. Setting `always_changed` to `yes` causes `abgleich` to beliefe that all datasets have always changed since the last snapshot, completely ignoring what ZFS actually reports. No diff will be produced & checked for values of `written` lower than `written_threshold`. Checking diffs can be completely deactivated by setting `check_diff` to `no`. `digits` specifies how many digits are used for a decimal number describing the n-th snapshot per dataset per day as part of the name of new snapshots. `ignore` lists stuff underneath the `prefix` which will be ignored by this tool, i.e. no snapshots, backups or cleanups. `ssh` allows to fine-tune the speed of backups. In fast local networks, it is best to set `compression` to `no` because the compression is usually slowing down the transfer. However, for low-bandwidth transmissions, it makes sense to set it to `yes`. For significantly better speed in fast local networks, make sure that both the source and the target system support a common cipher, which is accelerated by [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) on both ends. The `ssh` port can be specified per side via the `port` configuration option, i.e. for `source` and/or `target`. Custom pre- and post-processing can be applied after `send` and before `receive` per side via shell commands specified in the `processing` configuration option. This can be useful for a custom transfer compression based on e.g. `lzma` or `bzip2`. +The prefix can be empty on either side. If a `host` is set to `localhost`, the `user` field can be left empty. Both source and target can be remote hosts or localhost at the same time. `include_root` indicates whether `{zpool}{/{prefix}}` should be included in all operations. `keep_snapshots` is an integer and must be greater or equal to `1`. It specifies the number of snapshots that are kept per dataset on the source side when a cleanup operation is triggered. `suffix` contains the name suffix for new snapshots. Setting `always_changed` to `yes` causes `abgleich` to beliefe that all datasets have always changed since the last snapshot, completely ignoring what ZFS actually reports. No diff will be produced & checked for values of `written` lower than `written_threshold`. Checking diffs can be completely deactivated by setting `check_diff` to `no`. `digits` specifies how many digits are used for a decimal number describing the n-th snapshot per dataset per day as part of the name of new snapshots. `ignore` lists stuff underneath the `prefix` which will be ignored by this tool, i.e. no snapshots, backups or cleanups. `ssh` allows to fine-tune the speed of backups. In fast local networks, it is best to set `compression` to `no` because the compression is usually slowing down the transfer. However, for low-bandwidth transmissions, it makes sense to set it to `yes`. For significantly better speed in fast local networks, make sure that both the source and the target system support a common cipher, which is accelerated by [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) on both ends. The `ssh` port can be specified per side via the `port` configuration option, i.e. for source and/or target. Custom pre- and post-processing can be applied after `send` and before `receive` per side via shell commands specified in the `processing` configuration option. This can be useful for a custom transfer compression based on e.g. `lzma` or `bzip2`. ## USAGE From 6a5348558ad90ecf7aa08dfd1c31546f8c2d0429 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 19:14:26 +0200 Subject: [PATCH 087/155] keep_backlog specifies how many snapshots are kept on target beyond what is present on source --- src/abgleich/core/configspec.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/abgleich/core/configspec.py b/src/abgleich/core/configspec.py index 2d3092d..6133a46 100644 --- a/src/abgleich/core/configspec.py +++ b/src/abgleich/core/configspec.py @@ -41,6 +41,11 @@ validate=lambda v: isinstance(v, int) and v >= 1, default=1, ), + ConfigField( + name="keep_backlog", + validate=lambda v: (isinstance(v, int) and v >= 0) or isinstance(v, bool), + default=True, + ), ConfigField( name="suffix", validate=lambda v: isinstance(v, str) and valid_name(v, min_len=0), @@ -122,11 +127,6 @@ validate=lambda v: isinstance(v, str), default="", ), - ConfigField( - name=f"{_side}/keep_snapshots", - validate=lambda v: isinstance(v, int) and v >= 1, - default=1, - ), ] ) From cb86106b9248318d609362edde664391e0bfd031 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 19:26:00 +0200 Subject: [PATCH 088/155] disjoint tails --- src/abgleich/core/comparison.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/abgleich/core/comparison.py b/src/abgleich/core/comparison.py index c5f2115..3590a0f 100644 --- a/src/abgleich/core/comparison.py +++ b/src/abgleich/core/comparison.py @@ -87,6 +87,14 @@ def a_disjoint_head(self) -> List[ComparisonStrictItemType]: target=[item.b for item in self._merged], ) + @property + def a_disjoint_tail(self) -> List[ComparisonStrictItemType]: + + return self._disjoint_head( + source=[item.a for item in self._merged][::-1], + target=[item.b for item in self._merged][::-1], + )[::-1] + @property def a_overlap_tail(self) -> List[ComparisonStrictItemType]: @@ -108,6 +116,14 @@ def b_disjoint_head(self) -> List[ComparisonStrictItemType]: target=[item.a for item in self._merged], ) + @property + def b_disjoint_tail(self) -> List[ComparisonStrictItemType]: + + return self._disjoint_head( + source=[item.b for item in self._merged][::-1], + target=[item.a for item in self._merged][::-1], + )[::-1] + @property def b_overlap_tail(self) -> List[ComparisonStrictItemType]: From 082d265d28f1634b5989cb872814d5a12ed19e91 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 19:28:13 +0200 Subject: [PATCH 089/155] target zpool can be cleaned based on keep_backlog, implementing #24 --- src/abgleich/core/zpool.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index b8998c3..d8b18a6 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -131,38 +131,37 @@ def generate_cleanup_transactions( assert self.side in ("source", "target") assert other.side in ("source", "target") - keep_snapshots = self._config["keep_snapshots"].value - if keep_snapshots < self._config[f"{self.side:s}/keep_snapshots"].value: - keep_snapshots = self._config[f"{self.side:s}/keep_snapshots"].value - zpool_comparison = Comparison.from_zpools(self, other) yield len(zpool_comparison), None for index, dataset_item in enumerate(zpool_comparison.merged): - yield index, self._get_cleanup_from_datasetitem( - dataset_item, keep_snapshots - ) + yield index, self._get_cleanup_from_datasetitem(dataset_item) def _get_cleanup_from_datasetitem( self, dataset_item: ComparisonItemABC, - keep_snapshots: int, ) -> Union[None, Generator[TransactionABC, None, None]]: if dataset_item.get_item().subname in self._config["ignore"]: return if dataset_item.a is None or dataset_item.b is None: return + if self.side == "target" and self._config["keep_backlog"].value == True: + return dataset_comparison = Comparison.from_datasets( dataset_item.a, dataset_item.b ) # TODO namespace if self.side == "source": - snapshots = dataset_comparison.a_overlap_tail[:-keep_snapshots] + snapshots = dataset_comparison.a_overlap_tail[:-self._config["keep_snapshots"].value] else: # target - snapshots = [] # TODO + if self._config["keep_backlog"].value in (False, 0): + keep_backlog = None + else: + keep_backlog = -self._config["keep_backlog"].value + snapshots = dataset_comparison.b_disjoint_tail[:keep_backlog] return (snapshot.get_cleanup_transaction() for snapshot in snapshots) From 1196d14a494114c68ab4980caedb4a2007ed3133 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 19:33:21 +0200 Subject: [PATCH 090/155] log changes --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 4d7b651..8a839b9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ - FEATURE: `ssh`-port on source and target becomes configurable, see #22. - FEATURE: New configuration fields for `source` and `target` each: `processing`. They can carry shell commands for pre- and post-processing of data before and after it is transferred via ssh. This enables the use of e.g. `lzma` or `bzip2` as a custom transfer compression beyond the compression capabilities of `ssh` itself. See #23. +- FEATURE: `abgleich clean` can also remove snapshots on `target` but only if they are not part of the current overlap with `source`. The behavior can be controlled via the new `keep_backlog` configuration option, see #24 and #25. - FEATURE: Configuration module contains default values for parameters, making it much easier to write lightweight configuration files, see #28. The configuration parser now also provides much more useful output. - FEATURE: Significantly more flexible shell command wrapper and, as a result, cleaned up transaction handling. - FIX: Many cleanups in code base, enabling future developments. From bbc2bc0afbd85a60448ea49ae972c2d1d48f9c17 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 19:39:27 +0200 Subject: [PATCH 091/155] add documentation on new clean features --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f799a33..71560f9 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ target: user: zfsadmin include_root: yes keep_snapshots: 2 +keep_backlog: True always_changed: no written_threshold: 1048576 check_diff: yes @@ -79,7 +80,7 @@ ssh: cipher: aes256-gcm@openssh.com ``` -The prefix can be empty on either side. If a `host` is set to `localhost`, the `user` field can be left empty. Both source and target can be remote hosts or localhost at the same time. `include_root` indicates whether `{zpool}{/{prefix}}` should be included in all operations. `keep_snapshots` is an integer and must be greater or equal to `1`. It specifies the number of snapshots that are kept per dataset on the source side when a cleanup operation is triggered. `suffix` contains the name suffix for new snapshots. Setting `always_changed` to `yes` causes `abgleich` to beliefe that all datasets have always changed since the last snapshot, completely ignoring what ZFS actually reports. No diff will be produced & checked for values of `written` lower than `written_threshold`. Checking diffs can be completely deactivated by setting `check_diff` to `no`. `digits` specifies how many digits are used for a decimal number describing the n-th snapshot per dataset per day as part of the name of new snapshots. `ignore` lists stuff underneath the `prefix` which will be ignored by this tool, i.e. no snapshots, backups or cleanups. `ssh` allows to fine-tune the speed of backups. In fast local networks, it is best to set `compression` to `no` because the compression is usually slowing down the transfer. However, for low-bandwidth transmissions, it makes sense to set it to `yes`. For significantly better speed in fast local networks, make sure that both the source and the target system support a common cipher, which is accelerated by [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) on both ends. The `ssh` port can be specified per side via the `port` configuration option, i.e. for source and/or target. Custom pre- and post-processing can be applied after `send` and before `receive` per side via shell commands specified in the `processing` configuration option. This can be useful for a custom transfer compression based on e.g. `lzma` or `bzip2`. +The prefix can be empty on either side. If a `host` is set to `localhost`, the `user` field can be left empty. Both source and target can be remote hosts or localhost at the same time. `include_root` indicates whether `{zpool}{/{prefix}}` should be included in all operations. `keep_snapshots` is an integer and must be greater or equal to `1`. It specifies the number of snapshots that are kept per dataset on the source side when a cleanup operation is triggered. `keep_backlog` is either an integer or a boolean. It specifies if (or how many) snapshots are kept on the target side if the target side be cleaned. Snapshots that are part of the overlap with the source side are never considered for removal. `suffix` contains the name suffix for new snapshots. Setting `always_changed` to `yes` causes `abgleich` to beliefe that all datasets have always changed since the last snapshot, completely ignoring what ZFS actually reports. No diff will be produced & checked for values of `written` lower than `written_threshold`. Checking diffs can be completely deactivated by setting `check_diff` to `no`. `digits` specifies how many digits are used for a decimal number describing the n-th snapshot per dataset per day as part of the name of new snapshots. `ignore` lists stuff underneath the `prefix` which will be ignored by this tool, i.e. no snapshots, backups or cleanups. `ssh` allows to fine-tune the speed of backups. In fast local networks, it is best to set `compression` to `no` because the compression is usually slowing down the transfer. However, for low-bandwidth transmissions, it makes sense to set it to `yes`. For significantly better speed in fast local networks, make sure that both the source and the target system support a common cipher, which is accelerated by [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) on both ends. The `ssh` port can be specified per side via the `port` configuration option, i.e. for source and/or target. Custom pre- and post-processing can be applied after `send` and before `receive` per side via shell commands specified in the `processing` configuration option. This can be useful for a custom transfer compression based on e.g. `lzma` or `bzip2`. ## USAGE @@ -101,9 +102,9 @@ Compare source ZFS tree with target ZFS tree. See what is missing where. Send (new) datasets and new snapshots from source to target. -### `abgleich cleanup config.yaml` +### `abgleich cleanup config.yaml [source|target]` -Cleanup older local snapshots on source side if they are present on both sides. Of those snapshots present on both sides, keep at least `keep_snapshots` number of snapshots on source side. +Cleanup older local snapshots on source side if they are present on both sides. Of those snapshots present on both sides, keep at least `keep_snapshots` number of snapshots on source side. Or: Cleanup older snapshots on target side. Beyond the overlap with source, keep at least `keep_backlog` snapshots. If `keep_backlog` is `False`, all snapshots older than the overlap will be removed. If `keep_backlog` is `True`, no snapshots will be removed. If `abgleich clean` runs against the target side, an extra warning will be displayed and must be confirmed by the user before any dangerous actions are attempted. ### `abgleich wizard config.yaml` From b9639529b6adad3027ce5d190c6f1ad258a36328 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 19:40:51 +0200 Subject: [PATCH 092/155] clarification --- src/abgleich/cli/cleanup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/cli/cleanup.py b/src/abgleich/cli/cleanup.py index c7a0a4b..d1c232c 100644 --- a/src/abgleich/cli/cleanup.py +++ b/src/abgleich/cli/cleanup.py @@ -60,7 +60,7 @@ def cleanup(configfile, side): if cleanup_side == "target": click.confirm( t( - "DANGER ZONE: You are about to clean the target. Do you want to continue?" + "DANGER ZONE: You are about to clean the TARGET. Do you want to continue?" ), abort=True, ) From 805e5a515d3186207d7d715322b9fa17524f247c Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 19:43:15 +0200 Subject: [PATCH 093/155] ! --- src/abgleich/core/comparison.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/core/comparison.py b/src/abgleich/core/comparison.py index 3590a0f..04fb0cb 100644 --- a/src/abgleich/core/comparison.py +++ b/src/abgleich/core/comparison.py @@ -250,7 +250,7 @@ def _left_strip_none( return list(itertools.dropwhile(lambda element: element is None, elements)) @staticmethod - def _test_alternations(items: List[SnapshotABC, None]): + def _test_alternations(items: List[Union[SnapshotABC, None]]): alternations = 0 state = False # None From 48757b76fdc6a79602106b989950552686da3af6 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 19:56:44 +0200 Subject: [PATCH 094/155] todo --- src/abgleich/core/zpool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index d8b18a6..2ecf833 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -173,7 +173,7 @@ def get_backup_transactions( assert self.side == "source" assert other.side == "target" - zpool_comparison = Comparison.from_zpools(self, other) + zpool_comparison = Comparison.from_zpools(self, other) # TODO namespace transactions = TransactionList() for dataset_item in zpool_comparison.merged: From 3b384546f60c8bf232aca76c97cda63cc828ab4a Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 20:14:13 +0200 Subject: [PATCH 095/155] fix: grap values from config --- src/abgleich/core/command.py | 2 +- src/abgleich/core/config.py | 4 ++-- src/abgleich/core/dataset.py | 20 ++++++++++---------- src/abgleich/core/lib.py | 2 +- src/abgleich/core/snapshot.py | 2 +- src/abgleich/core/zpool.py | 13 ++++++------- 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 662ab29..e883cd0 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -150,7 +150,7 @@ def on_side(self, side: str, config: ConfigABC) -> CommandABC: ] if ssh_config["cipher"] is not None: cmd_ssh.extend(("-c", ssh_config["cipher"])) - cmd_ssh.extend([f'{side_config["user"]:s}@{side_config["host"]:s}', str(self)]) + cmd_ssh.extend([f'{side_config["user"].value:s}@{side_config["host"].value:s}', str(self)]) return type(self)([cmd_ssh]) diff --git a/src/abgleich/core/config.py b/src/abgleich/core/config.py index 2cb6c2d..5317c0b 100644 --- a/src/abgleich/core/config.py +++ b/src/abgleich/core/config.py @@ -66,9 +66,9 @@ def __repr__(self): def __getitem__(self, key: str) -> ConfigValueTypes: return ( - self._fields[key].value + self._fields[key] if self._root is None - else self._fields[f"{self._root:s}/{key:s}"].value + else self._fields[f"{self._root:s}/{key:s}"] ) def group(self, root: str) -> ConfigABC: diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index ac4eb3a..dd1b442 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -73,7 +73,7 @@ def __init__( self._side = side self._config = config - self._root = root(config[f"{side:s}/zpool"], config[f"{side:s}/prefix"]) + self._root = root(config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value) assert self._name.startswith(self._root) self._subname = self._name[len(self._root) :].strip("/") @@ -116,18 +116,18 @@ def changed(self) -> bool: if len(self) == 0: return True - if self._config["always_changed"]: + if self._config["always_changed"].value: return True if self._properties["written"].value == 0: return False if self._properties["type"].value == "volume": return True - if self._config["written_threshold"] is not None: - if self._properties["written"].value > self._config["written_threshold"]: + if self._config["written_threshold"].value is not None: + if self._properties["written"].value > self._config["written_threshold"].value: return True - if not self._config["check_diff"]: + if not self._config["check_diff"].value: return True output, _ = ( @@ -180,8 +180,8 @@ def get_snapshot_transaction(self) -> TransactionABC: def _new_snapshot_name(self) -> str: today = datetime.datetime.now().strftime("%Y%m%d") - max_snapshots = (10 ** self._config["digits"]) - 1 - suffix = self._config["suffix"] if self._config["suffix"] is not None else "" + max_snapshots = (10 ** self._config["digits"].value) - 1 + suffix = self._config["suffix"].value if self._config["suffix"].value is not None else "" todays_names = [ snapshot.name @@ -191,14 +191,14 @@ def _new_snapshot_name(self) -> str: snapshot.name.startswith(today), snapshot.name.endswith(suffix), len(snapshot.name) - == len(today) + self._config["digits"] + len(suffix), + == len(today) + self._config["digits"].value + len(suffix), ) ) ] todays_numbers = [ - int(name[len(today) : len(today) + self._config["digits"]]) + int(name[len(today) : len(today) + self._config["digits"].value]) for name in todays_names - if name[len(today) : len(today) + self._config["digits"]].isnumeric() + if name[len(today) : len(today) + self._config["digits"].value].isnumeric() ] if len(todays_numbers) != 0: todays_numbers.sort() diff --git a/src/abgleich/core/lib.py b/src/abgleich/core/lib.py index d34eb70..2e342bb 100644 --- a/src/abgleich/core/lib.py +++ b/src/abgleich/core/lib.py @@ -45,7 +45,7 @@ def is_host_up(side: str, config: ConfigABC) -> bool: assert side in ("source", "target") - if config[f"{side:s}/host"] == "localhost": + if config[f"{side:s}/host"].value == "localhost": return True _, _, returncode, _ = ( diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index db61afa..79a8cb1 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -67,7 +67,7 @@ def __init__( self._side = side self._config = config - self._root = root(config[f"{side:s}/zpool"], config[f"{side:s}/prefix"]) + self._root = root(config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value) assert self._parent.startswith(self._root) self._subparent = self._parent[len(self._root) :].strip("/") diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 2ecf833..9c6c37f 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -74,7 +74,7 @@ def __init__( self._side = side self._config = config - self._root = root(config[f"{side:s}/zpool"], config[f"{side:s}/prefix"]) + self._root = root(config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value) def __eq__(self, other: ZpoolABC) -> bool: @@ -143,7 +143,7 @@ def _get_cleanup_from_datasetitem( dataset_item: ComparisonItemABC, ) -> Union[None, Generator[TransactionABC, None, None]]: - if dataset_item.get_item().subname in self._config["ignore"]: + if dataset_item.get_item().subname in self._config["ignore"].value: return if dataset_item.a is None or dataset_item.b is None: return @@ -216,7 +216,7 @@ def _get_backup_transactions_from_datasetitem( dataset_item: ComparisonItemABC, ) -> Union[None, Generator[TransactionABC, None, None]]: - if dataset_item.get_item().subname in self._config["ignore"]: + if dataset_item.get_item().subname in self._config["ignore"].value: return if dataset_item.a is None: return @@ -280,7 +280,7 @@ def _get_snapshot_transactions_from_dataset( self, dataset: DatasetABC ) -> Union[None, TransactionABC]: - if dataset.subname in self._config["ignore"]: + if dataset.subname in self._config["ignore"].value: return if ( dataset.get("mountpoint").value is None @@ -402,8 +402,7 @@ def from_config( config: ConfigABC, ) -> ZpoolABC: - side_config = config.group(side) - root_dataset = root(side_config["zpool"], side_config["prefix"]) + root_dataset = root(config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value) output, errors, returncode, exception = ( Command.from_list( @@ -437,7 +436,7 @@ def from_config( for line_list in output: entities[line_list[0]].append(line_list[1:]) - if not config["include_root"]: + if not config["include_root"].value: entities.pop(root_dataset) for name in [ snapshot From f6e20c1fe39c6705e51bf22e8b97f68ee59f8c48 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 20:22:09 +0200 Subject: [PATCH 096/155] fix: type check --- src/abgleich/core/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/abgleich/core/config.py b/src/abgleich/core/config.py index 5317c0b..4d8e8e2 100644 --- a/src/abgleich/core/config.py +++ b/src/abgleich/core/config.py @@ -40,7 +40,6 @@ from yaml import FullLoader as Loader from .abc import ConfigABC, ConfigFieldABC -from .configfield import ConfigValueTypes from .configspec import CONFIGSPEC # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -63,7 +62,7 @@ def __repr__(self): return "" if self._root is None else f'' - def __getitem__(self, key: str) -> ConfigValueTypes: + def __getitem__(self, key: str) -> ConfigFieldABC: return ( self._fields[key] From 870f54a5e561ca1d3feeb82476a1daee3d911269 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 20:23:08 +0200 Subject: [PATCH 097/155] fix: missing config values --- src/abgleich/core/command.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index e883cd0..7cb60c8 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -134,7 +134,7 @@ def run( def on_side(self, side: str, config: ConfigABC) -> CommandABC: - if config[f"{side:s}/host"] == "localhost": + if config[f"{side:s}/host"].value == "localhost": return self side_config = config.group(side) @@ -148,8 +148,8 @@ def on_side(self, side: str, config: ConfigABC) -> CommandABC: "-o", # Option parameter "Compression=yes" if ssh_config["compression"] else "Compression=no", ] - if ssh_config["cipher"] is not None: - cmd_ssh.extend(("-c", ssh_config["cipher"])) + if ssh_config["cipher"].value is not None: + cmd_ssh.extend(("-c", ssh_config["cipher"].value)) cmd_ssh.extend([f'{side_config["user"].value:s}@{side_config["host"].value:s}', str(self)]) return type(self)([cmd_ssh]) From 0a52951762ca51a972715e09dc89ef8c7d5d13c1 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 20:23:49 +0200 Subject: [PATCH 098/155] fix: missing config value --- src/abgleich/core/command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 7cb60c8..14d5c6f 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -144,7 +144,7 @@ def on_side(self, side: str, config: ConfigABC) -> CommandABC: "ssh", "-T", # Disable pseudo-terminal allocation "-p", # Port parameter - f'{side_config["port"]:d}', + f'{side_config["port"].value:d}', "-o", # Option parameter "Compression=yes" if ssh_config["compression"] else "Compression=no", ] From 8276918c9e1793539d48016f804fb1294e4eed55 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 20:29:22 +0200 Subject: [PATCH 099/155] fix: missing value --- src/abgleich/core/command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 14d5c6f..1b6fd40 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -146,7 +146,7 @@ def on_side(self, side: str, config: ConfigABC) -> CommandABC: "-p", # Port parameter f'{side_config["port"].value:d}', "-o", # Option parameter - "Compression=yes" if ssh_config["compression"] else "Compression=no", + "Compression=yes" if ssh_config["compression"].value else "Compression=no", ] if ssh_config["cipher"].value is not None: cmd_ssh.extend(("-c", ssh_config["cipher"].value)) From 6ff730a1aa548856feca36614a8911a9c6aae664 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 20:34:24 +0200 Subject: [PATCH 100/155] fix: missing config values --- src/abgleich/core/zpool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 9c6c37f..5683646 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -386,7 +386,7 @@ def available( "available", "-H", "-p", - root(config[f"{side:s}/zpool"], config[f"{side:s}/prefix"]), + root(config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value), ] ) .on_side(side=side, config=config) From ad2a23d6d774380cf3a16b6bd92a1dd95481173b Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 20:39:10 +0200 Subject: [PATCH 101/155] black --- src/abgleich/core/command.py | 4 +++- src/abgleich/core/dataset.py | 15 ++++++++++++--- src/abgleich/core/snapshot.py | 4 +++- src/abgleich/core/zpool.py | 17 +++++++++++++---- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 1b6fd40..362249f 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -150,7 +150,9 @@ def on_side(self, side: str, config: ConfigABC) -> CommandABC: ] if ssh_config["cipher"].value is not None: cmd_ssh.extend(("-c", ssh_config["cipher"].value)) - cmd_ssh.extend([f'{side_config["user"].value:s}@{side_config["host"].value:s}', str(self)]) + cmd_ssh.extend( + [f'{side_config["user"].value:s}@{side_config["host"].value:s}', str(self)] + ) return type(self)([cmd_ssh]) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index dd1b442..a9ce532 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -73,7 +73,9 @@ def __init__( self._side = side self._config = config - self._root = root(config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value) + self._root = root( + config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value + ) assert self._name.startswith(self._root) self._subname = self._name[len(self._root) :].strip("/") @@ -124,7 +126,10 @@ def changed(self) -> bool: return True if self._config["written_threshold"].value is not None: - if self._properties["written"].value > self._config["written_threshold"].value: + if ( + self._properties["written"].value + > self._config["written_threshold"].value + ): return True if not self._config["check_diff"].value: @@ -181,7 +186,11 @@ def _new_snapshot_name(self) -> str: today = datetime.datetime.now().strftime("%Y%m%d") max_snapshots = (10 ** self._config["digits"].value) - 1 - suffix = self._config["suffix"].value if self._config["suffix"].value is not None else "" + suffix = ( + self._config["suffix"].value + if self._config["suffix"].value is not None + else "" + ) todays_names = [ snapshot.name diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 79a8cb1..4c8bc14 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -67,7 +67,9 @@ def __init__( self._side = side self._config = config - self._root = root(config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value) + self._root = root( + config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value + ) assert self._parent.startswith(self._root) self._subparent = self._parent[len(self._root) :].strip("/") diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 5683646..dd83a1f 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -74,7 +74,9 @@ def __init__( self._side = side self._config = config - self._root = root(config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value) + self._root = root( + config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value + ) def __eq__(self, other: ZpoolABC) -> bool: @@ -155,7 +157,9 @@ def _get_cleanup_from_datasetitem( ) # TODO namespace if self.side == "source": - snapshots = dataset_comparison.a_overlap_tail[:-self._config["keep_snapshots"].value] + snapshots = dataset_comparison.a_overlap_tail[ + : -self._config["keep_snapshots"].value + ] else: # target if self._config["keep_backlog"].value in (False, 0): keep_backlog = None @@ -386,7 +390,10 @@ def available( "available", "-H", "-p", - root(config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value), + root( + config[f"{side:s}/zpool"].value, + config[f"{side:s}/prefix"].value, + ), ] ) .on_side(side=side, config=config) @@ -402,7 +409,9 @@ def from_config( config: ConfigABC, ) -> ZpoolABC: - root_dataset = root(config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value) + root_dataset = root( + config[f"{side:s}/zpool"].value, config[f"{side:s}/prefix"].value + ) output, errors, returncode, exception = ( Command.from_list( From 681ae50a122e02d1fbb969791cd64ec98ff97b6b Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 20:46:35 +0200 Subject: [PATCH 102/155] fix: mising api entry for target cleanup --- src/abgleich/core/zpool.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index dd83a1f..70ec7ef 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -102,8 +102,9 @@ def get_cleanup_transactions( other: ZpoolABC, ) -> TransactionListABC: - assert self.side == "source" - assert other.side == "target" + assert self.side != other.side + assert self.side in ("source", "target") + assert other.side in ("source", "target") zpool_comparison = Comparison.from_zpools(self, other) transactions = TransactionList() From 9681b2072ea1bd86cf5f3a24558a8348b6bb2095 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 20:51:57 +0200 Subject: [PATCH 103/155] fix: remove old assertion --- src/abgleich/core/snapshot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 4c8bc14..e64153e 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -84,8 +84,6 @@ def __getitem__(self, name: str) -> PropertyABC: def get_cleanup_transaction(self) -> TransactionABC: - assert self._side == "source" - return Transaction( meta=TransactionMeta( **{ From 8c96125030a7790de1acc51e4caebd873b89820c Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 20:52:21 +0200 Subject: [PATCH 104/155] fix: wrong side for disjoint --- src/abgleich/core/zpool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 70ec7ef..763cc58 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -166,7 +166,7 @@ def _get_cleanup_from_datasetitem( keep_backlog = None else: keep_backlog = -self._config["keep_backlog"].value - snapshots = dataset_comparison.b_disjoint_tail[:keep_backlog] + snapshots = dataset_comparison.a_disjoint_tail[:keep_backlog] return (snapshot.get_cleanup_transaction() for snapshot in snapshots) From c44a5178013fa7fd5836a4b5534673643187a217 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 21:00:39 +0200 Subject: [PATCH 105/155] shortcut --- src/abgleich/cli/cleanup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/abgleich/cli/cleanup.py b/src/abgleich/cli/cleanup.py index d1c232c..547361a 100644 --- a/src/abgleich/cli/cleanup.py +++ b/src/abgleich/cli/cleanup.py @@ -64,6 +64,9 @@ def cleanup(configfile, side): ), abort=True, ) + if config['keep_backlog'].value == True: + print(t("nothing to do")) + return for side in (cleanup_side, control_side): if not is_host_up(side, config): From a23db9ecb7ed7fe7f6dfab2b788fb1915b85dcf2 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 21:09:39 +0200 Subject: [PATCH 106/155] inherit default port from ssh config --- src/abgleich/core/command.py | 4 ++-- src/abgleich/core/configspec.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index 362249f..fa907d9 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -143,11 +143,11 @@ def on_side(self, side: str, config: ConfigABC) -> CommandABC: cmd_ssh = [ "ssh", "-T", # Disable pseudo-terminal allocation - "-p", # Port parameter - f'{side_config["port"].value:d}', "-o", # Option parameter "Compression=yes" if ssh_config["compression"].value else "Compression=no", ] + if side_config["port"].value != 0: + cmd_ssh.extend(["-p", f'{side_config["port"].value:d}']) if ssh_config["cipher"].value is not None: cmd_ssh.extend(("-c", ssh_config["cipher"].value)) cmd_ssh.extend( diff --git a/src/abgleich/core/configspec.py b/src/abgleich/core/configspec.py index 6133a46..d49dfe2 100644 --- a/src/abgleich/core/configspec.py +++ b/src/abgleich/core/configspec.py @@ -119,8 +119,8 @@ ), ConfigField( name=f"{_side}/port", - validate=lambda v: isinstance(v, int) and v > 0, - default=22, + validate=lambda v: isinstance(v, int) and v >= 0, + default=0, ), ConfigField( name=f"{_side}/processing", From b3f51daa09f176833d52bbb818cd70b62f818aac Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 27 Aug 2020 21:10:39 +0200 Subject: [PATCH 107/155] black --- src/abgleich/cli/cleanup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/cli/cleanup.py b/src/abgleich/cli/cleanup.py index 547361a..7861aed 100644 --- a/src/abgleich/cli/cleanup.py +++ b/src/abgleich/cli/cleanup.py @@ -64,7 +64,7 @@ def cleanup(configfile, side): ), abort=True, ) - if config['keep_backlog'].value == True: + if config["keep_backlog"].value == True: print(t("nothing to do")) return From 842eac9f026023cf7f17d0939bff4241d915dd84 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Fri, 28 Aug 2020 11:30:53 +0200 Subject: [PATCH 108/155] spelling --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 8a839b9..9f981e9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,7 +11,7 @@ ## 0.0.7 (2020-08-05) -- FIX: `tree` now property checks if source or target is up, depending on what a user wants to see, see #20. +- FIX: `tree` now properly checks if source or target is up, depending on what a user wants to see, see #20. - FIX: All `abgleich` commands can properly initialize (instead of crashing) if the target tree is empty, see #19. - FIX: `tree` shows message if there is no tree instead of crashing, see #18. From ef749b55213f159a6e847a201da91f9f3d1c4e30 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 12:53:49 +0200 Subject: [PATCH 109/155] new abc(s) --- src/abgleich/core/abc.py | 6 +- src/abgleich/core/comparison.py | 400 -------------------------------- 2 files changed, 5 insertions(+), 401 deletions(-) delete mode 100644 src/abgleich/core/comparison.py diff --git a/src/abgleich/core/abc.py b/src/abgleich/core/abc.py index 8cd5f02..255289c 100644 --- a/src/abgleich/core/abc.py +++ b/src/abgleich/core/abc.py @@ -43,7 +43,7 @@ class CommandABC(ABC): pass -class ComparisonABC(ABC): +class ComparisonDatasetABC(ABC): pass @@ -51,6 +51,10 @@ class ComparisonItemABC(ABC): pass +class ComparisonZpoolABC(ABC): + pass + + class ConfigABC(ABC): pass diff --git a/src/abgleich/core/comparison.py b/src/abgleich/core/comparison.py deleted file mode 100644 index 04fb0cb..0000000 --- a/src/abgleich/core/comparison.py +++ /dev/null @@ -1,400 +0,0 @@ -# -*- coding: utf-8 -*- - -""" - -ABGLEICH -zfs sync tool -https://github.com/pleiszenburg/abgleich - - src/abgleich/core/comparison.py: ZFS comparison - - Copyright (C) 2019-2020 Sebastian M. Ernst - - -The contents of this file are subject to the GNU Lesser General Public License -Version 2.1 ("LGPL" or "License"). You may not use this file except in -compliance with the License. You may obtain a copy of the License at -https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -https://github.com/pleiszenburg/abgleich/blob/master/LICENSE - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -specific language governing rights and limitations under the License. - - -""" - -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# IMPORT -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -import itertools -from typing import Generator, List, Union - -from typeguard import typechecked - -from .abc import ComparisonABC, ComparisonItemABC, DatasetABC, SnapshotABC, ZpoolABC -from .comparisonitem import ComparisonItem, ComparisonItemType, ComparisonStrictItemType - -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# TYPING -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -ComparisonParentTypes = Union[ - ZpoolABC, - DatasetABC, - None, -] - -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# CLASS -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - -@typechecked -class Comparison(ComparisonABC): - """ - Immutable. - """ - - def __init__( - self, - a: ComparisonParentTypes, - b: ComparisonParentTypes, - merged: List[ComparisonItemABC], - ): - - assert a is not None or b is not None - if a is not None and b is not None: - assert type(a) == type(b) - - self._a, self._b, self._merged = a, b, merged - - def __len__(self) -> int: - - return len(self._merged) - - @property - def a(self) -> ComparisonParentTypes: - - return self._a - - @property - def a_disjoint_head(self) -> List[ComparisonStrictItemType]: - - return self._disjoint_head( - source=[item.a for item in self._merged], - target=[item.b for item in self._merged], - ) - - @property - def a_disjoint_tail(self) -> List[ComparisonStrictItemType]: - - return self._disjoint_head( - source=[item.a for item in self._merged][::-1], - target=[item.b for item in self._merged][::-1], - )[::-1] - - @property - def a_overlap_tail(self) -> List[ComparisonStrictItemType]: - - return self._overlap_tail( - source=[item.a for item in self._merged], - target=[item.b for item in self._merged], - ) - - @property - def b(self) -> ComparisonParentTypes: - - return self._b - - @property - def b_disjoint_head(self) -> List[ComparisonStrictItemType]: - - return self._disjoint_head( - source=[item.b for item in self._merged], - target=[item.a for item in self._merged], - ) - - @property - def b_disjoint_tail(self) -> List[ComparisonStrictItemType]: - - return self._disjoint_head( - source=[item.b for item in self._merged][::-1], - target=[item.a for item in self._merged][::-1], - )[::-1] - - @property - def b_overlap_tail(self) -> List[ComparisonStrictItemType]: - - return self._overlap_tail( - source=[item.b for item in self._merged], - target=[item.a for item in self._merged], - ) - - @property - def merged(self) -> Generator[ComparisonItemABC, None, None]: - - return (item for item in self._merged) - - @classmethod - def _disjoint_head( - cls, - source: List[ComparisonItemType], - target: List[ComparisonItemType], - ) -> List[ComparisonItemType]: - """ - Returns new elements from source. - If target is empty, returns source. - If head of target and head of source are identical, returns empty list. - """ - - source, target = cls._strip_none(source), cls._strip_none(target) - - if any((element is None for element in source)): - raise ValueError("source is not consecutive") - if any((element is None for element in target)): - raise ValueError("target is not consecutive") - - if len(source) == 0: - raise ValueError("source must not be empty") - - if len(set([item.name for item in source])) != len(source): - raise ValueError("source contains doublicate entires") - if len(set([item.name for item in target])) != len(target): - raise ValueError("target contains doublicate entires") - - if len(target) == 0: - return source # all of source, target is empty - - try: - source_index = [item.name for item in source].index(target[-1].name) - except ValueError: - raise ValueError("last target element not in source") - - old_source = source[: source_index + 1] - - if len(old_source) <= len(target): - if target[-len(old_source) :] != old_source: - raise ValueError( - "no clean match between end of target and beginning of source" - ) - else: - if target != source[source_index + 1 - len(target) : source_index + 1]: - raise ValueError( - "no clean match between entire target and beginning of source" - ) - - return source[source_index + 1 :] - - @classmethod - def _overlap_tail( - cls, - source: List[ComparisonItemType], - target: List[ComparisonItemType], - ) -> List[ComparisonItemType]: - """ - Overlap must include first element of source. - """ - - source, target = cls._strip_none(source), cls._strip_none(target) - - if len(source) == 0 or len(target) == 0: - return [] - - if any((element is None for element in source)): - raise ValueError("source is not consecutive") - if any((element is None for element in target)): - raise ValueError("target is not consecutive") - - source_names = {item.name for item in source} - target_names = {item.name for item in target} - - if len(source_names) != len(source): - raise ValueError("source contains doublicate entires") - if len(target_names) != len(target): - raise ValueError("target contains doublicate entires") - - overlap_tail = [] - for item in source: - if item.name not in target_names: - break - overlap_tail.append(item) - - if len(overlap_tail) == 0: - return overlap_tail - - target_index = target.index(overlap_tail[0]) - if overlap_tail != target[target_index : target_index + len(overlap_tail)]: - raise ValueError("no clean match in overlap area") - - return overlap_tail - - @classmethod - def _strip_none( - cls, elements: List[ComparisonItemType] - ) -> List[ComparisonItemType]: - - elements = cls._left_strip_none(elements) # left strip - elements.reverse() # flip into reverse - elements = cls._left_strip_none(elements) # right strip - elements.reverse() # flip back - - return elements - - @staticmethod - def _left_strip_none( - elements: List[ComparisonItemType], - ) -> List[ComparisonItemType]: - - return list(itertools.dropwhile(lambda element: element is None, elements)) - - @staticmethod - def _test_alternations(items: List[Union[SnapshotABC, None]]): - - alternations = 0 - state = False # None - - for item in items: - - new_state = item is not None - - if new_state == state: - continue - - alternations += 1 - state = new_state - - if alternations > 2: - raise ValueError("gap in snapshot series") - - @staticmethod - def _test_names(items: List[ComparisonItemABC]): - - for item in items: - if item.a is not None and item.b is not None: - if item.a.name != item.b.name: - raise ValueError("inconsistent snapshot names") - - @staticmethod - def _merge_datasets( - items_a: Generator[DatasetABC, None, None], - items_b: Generator[DatasetABC, None, None], - ) -> List[ComparisonItemABC]: - - items_a = {item.subname: item for item in items_a} - items_b = {item.subname: item for item in items_b} - - names = list(items_a.keys() | items_b.keys()) - merged = [ - ComparisonItem(items_a.get(name, None), items_b.get(name, None)) - for name in names - ] - merged.sort(key=lambda item: item.get_item().name) - - return merged - - @classmethod - def from_zpools( - cls, - zpool_a: Union[ZpoolABC, None], - zpool_b: Union[ZpoolABC, None], - ) -> ComparisonABC: - - assert zpool_a is not None or zpool_b is not None - - if (zpool_a is None) ^ (zpool_b is None): - return cls( - a=zpool_a, - b=zpool_b, - merged=ComparisonItem.list_from_singles( - getattr(zpool_a, "datasets", None), - getattr(zpool_b, "datasets", None), - ), - ) - - assert zpool_a is not zpool_b - assert zpool_a != zpool_b - - return cls( - a=zpool_a, - b=zpool_b, - merged=cls._merge_datasets(zpool_a.datasets, zpool_b.datasets), - ) - - @classmethod - def _merge_snapshots( - cls, - items_a: Generator[SnapshotABC, None, None], - items_b: Generator[SnapshotABC, None, None], - ) -> List[ComparisonItemABC]: - - items_a, items_b = list(items_a), list(items_b) - - names_a = [item.name for item in items_a] - names_b = [item.name for item in items_b] - - assert len(set(names_a)) == len(items_a) # unique names - assert len(set(names_b)) == len(items_b) # unique names - - if len(items_a) == 0 and len(items_b) == 0: - return [] - if len(items_a) == 0: - return [ComparisonItem(None, item) for item in items_b] - if len(items_b) == 0: - return [ComparisonItem(item, None) for item in items_a] - - start_b = names_a.index(names_b[0]) if names_b[0] in names_a else None - start_a = names_b.index(names_a[0]) if names_a[0] in names_b else None - - assert start_a is not None or start_b is not None # overlap - - if start_a is not None: # fill prefix - items_a = [None for _ in range(start_a)] + items_a - if start_b is not None: # fill prefix - items_b = [None for _ in range(start_b)] + items_b - if len(items_a) < len(items_b): # fill suffix - items_a = items_a + [None for _ in range(len(items_b) - len(items_a))] - if len(items_b) < len(items_a): # fill suffix - items_b = items_b + [None for _ in range(len(items_a) - len(items_b))] - - assert len(items_a) == len(items_b) - - cls._test_alternations(items_a) - cls._test_alternations(items_b) - - merged = [ - ComparisonItem(item_a, item_b) for item_a, item_b in zip(items_a, items_b) - ] - - cls._test_names(merged) - - return merged - - @classmethod - def from_datasets( - cls, - dataset_a: Union[DatasetABC, None], - dataset_b: Union[DatasetABC, None], - ) -> ComparisonABC: - - assert dataset_a is not None or dataset_b is not None - - if (dataset_a is None) ^ (dataset_b is None): - return cls( - a=dataset_a, - b=dataset_b, - merged=ComparisonItem.list_from_singles( - getattr(dataset_a, "snapshots", None), - getattr(dataset_b, "snapshots", None), - ), - ) - - assert dataset_a is not dataset_b - assert dataset_a == dataset_b - - return cls( - a=dataset_a, - b=dataset_b, - merged=cls._merge_snapshots(dataset_a.snapshots, dataset_b.snapshots), - ) From 5c77c13b3065894ff838e674f0c493f3d6ff6b83 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 12:54:06 +0200 Subject: [PATCH 110/155] new comparison for zpools --- src/abgleich/core/comparisonzpool.py | 125 +++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/abgleich/core/comparisonzpool.py diff --git a/src/abgleich/core/comparisonzpool.py b/src/abgleich/core/comparisonzpool.py new file mode 100644 index 0000000..70f2154 --- /dev/null +++ b/src/abgleich/core/comparisonzpool.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +""" + +ABGLEICH +zfs sync tool +https://github.com/pleiszenburg/abgleich + + src/abgleich/core/comparisonzpool.py: ZFS zpool comparison + + Copyright (C) 2019-2020 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/abgleich/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from typing import Generator, List, Union + +from typeguard import typechecked + +from .abc import ComparisonItemABC, ComparisonZpoolABC, DatasetABC, ZpoolABC +from .comparisonitem import ComparisonItem + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + +@typechecked +class ComparisonZpool(ComparisonZpoolABC): + """ + Immutable. No order, just name-based matching. + """ + + def __init__( + self, + a: Union[ZpoolABC, None], + b: Union[ZpoolABC, None], + merged: List[ComparisonItemABC], + ): + + assert a is not None or b is not None + if a is not None and b is not None: + assert type(a) == type(b) + + self._a, self._b, self._merged = a, b, merged + + def __len__(self) -> int: + + return len(self._merged) + + @property + def a(self) -> Union[ZpoolABC, None]: + + return self._a + + @property + def b(self) -> Union[ZpoolABC, None]: + + return self._b + + @property + def merged(self) -> Generator[ComparisonItemABC, None, None]: + + return (item for item in self._merged) + + @staticmethod + def _merge_datasets( + items_a: Generator[DatasetABC, None, None], + items_b: Generator[DatasetABC, None, None], + ) -> List[ComparisonItemABC]: + + items_a = {item.subname: item for item in items_a} + items_b = {item.subname: item for item in items_b} + + names = list(items_a.keys() | items_b.keys()) + merged = [ + ComparisonItem(items_a.get(name, None), items_b.get(name, None)) + for name in names + ] + merged.sort(key=lambda item: item.get_item().name) + + return merged + + @classmethod + def from_zpools( + cls, + zpool_a: Union[ZpoolABC, None], + zpool_b: Union[ZpoolABC, None], + ) -> ComparisonZpoolABC: + + assert zpool_a is not None or zpool_b is not None + + if (zpool_a is None) ^ (zpool_b is None): + return cls( + a=zpool_a, + b=zpool_b, + merged=ComparisonItem.list_from_singles( + getattr(zpool_a, "datasets", None), + getattr(zpool_b, "datasets", None), + ), + ) + + assert zpool_a is not zpool_b + assert zpool_a != zpool_b + + return cls( + a=zpool_a, + b=zpool_b, + merged=cls._merge_datasets(zpool_a.datasets, zpool_b.datasets), + ) From 44e702f9c0609ad4b855bcf47ec5a77cfc42bb6f Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 12:54:18 +0200 Subject: [PATCH 111/155] new comparison for datasets --- src/abgleich/core/comparisondataset.py | 344 +++++++++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 src/abgleich/core/comparisondataset.py diff --git a/src/abgleich/core/comparisondataset.py b/src/abgleich/core/comparisondataset.py new file mode 100644 index 0000000..2d297f2 --- /dev/null +++ b/src/abgleich/core/comparisondataset.py @@ -0,0 +1,344 @@ +# -*- coding: utf-8 -*- + +""" + +ABGLEICH +zfs sync tool +https://github.com/pleiszenburg/abgleich + + src/abgleich/core/comparisondataset.py: ZFS dataset comparison + + Copyright (C) 2019-2020 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/abgleich/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import itertools +from typing import Generator, List, Union + +from typeguard import typechecked + +from .abc import ComparisonDatasetABC, ComparisonItemABC, DatasetABC, SnapshotABC +from .comparisonitem import ComparisonItem, ComparisonItemType, ComparisonStrictItemType + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + +@typechecked +class ComparisonDataset(ComparisonDatasetABC): + """ + Immutable. + """ + + def __init__( + self, + a: Union[DatasetABC, None], + b: Union[DatasetABC, None], + merged: List[ComparisonItemABC], + ): + + assert a is not None or b is not None + if a is not None and b is not None: + assert type(a) == type(b) + + self._a, self._b, self._merged = a, b, merged + + def __len__(self) -> int: + + return len(self._merged) + + @property + def a(self) -> Union[DatasetABC, None]: + + return self._a + + @property + def a_disjoint_head(self) -> List[ComparisonStrictItemType]: + + return self._disjoint_head( + source=[item.a for item in self._merged], + target=[item.b for item in self._merged], + ) + + @property + def a_disjoint_tail(self) -> List[ComparisonStrictItemType]: + + return self._disjoint_head( + source=[item.a for item in self._merged][::-1], + target=[item.b for item in self._merged][::-1], + )[::-1] + + @property + def a_overlap_tail(self) -> List[ComparisonStrictItemType]: + + return self._overlap_tail( + source=[item.a for item in self._merged], + target=[item.b for item in self._merged], + ) + + @property + def b(self) -> Union[DatasetABC, None]: + + return self._b + + @property + def b_disjoint_head(self) -> List[ComparisonStrictItemType]: + + return self._disjoint_head( + source=[item.b for item in self._merged], + target=[item.a for item in self._merged], + ) + + @property + def b_disjoint_tail(self) -> List[ComparisonStrictItemType]: + + return self._disjoint_head( + source=[item.b for item in self._merged][::-1], + target=[item.a for item in self._merged][::-1], + )[::-1] + + @property + def b_overlap_tail(self) -> List[ComparisonStrictItemType]: + + return self._overlap_tail( + source=[item.b for item in self._merged], + target=[item.a for item in self._merged], + ) + + @property + def merged(self) -> Generator[ComparisonItemABC, None, None]: + + return (item for item in self._merged) + + @classmethod + def _disjoint_head( + cls, + source: List[ComparisonItemType], + target: List[ComparisonItemType], + ) -> List[ComparisonItemType]: + """ + Returns new elements from source. + If target is empty, returns source. + If head of target and head of source are identical, returns empty list. + """ + + source, target = cls._strip_none(source), cls._strip_none(target) + + if any((element is None for element in source)): + raise ValueError("source is not consecutive") + if any((element is None for element in target)): + raise ValueError("target is not consecutive") + + if len(source) == 0: + raise ValueError("source must not be empty") + + if len(set([item.name for item in source])) != len(source): + raise ValueError("source contains doublicate entires") + if len(set([item.name for item in target])) != len(target): + raise ValueError("target contains doublicate entires") + + if len(target) == 0: + return source # all of source, target is empty + + try: + source_index = [item.name for item in source].index(target[-1].name) + except ValueError: + raise ValueError("last target element not in source") + + old_source = source[: source_index + 1] + + if len(old_source) <= len(target): + if target[-len(old_source) :] != old_source: + raise ValueError( + "no clean match between end of target and beginning of source" + ) + else: + if target != source[source_index + 1 - len(target) : source_index + 1]: + raise ValueError( + "no clean match between entire target and beginning of source" + ) + + return source[source_index + 1 :] + + @classmethod + def _overlap_tail( + cls, + source: List[ComparisonItemType], + target: List[ComparisonItemType], + ) -> List[ComparisonItemType]: + """ + Overlap must include first element of source. + """ + + source, target = cls._strip_none(source), cls._strip_none(target) + + if len(source) == 0 or len(target) == 0: + return [] + + if any((element is None for element in source)): + raise ValueError("source is not consecutive") + if any((element is None for element in target)): + raise ValueError("target is not consecutive") + + source_names = {item.name for item in source} + target_names = {item.name for item in target} + + if len(source_names) != len(source): + raise ValueError("source contains doublicate entires") + if len(target_names) != len(target): + raise ValueError("target contains doublicate entires") + + overlap_tail = [] + for item in source: + if item.name not in target_names: + break + overlap_tail.append(item) + + if len(overlap_tail) == 0: + return overlap_tail + + target_index = target.index(overlap_tail[0]) + if overlap_tail != target[target_index : target_index + len(overlap_tail)]: + raise ValueError("no clean match in overlap area") + + return overlap_tail + + @classmethod + def _strip_none( + cls, elements: List[ComparisonItemType] + ) -> List[ComparisonItemType]: + + elements = cls._left_strip_none(elements) # left strip + elements.reverse() # flip into reverse + elements = cls._left_strip_none(elements) # right strip + elements.reverse() # flip back + + return elements + + @staticmethod + def _left_strip_none( + elements: List[ComparisonItemType], + ) -> List[ComparisonItemType]: + + return list(itertools.dropwhile(lambda element: element is None, elements)) + + @staticmethod + def _test_alternations(items: List[Union[SnapshotABC, None]]): + + alternations = 0 + state = False # None + + for item in items: + + new_state = item is not None + + if new_state == state: + continue + + alternations += 1 + state = new_state + + if alternations > 2: + raise ValueError("gap in snapshot series") + + @staticmethod + def _test_names(items: List[ComparisonItemABC]): + + for item in items: + if item.a is not None and item.b is not None: + if item.a.name != item.b.name: + raise ValueError("inconsistent snapshot names") + + @classmethod + def _merge_snapshots( + cls, + items_a: Generator[SnapshotABC, None, None], + items_b: Generator[SnapshotABC, None, None], + ) -> List[ComparisonItemABC]: + + items_a, items_b = list(items_a), list(items_b) + + names_a = [item.name for item in items_a] + names_b = [item.name for item in items_b] + + assert len(set(names_a)) == len(items_a) # unique names + assert len(set(names_b)) == len(items_b) # unique names + + if len(items_a) == 0 and len(items_b) == 0: + return [] + if len(items_a) == 0: + return [ComparisonItem(None, item) for item in items_b] + if len(items_b) == 0: + return [ComparisonItem(item, None) for item in items_a] + + start_b = names_a.index(names_b[0]) if names_b[0] in names_a else None + start_a = names_b.index(names_a[0]) if names_a[0] in names_b else None + + assert start_a is not None or start_b is not None # overlap + + if start_a is not None: # fill prefix + items_a = [None for _ in range(start_a)] + items_a + if start_b is not None: # fill prefix + items_b = [None for _ in range(start_b)] + items_b + if len(items_a) < len(items_b): # fill suffix + items_a = items_a + [None for _ in range(len(items_b) - len(items_a))] + if len(items_b) < len(items_a): # fill suffix + items_b = items_b + [None for _ in range(len(items_a) - len(items_b))] + + assert len(items_a) == len(items_b) + + cls._test_alternations(items_a) + cls._test_alternations(items_b) + + merged = [ + ComparisonItem(item_a, item_b) for item_a, item_b in zip(items_a, items_b) + ] + + cls._test_names(merged) + + return merged + + @classmethod + def from_datasets( + cls, + dataset_a: Union[DatasetABC, None], + dataset_b: Union[DatasetABC, None], + ) -> ComparisonDatasetABC: + + assert dataset_a is not None or dataset_b is not None + + if (dataset_a is None) ^ (dataset_b is None): + return cls( + a=dataset_a, + b=dataset_b, + merged=ComparisonItem.list_from_singles( + getattr(dataset_a, "snapshots", None), + getattr(dataset_b, "snapshots", None), + ), + ) + + assert dataset_a is not dataset_b + assert dataset_a == dataset_b + + return cls( + a=dataset_a, + b=dataset_b, + merged=cls._merge_snapshots(dataset_a.snapshots, dataset_b.snapshots), + ) From e2a56bdfa94f4706b2103392a331776675b15857 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 12:54:32 +0200 Subject: [PATCH 112/155] zpool is running on new comparisons --- src/abgleich/core/zpool.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 763cc58..8e2dfee 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -44,7 +44,8 @@ ZpoolABC, ) from .command import Command -from .comparison import Comparison +from .comparisondataset import ComparisonDataset +from .comparisonzpool import ComparisonZpool from .dataset import Dataset from .i18n import t from .io import colorize, humanize_size @@ -106,7 +107,7 @@ def get_cleanup_transactions( assert self.side in ("source", "target") assert other.side in ("source", "target") - zpool_comparison = Comparison.from_zpools(self, other) + zpool_comparison = ComparisonZpool.from_zpools(self, other) transactions = TransactionList() for dataset_item in zpool_comparison.merged: @@ -134,7 +135,7 @@ def generate_cleanup_transactions( assert self.side in ("source", "target") assert other.side in ("source", "target") - zpool_comparison = Comparison.from_zpools(self, other) + zpool_comparison = ComparisonZpool.from_zpools(self, other) yield len(zpool_comparison), None @@ -153,7 +154,7 @@ def _get_cleanup_from_datasetitem( if self.side == "target" and self._config["keep_backlog"].value == True: return - dataset_comparison = Comparison.from_datasets( + dataset_comparison = ComparisonDataset.from_datasets( dataset_item.a, dataset_item.b ) # TODO namespace @@ -178,7 +179,7 @@ def get_backup_transactions( assert self.side == "source" assert other.side == "target" - zpool_comparison = Comparison.from_zpools(self, other) # TODO namespace + zpool_comparison = ComparisonZpool.from_zpools(self, other) # TODO namespace transactions = TransactionList() for dataset_item in zpool_comparison.merged: @@ -206,7 +207,7 @@ def generate_backup_transactions( assert self.side == "source" assert other.side == "target" - zpool_comparison = Comparison.from_zpools(self, other) + zpool_comparison = ComparisonZpool.from_zpools(self, other) yield len(zpool_comparison), None @@ -229,7 +230,7 @@ def _get_backup_transactions_from_datasetitem( if dataset_item.b is None: snapshots = list(dataset_item.a.snapshots) else: - dataset_comparison = Comparison.from_datasets( + dataset_comparison = ComparisonDataset.from_datasets( dataset_item.a, dataset_item.b ) snapshots = dataset_comparison.a_disjoint_head @@ -332,19 +333,19 @@ def _table_row(entity: Union[SnapshotABC, DatasetABC]) -> List[str]: def print_comparison_table(self, other: ZpoolABC): - zpool_comparison = Comparison.from_zpools(self, other) + zpool_comparison = ComparisonZpool.from_zpools(self, other) table = [] for dataset_item in zpool_comparison.merged: table.append(self._comparison_table_row(dataset_item)) if dataset_item.complete: - dataset_comparison = Comparison.from_datasets( + dataset_comparison = ComparisonDataset.from_datasets( dataset_item.a, dataset_item.b ) elif dataset_item.a is not None: - dataset_comparison = Comparison.from_datasets(dataset_item.a, None) + dataset_comparison = ComparisonDataset.from_datasets(dataset_item.a, None) else: - dataset_comparison = Comparison.from_datasets(None, dataset_item.b) + dataset_comparison = ComparisonDataset.from_datasets(None, dataset_item.b) for snapshot_item in dataset_comparison.merged: table.append(self._comparison_table_row(snapshot_item)) From e8354ce0dd5699e61924fd1677389de7be7edf08 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 12:55:05 +0200 Subject: [PATCH 113/155] black --- src/abgleich/core/zpool.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 8e2dfee..eae4ef9 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -343,9 +343,13 @@ def print_comparison_table(self, other: ZpoolABC): dataset_item.a, dataset_item.b ) elif dataset_item.a is not None: - dataset_comparison = ComparisonDataset.from_datasets(dataset_item.a, None) + dataset_comparison = ComparisonDataset.from_datasets( + dataset_item.a, None + ) else: - dataset_comparison = ComparisonDataset.from_datasets(None, dataset_item.b) + dataset_comparison = ComparisonDataset.from_datasets( + None, dataset_item.b + ) for snapshot_item in dataset_comparison.merged: table.append(self._comparison_table_row(snapshot_item)) From e66cf274eb5352694db0298ef25d31e3cccec4b4 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 13:02:15 +0200 Subject: [PATCH 114/155] todos for namespace --- src/abgleich/core/dataset.py | 2 ++ src/abgleich/core/zpool.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index a9ce532..d485a65 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -116,6 +116,8 @@ def get( @property def changed(self) -> bool: + # TODO namespace: if last snapshot is from other namespace, make one ... + if len(self) == 0: return True if self._config["always_changed"].value: diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index eae4ef9..13d783d 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -179,7 +179,7 @@ def get_backup_transactions( assert self.side == "source" assert other.side == "target" - zpool_comparison = ComparisonZpool.from_zpools(self, other) # TODO namespace + zpool_comparison = ComparisonZpool.from_zpools(self, other) transactions = TransactionList() for dataset_item in zpool_comparison.merged: @@ -232,7 +232,7 @@ def _get_backup_transactions_from_datasetitem( else: dataset_comparison = ComparisonDataset.from_datasets( dataset_item.a, dataset_item.b - ) + ) # TODO namespace snapshots = dataset_comparison.a_disjoint_head if len(snapshots) == 0: @@ -293,7 +293,7 @@ def _get_snapshot_transactions_from_dataset( and dataset["type"].value == "filesystem" ): return - if not dataset.changed: + if not dataset.changed: # TODO namespace return return dataset.get_snapshot_transaction() From 48b4a89d4ea2c9337a9c1a18ad7d3d99dc59a363 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 13:05:38 +0200 Subject: [PATCH 115/155] get method for snapshot properties --- src/abgleich/core/snapshot.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index e64153e..15d07a3 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -82,6 +82,17 @@ def __getitem__(self, name: str) -> PropertyABC: return self._properties[name] + def get( + self, + key: str, + default: Union[None, PropertyABC] = None, + ) -> Union[None, PropertyABC]: + + return self._properties.get( + key, + Property(key, None, None) if default is None else default, + ) + def get_cleanup_transaction(self) -> TransactionABC: return Transaction( From 2421aa31c29187eb33706ef71c64cfe1be565d5a Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 18:24:13 +0200 Subject: [PATCH 116/155] dataset has ignore property --- src/abgleich/core/dataset.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index d485a65..69e3d97 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -146,6 +146,11 @@ def changed(self) -> bool: ) return len(output[0].strip(" \t\n")) > 0 + @property + def ignore(self) -> bool: + + return self._subname in self._config['ignore'].value + @property def name(self) -> str: From d22ffe6eb33d7093ec545101f1bc0c126eb52dea Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 18:26:49 +0200 Subject: [PATCH 117/155] tree highlights ignored datasets --- src/abgleich/core/zpool.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 13d783d..9f232ae 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -302,7 +302,7 @@ def print_table(self): table = [] for dataset in self._datasets: - table.append(self._table_row(dataset)) + table.append(self._table_row(dataset, ignore = dataset.ignore)) for snapshot in dataset.snapshots: table.append(self._table_row(snapshot)) @@ -320,12 +320,14 @@ def print_table(self): ) @staticmethod - def _table_row(entity: Union[SnapshotABC, DatasetABC]) -> List[str]: + def _table_row(entity: Union[SnapshotABC, DatasetABC], ignore: bool = False) -> List[str]: + + color = "white" if not ignore else "red" return [ "- " + colorize(entity.name, "grey") if isinstance(entity, SnapshotABC) - else colorize(entity.name, "white"), + else colorize(entity.name, color), humanize_size(entity["used"].value, add_color=True), humanize_size(entity["referenced"].value, add_color=True), f'{entity["compressratio"].value:.02f}', From 91cb78f2a92b96b2a6aad532d1d7b12b02cdb10a Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 18:28:48 +0200 Subject: [PATCH 118/155] zpool uses ignore property from dataset --- src/abgleich/core/zpool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 9f232ae..f4e03ab 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -147,7 +147,7 @@ def _get_cleanup_from_datasetitem( dataset_item: ComparisonItemABC, ) -> Union[None, Generator[TransactionABC, None, None]]: - if dataset_item.get_item().subname in self._config["ignore"].value: + if dataset_item.get_item().ignore: return if dataset_item.a is None or dataset_item.b is None: return @@ -222,7 +222,7 @@ def _get_backup_transactions_from_datasetitem( dataset_item: ComparisonItemABC, ) -> Union[None, Generator[TransactionABC, None, None]]: - if dataset_item.get_item().subname in self._config["ignore"].value: + if dataset_item.get_item().ignore: return if dataset_item.a is None: return @@ -286,7 +286,7 @@ def _get_snapshot_transactions_from_dataset( self, dataset: DatasetABC ) -> Union[None, TransactionABC]: - if dataset.subname in self._config["ignore"].value: + if dataset.ignore: return if ( dataset.get("mountpoint").value is None From 77609b2b2e3d808204d356cbf8ef0f86a88708bc Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 18:42:08 +0200 Subject: [PATCH 119/155] comparison highlights ignored datasets --- src/abgleich/core/zpool.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index f4e03ab..010bb9c 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -339,7 +339,7 @@ def print_comparison_table(self, other: ZpoolABC): table = [] for dataset_item in zpool_comparison.merged: - table.append(self._comparison_table_row(dataset_item)) + table.append(self._comparison_table_row(dataset_item, ignore = dataset_item.get_item().ignore)) if dataset_item.complete: dataset_comparison = ComparisonDataset.from_datasets( dataset_item.a, dataset_item.b @@ -353,7 +353,7 @@ def print_comparison_table(self, other: ZpoolABC): None, dataset_item.b ) for snapshot_item in dataset_comparison.merged: - table.append(self._comparison_table_row(snapshot_item)) + table.append(self._comparison_table_row(snapshot_item, ignore = dataset_item.get_item().ignore)) print( tabulate( @@ -364,22 +364,27 @@ def print_comparison_table(self, other: ZpoolABC): ) @staticmethod - def _comparison_table_row(item: ComparisonItemABC) -> List[str]: + def _comparison_table_row(item: ComparisonItemABC, ignore: bool = False) -> List[str]: + + color1, color2 = ("white", "grey") if not ignore else ("red", "yellow") + colorR, colorG, colorB = ("red", "green", "blue") if not ignore else ("grey", "grey", "grey") + + symbol = "X" entity = item.get_item() name = entity.name if isinstance(entity, SnapshotABC) else entity.subname if item.a is not None and item.b is not None: - a, b = colorize("X", "green"), colorize("X", "green") + a, b = colorize(symbol, colorG), colorize(symbol, colorG) elif item.a is None and item.b is not None: - a, b = "", colorize("X", "blue") + a, b = "", colorize(symbol, colorB) elif item.a is not None and item.b is None: - a, b = colorize("X", "red"), "" + a, b = colorize(symbol, colorR), "" return [ - "- " + colorize(name, "grey") + "- " + colorize(name, color2) if isinstance(entity, SnapshotABC) - else colorize(name, "white"), + else colorize(name, color1), a, b, ] From 4a741c733a95c32c94c386ca7e8b4f106ee2378e Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 18:42:48 +0200 Subject: [PATCH 120/155] log change --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 9f981e9..1cefc3e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ - FEATURE: New configuration fields for `source` and `target` each: `processing`. They can carry shell commands for pre- and post-processing of data before and after it is transferred via ssh. This enables the use of e.g. `lzma` or `bzip2` as a custom transfer compression beyond the compression capabilities of `ssh` itself. See #23. - FEATURE: `abgleich clean` can also remove snapshots on `target` but only if they are not part of the current overlap with `source`. The behavior can be controlled via the new `keep_backlog` configuration option, see #24 and #25. - FEATURE: Configuration module contains default values for parameters, making it much easier to write lightweight configuration files, see #28. The configuration parser now also provides much more useful output. +- FEATURE: `abgleich tree` and `abgleich compare` highlight ignored datasets. - FEATURE: Significantly more flexible shell command wrapper and, as a result, cleaned up transaction handling. - FIX: Many cleanups in code base, enabling future developments. From 3cb95ae2d9c410d758c077dac615d760436cf49a Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 18:55:12 +0200 Subject: [PATCH 121/155] broken transaction classes into modules --- src/abgleich/core/transaction.py | 202 +------------------------- src/abgleich/core/transactionlist.py | 208 +++++++++++++++++++++++++++ src/abgleich/core/transactionmeta.py | 71 +++++++++ 3 files changed, 281 insertions(+), 200 deletions(-) create mode 100644 src/abgleich/core/transactionlist.py create mode 100644 src/abgleich/core/transactionmeta.py diff --git a/src/abgleich/core/transaction.py b/src/abgleich/core/transaction.py index 9705044..79ea814 100644 --- a/src/abgleich/core/transaction.py +++ b/src/abgleich/core/transaction.py @@ -28,20 +28,16 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -from typing import Callable, Generator, List, Tuple, Union +from typing import Callable, Union -from tabulate import tabulate from typeguard import typechecked -from .abc import CommandABC, TransactionABC, TransactionListABC, TransactionMetaABC -from .i18n import t -from .io import colorize, humanize_size +from .abc import CommandABC, TransactionABC, TransactionMetaABC # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - @typechecked class Transaction(TransactionABC): """ @@ -115,197 +111,3 @@ def run(self): self._complete = True if self._changed is not None: self._changed() - - -MetaTypes = Union[str, int, float] -MetaNoneTypes = Union[str, int, float, None] - - -@typechecked -class TransactionMeta(TransactionMetaABC): - """ - Immutable. - """ - - def __init__(self, **kwargs: MetaTypes): - - self._meta = kwargs - - def __getitem__(self, key: str) -> MetaTypes: - - return self._meta[key] - - def __len__(self) -> int: - - return len(self._meta) - - def get(self, key: str) -> MetaNoneTypes: - - return self._meta.get(key, None) - - def keys(self) -> Generator[str, None, None]: - - return (key for key in self._meta.keys()) - - -TransactionIterableTypes = Union[ - Generator[TransactionABC, None, None], - List[TransactionABC], - Tuple[TransactionABC], -] - - -@typechecked -class TransactionList(TransactionListABC): - """ - Mutable. - """ - - def __init__(self): - - self._transactions = [] - self._changed = None - - def __len__(self) -> int: - - return len(self._transactions) - - def __getitem__(self, index: int) -> TransactionABC: - - return self._transactions[index] - - @property - def changed(self) -> Union[None, Callable]: - - return self._changed - - @changed.setter - def changed(self, value: Union[None, Callable]): - - self._changed = value - - @property - def table_columns(self) -> List[str]: - - headers = set() - for transaction in self._transactions: - keys = list(transaction.meta.keys()) - assert t("type") in keys - headers.update(keys) - headers = list(headers) - headers.sort() - - if len(headers) == 0: - return headers - - type_index = headers.index(t("type")) - if type_index != 0: - headers.pop(type_index) - headers.insert(0, t("type")) - - return headers - - @property - def table_rows(self) -> List[str]: - - return [f'{t("transaction"):s} #{index:d}' for index in range(1, len(self) + 1)] - - def append(self, transaction: TransactionABC): - - self._transactions.append(transaction) - if self._changed is not None: - self._link_transaction(transaction) - - def extend(self, transactions: TransactionIterableTypes): - - transactions = list(transactions) - self._transactions.extend(transactions) - if self._changed is not None: - for transaction in transactions: - self._link_transaction(transaction) - - def clear(self): - - self._transactions.clear() - self._changed() - - def _link_transaction(self, transaction: TransactionABC): - - transaction.changed = lambda: self._changed( - self._transactions.index(transaction) - ) - transaction.changed() - - def print_table(self): - - if len(self) == 0: - return - - table_columns = self.table_columns - colalign = self._table_colalign(table_columns) - - table = [ - [ - self._table_format_cell(header, transaction.meta.get(header)) - for header in table_columns - ] - for transaction in self._transactions - ] - - print( - tabulate( - table, - headers=table_columns, - tablefmt="github", - colalign=colalign, - ) - ) - - @staticmethod - def _table_format_cell(header: str, value: MetaNoneTypes) -> str: - - FORMAT = { - t("written"): lambda v: humanize_size(v, add_color=True), - } - - return FORMAT.get(header, str)(value) - - @staticmethod - def _table_colalign(headers: List[str]) -> List[str]: - - RIGHT = (t("written"),) - DECIMAL = tuple() - - colalign = [] - for header in headers: - if header in RIGHT: - colalign.append("right") - elif header in DECIMAL: - colalign.append("decimal") - else: - colalign.append("left") - - return colalign - - def run(self): - - for transaction in self._transactions: - - print( - f'({colorize(transaction.meta[t("type")], "white"):s}) ' - f'{colorize(str(transaction.command), "yellow"):s}' - ) - - assert not transaction.running - assert not transaction.complete - - transaction.run() - - assert not transaction.running - assert transaction.complete - - if transaction.error is not None: - print(colorize(t("FAILED"), "red")) - raise transaction.error - else: - print(colorize(t("OK"), "green")) diff --git a/src/abgleich/core/transactionlist.py b/src/abgleich/core/transactionlist.py new file mode 100644 index 0000000..787bb94 --- /dev/null +++ b/src/abgleich/core/transactionlist.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- + +""" + +ABGLEICH +zfs sync tool +https://github.com/pleiszenburg/abgleich + + src/abgleich/core/transactionlist.py: ZFS transaction list + + Copyright (C) 2019-2020 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/abgleich/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from typing import Callable, Generator, List, Tuple, Union + +from tabulate import tabulate +from typeguard import typechecked + +from .abc import TransactionABC, TransactionListABC +from .i18n import t +from .io import colorize, humanize_size +from .transactionmeta import TransactionMetaTypes + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TYPES +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +TransactionIterableTypes = Union[ + Generator[TransactionABC, None, None], + List[TransactionABC], + Tuple[TransactionABC], +] + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +@typechecked +class TransactionList(TransactionListABC): + """ + Mutable. + """ + + def __init__(self): + + self._transactions = [] + self._changed = None + + def __len__(self) -> int: + + return len(self._transactions) + + def __getitem__(self, index: int) -> TransactionABC: + + return self._transactions[index] + + @property + def changed(self) -> Union[None, Callable]: + + return self._changed + + @changed.setter + def changed(self, value: Union[None, Callable]): + + self._changed = value + + @property + def table_columns(self) -> List[str]: + + headers = set() + for transaction in self._transactions: + keys = list(transaction.meta.keys()) + assert t("type") in keys + headers.update(keys) + headers = list(headers) + headers.sort() + + if len(headers) == 0: + return headers + + type_index = headers.index(t("type")) + if type_index != 0: + headers.pop(type_index) + headers.insert(0, t("type")) + + return headers + + @property + def table_rows(self) -> List[str]: + + return [f'{t("transaction"):s} #{index:d}' for index in range(1, len(self) + 1)] + + def append(self, transaction: TransactionABC): + + self._transactions.append(transaction) + if self._changed is not None: + self._link_transaction(transaction) + + def extend(self, transactions: TransactionIterableTypes): + + transactions = list(transactions) + self._transactions.extend(transactions) + if self._changed is not None: + for transaction in transactions: + self._link_transaction(transaction) + + def clear(self): + + self._transactions.clear() + self._changed() + + def _link_transaction(self, transaction: TransactionABC): + + transaction.changed = lambda: self._changed( + self._transactions.index(transaction) + ) + transaction.changed() + + def print_table(self): + + if len(self) == 0: + return + + table_columns = self.table_columns + colalign = self._table_colalign(table_columns) + + table = [ + [ + self._table_format_cell(header, transaction.meta.get(header)) + for header in table_columns + ] + for transaction in self._transactions + ] + + print( + tabulate( + table, + headers=table_columns, + tablefmt="github", + colalign=colalign, + ) + ) + + @staticmethod + def _table_format_cell(header: str, value: Union[TransactionMetaTypes, None]) -> str: + + FORMAT = { + t("written"): lambda v: humanize_size(v, add_color=True), + } + + return FORMAT.get(header, str)(value) + + @staticmethod + def _table_colalign(headers: List[str]) -> List[str]: + + RIGHT = (t("written"),) + DECIMAL = tuple() + + colalign = [] + for header in headers: + if header in RIGHT: + colalign.append("right") + elif header in DECIMAL: + colalign.append("decimal") + else: + colalign.append("left") + + return colalign + + def run(self): + + for transaction in self._transactions: + + print( + f'({colorize(transaction.meta[t("type")], "white"):s}) ' + f'{colorize(str(transaction.command), "yellow"):s}' + ) + + assert not transaction.running + assert not transaction.complete + + transaction.run() + + assert not transaction.running + assert transaction.complete + + if transaction.error is not None: + print(colorize(t("FAILED"), "red")) + raise transaction.error + else: + print(colorize(t("OK"), "green")) diff --git a/src/abgleich/core/transactionmeta.py b/src/abgleich/core/transactionmeta.py new file mode 100644 index 0000000..1f6a974 --- /dev/null +++ b/src/abgleich/core/transactionmeta.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +""" + +ABGLEICH +zfs sync tool +https://github.com/pleiszenburg/abgleich + + src/abgleich/core/transactionmeta.py: ZFS transaction meta + + Copyright (C) 2019-2020 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/abgleich/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from typing import Generator, Union + +from typeguard import typechecked + +from .abc import TransactionMetaABC + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TYPES +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +TransactionMetaTypes = Union[str, int, float] + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASS +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +@typechecked +class TransactionMeta(TransactionMetaABC): + """ + Immutable. + """ + + def __init__(self, **kwargs: TransactionMetaTypes): + + self._meta = kwargs + + def __getitem__(self, key: str) -> TransactionMetaTypes: + + return self._meta[key] + + def __len__(self) -> int: + + return len(self._meta) + + def get(self, key: str) -> Union[TransactionMetaTypes, None]: + + return self._meta.get(key, None) + + def keys(self) -> Generator[str, None, None]: + + return (key for key in self._meta.keys()) From 9fc91747cef79e69a1a78d4f19b1ce63bccf2511 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 19:06:13 +0200 Subject: [PATCH 122/155] fixed imports --- src/abgleich/core/dataset.py | 3 ++- src/abgleich/core/snapshot.py | 3 ++- src/abgleich/core/zpool.py | 2 +- src/abgleich/gui/wizard.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index 69e3d97..1e15b5c 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -44,7 +44,8 @@ from .i18n import t from .lib import root from .property import Property -from .transaction import Transaction, TransactionMeta +from .transaction import Transaction +from .transactionmeta import TransactionMeta from .snapshot import Snapshot # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 15d07a3..09aafb5 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -37,7 +37,8 @@ from .i18n import t from .lib import root from .property import Property -from .transaction import Transaction, TransactionMeta +from .transaction import Transaction +from .transactionmeta import TransactionMeta # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 010bb9c..2c3019a 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -51,7 +51,7 @@ from .io import colorize, humanize_size from .lib import join, root from .property import Property -from .transaction import TransactionList +from .transactionlist import TransactionList # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS diff --git a/src/abgleich/gui/wizard.py b/src/abgleich/gui/wizard.py index e09693b..1db28b1 100644 --- a/src/abgleich/gui/wizard.py +++ b/src/abgleich/gui/wizard.py @@ -37,7 +37,7 @@ from .transaction import TransactionListModel from .wizard_base import WizardUiBase from ..core.abc import ConfigABC -from ..core.transaction import TransactionList +from ..core.transactionlist import TransactionList from ..core.i18n import t from ..core.zpool import Zpool from .. import __version__ From bd9631fe9acdda95490dcb0ee1a106af906a07a8 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 19:07:19 +0200 Subject: [PATCH 123/155] black --- src/abgleich/core/dataset.py | 2 +- src/abgleich/core/transaction.py | 1 + src/abgleich/core/transactionlist.py | 5 ++++- src/abgleich/core/transactionmeta.py | 1 + src/abgleich/core/zpool.py | 26 ++++++++++++++++++++------ 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index 1e15b5c..7a7fa81 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -150,7 +150,7 @@ def changed(self) -> bool: @property def ignore(self) -> bool: - return self._subname in self._config['ignore'].value + return self._subname in self._config["ignore"].value @property def name(self) -> str: diff --git a/src/abgleich/core/transaction.py b/src/abgleich/core/transaction.py index 79ea814..d4dd252 100644 --- a/src/abgleich/core/transaction.py +++ b/src/abgleich/core/transaction.py @@ -38,6 +38,7 @@ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + @typechecked class Transaction(TransactionABC): """ diff --git a/src/abgleich/core/transactionlist.py b/src/abgleich/core/transactionlist.py index 787bb94..cf71d92 100644 --- a/src/abgleich/core/transactionlist.py +++ b/src/abgleich/core/transactionlist.py @@ -52,6 +52,7 @@ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + @typechecked class TransactionList(TransactionListABC): """ @@ -159,7 +160,9 @@ def print_table(self): ) @staticmethod - def _table_format_cell(header: str, value: Union[TransactionMetaTypes, None]) -> str: + def _table_format_cell( + header: str, value: Union[TransactionMetaTypes, None] + ) -> str: FORMAT = { t("written"): lambda v: humanize_size(v, add_color=True), diff --git a/src/abgleich/core/transactionmeta.py b/src/abgleich/core/transactionmeta.py index 1f6a974..cae4ed6 100644 --- a/src/abgleich/core/transactionmeta.py +++ b/src/abgleich/core/transactionmeta.py @@ -44,6 +44,7 @@ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + @typechecked class TransactionMeta(TransactionMetaABC): """ diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 2c3019a..b69ef56 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -302,7 +302,7 @@ def print_table(self): table = [] for dataset in self._datasets: - table.append(self._table_row(dataset, ignore = dataset.ignore)) + table.append(self._table_row(dataset, ignore=dataset.ignore)) for snapshot in dataset.snapshots: table.append(self._table_row(snapshot)) @@ -320,7 +320,9 @@ def print_table(self): ) @staticmethod - def _table_row(entity: Union[SnapshotABC, DatasetABC], ignore: bool = False) -> List[str]: + def _table_row( + entity: Union[SnapshotABC, DatasetABC], ignore: bool = False + ) -> List[str]: color = "white" if not ignore else "red" @@ -339,7 +341,11 @@ def print_comparison_table(self, other: ZpoolABC): table = [] for dataset_item in zpool_comparison.merged: - table.append(self._comparison_table_row(dataset_item, ignore = dataset_item.get_item().ignore)) + table.append( + self._comparison_table_row( + dataset_item, ignore=dataset_item.get_item().ignore + ) + ) if dataset_item.complete: dataset_comparison = ComparisonDataset.from_datasets( dataset_item.a, dataset_item.b @@ -353,7 +359,11 @@ def print_comparison_table(self, other: ZpoolABC): None, dataset_item.b ) for snapshot_item in dataset_comparison.merged: - table.append(self._comparison_table_row(snapshot_item, ignore = dataset_item.get_item().ignore)) + table.append( + self._comparison_table_row( + snapshot_item, ignore=dataset_item.get_item().ignore + ) + ) print( tabulate( @@ -364,10 +374,14 @@ def print_comparison_table(self, other: ZpoolABC): ) @staticmethod - def _comparison_table_row(item: ComparisonItemABC, ignore: bool = False) -> List[str]: + def _comparison_table_row( + item: ComparisonItemABC, ignore: bool = False + ) -> List[str]: color1, color2 = ("white", "grey") if not ignore else ("red", "yellow") - colorR, colorG, colorB = ("red", "green", "blue") if not ignore else ("grey", "grey", "grey") + colorR, colorG, colorB = ( + ("red", "green", "blue") if not ignore else ("grey", "grey", "grey") + ) symbol = "X" From 549e0be3615d765e9d34b6c0a6fed7e93cfb9e87 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 20:23:42 +0200 Subject: [PATCH 124/155] list can be initialized and added --- src/abgleich/core/transactionlist.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/abgleich/core/transactionlist.py b/src/abgleich/core/transactionlist.py index cf71d92..c5e416e 100644 --- a/src/abgleich/core/transactionlist.py +++ b/src/abgleich/core/transactionlist.py @@ -59,15 +59,19 @@ class TransactionList(TransactionListABC): Mutable. """ - def __init__(self): + def __init__(self, *transactions: TransactionABC): - self._transactions = [] + self._transactions = list(transactions) self._changed = None def __len__(self) -> int: return len(self._transactions) + def __add__(self, other: TransactionListABC) -> TransactionListABC: + + return type(self)(*(tuple(self.transactions) + tuple(other.transactions))) + def __getitem__(self, index: int) -> TransactionABC: return self._transactions[index] @@ -108,6 +112,11 @@ def table_rows(self) -> List[str]: return [f'{t("transaction"):s} #{index:d}' for index in range(1, len(self) + 1)] + @property + def transactions(self) -> Generator[TransactionABC, None, None]: + + return (transaction for transaction in self._transactions) + def append(self, transaction: TransactionABC): self._transactions.append(transaction) From 2fb883eadface7188229d667fbbbfeee565429d5 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 20:27:27 +0200 Subject: [PATCH 125/155] transaction lists only work on transaction lists --- src/abgleich/core/transactionlist.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/abgleich/core/transactionlist.py b/src/abgleich/core/transactionlist.py index c5e416e..8c7bb05 100644 --- a/src/abgleich/core/transactionlist.py +++ b/src/abgleich/core/transactionlist.py @@ -28,7 +28,7 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -from typing import Callable, Generator, List, Tuple, Union +from typing import Callable, Generator, List, Union from tabulate import tabulate from typeguard import typechecked @@ -38,16 +38,6 @@ from .io import colorize, humanize_size from .transactionmeta import TransactionMetaTypes -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# TYPES -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -TransactionIterableTypes = Union[ - Generator[TransactionABC, None, None], - List[TransactionABC], - Tuple[TransactionABC], -] - # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -123,9 +113,9 @@ def append(self, transaction: TransactionABC): if self._changed is not None: self._link_transaction(transaction) - def extend(self, transactions: TransactionIterableTypes): + def extend(self, transactions: TransactionListABC): - transactions = list(transactions) + transactions = list(transactions.transactions) self._transactions.extend(transactions) if self._changed is not None: for transaction in transactions: From 96dfe8efd5762d7d570018a4af0d98f29f1c64d0 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 20:28:20 +0200 Subject: [PATCH 126/155] fix: conditional signal --- src/abgleich/core/transactionlist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/abgleich/core/transactionlist.py b/src/abgleich/core/transactionlist.py index 8c7bb05..4ccb8d1 100644 --- a/src/abgleich/core/transactionlist.py +++ b/src/abgleich/core/transactionlist.py @@ -124,7 +124,8 @@ def extend(self, transactions: TransactionListABC): def clear(self): self._transactions.clear() - self._changed() + if self._changed is not None: + self._changed() def _link_transaction(self, transaction: TransactionABC): From 16b32d89cfbc787c3ef04cebcd9315a6e27c1983 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 20:33:16 +0200 Subject: [PATCH 127/155] fix: double query for snapshot tasks --- src/abgleich/core/zpool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index b69ef56..50bcfff 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -267,7 +267,7 @@ def get_snapshot_transactions(self) -> TransactionListABC: transaction = self._get_snapshot_transactions_from_dataset(dataset) if transaction is None: continue - transactions.append(dataset.get_snapshot_transaction()) + transactions.append(transaction) return transactions From 4fc36d94c112f1be2546c06f6c7a4d4a08599b0c Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 21:02:28 +0200 Subject: [PATCH 128/155] snap runs on new transaction list api --- src/abgleich/core/dataset.py | 12 +++++++----- src/abgleich/core/zpool.py | 17 +++++++---------- src/abgleich/gui/wizard.py | 3 +-- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index 7a7fa81..b355d4a 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -39,12 +39,13 @@ from typeguard import typechecked -from .abc import ConfigABC, DatasetABC, PropertyABC, TransactionABC, SnapshotABC +from .abc import ConfigABC, DatasetABC, PropertyABC, SnapshotABC, TransactionListABC from .command import Command from .i18n import t from .lib import root from .property import Property from .transaction import Transaction +from .transactionlist import TransactionList from .transactionmeta import TransactionMeta from .snapshot import Snapshot @@ -172,11 +173,10 @@ def root(self) -> str: return self._root - def get_snapshot_transaction(self) -> TransactionABC: + def get_snapshot_transactions(self) -> TransactionListABC: snapshot_name = self._new_snapshot_name() - - return Transaction( + transactions = TransactionList(Transaction( meta=TransactionMeta( **{ t("type"): t("snapshot"), @@ -188,7 +188,9 @@ def get_snapshot_transaction(self) -> TransactionABC: command=Command.from_list( ["zfs", "snapshot", f"{self._name:s}@{snapshot_name:s}"] ).on_side(side=self._side, config=self._config), - ) + )) + # TODO append namespace transaction + return transactions def _new_snapshot_name(self) -> str: diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 50bcfff..be15a6e 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -264,16 +264,13 @@ def get_snapshot_transactions(self) -> TransactionListABC: transactions = TransactionList() for dataset in self._datasets: - transaction = self._get_snapshot_transactions_from_dataset(dataset) - if transaction is None: - continue - transactions.append(transaction) + transactions.extend(self._get_snapshot_transactions_from_dataset(dataset)) return transactions def generate_snapshot_transactions( self, - ) -> Generator[Tuple[int, Union[None, TransactionABC]], None, None]: + ) -> Generator[Tuple[int, Union[None, TransactionListABC]], None, None]: assert self._side == "source" @@ -284,19 +281,19 @@ def generate_snapshot_transactions( def _get_snapshot_transactions_from_dataset( self, dataset: DatasetABC - ) -> Union[None, TransactionABC]: + ) -> TransactionListABC: if dataset.ignore: - return + return TransactionList() if ( dataset.get("mountpoint").value is None and dataset["type"].value == "filesystem" ): - return + return TransactionList() if not dataset.changed: # TODO namespace - return + return TransactionList() - return dataset.get_snapshot_transaction() + return dataset.get_snapshot_transactions() def print_table(self): diff --git a/src/abgleich/gui/wizard.py b/src/abgleich/gui/wizard.py index 1db28b1..9420faa 100644 --- a/src/abgleich/gui/wizard.py +++ b/src/abgleich/gui/wizard.py @@ -215,8 +215,7 @@ def _prepare_snap(self) -> bool: QApplication.processEvents() for number, transaction in gen: - if transaction is not None: - self._transactions.append(transaction) + self._transactions.extend(transaction) self._ui["progress"].setValue(number + 1) QApplication.processEvents() From 7a84c219cd0e313c0e366a090cad078da3108ae8 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 21:03:29 +0200 Subject: [PATCH 129/155] black --- src/abgleich/core/dataset.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index b355d4a..86e372f 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -176,19 +176,21 @@ def root(self) -> str: def get_snapshot_transactions(self) -> TransactionListABC: snapshot_name = self._new_snapshot_name() - transactions = TransactionList(Transaction( - meta=TransactionMeta( - **{ - t("type"): t("snapshot"), - t("dataset_subname"): self._subname, - t("snapshot_name"): snapshot_name, - t("written"): self._properties["written"].value, - } - ), - command=Command.from_list( - ["zfs", "snapshot", f"{self._name:s}@{snapshot_name:s}"] - ).on_side(side=self._side, config=self._config), - )) + transactions = TransactionList( + Transaction( + meta=TransactionMeta( + **{ + t("type"): t("snapshot"), + t("dataset_subname"): self._subname, + t("snapshot_name"): snapshot_name, + t("written"): self._properties["written"].value, + } + ), + command=Command.from_list( + ["zfs", "snapshot", f"{self._name:s}@{snapshot_name:s}"] + ).on_side(side=self._side, config=self._config), + ) + ) # TODO append namespace transaction return transactions From 077da7ea31d047178e2e7ffda8a0d8f62c3d3353 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 21:14:20 +0200 Subject: [PATCH 130/155] backup runs on new transaction list api --- src/abgleich/core/snapshot.py | 11 ++++++----- src/abgleich/core/zpool.py | 36 ++++++++++++++--------------------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 09aafb5..d739cfa 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -32,12 +32,13 @@ from typeguard import typechecked -from .abc import ConfigABC, PropertyABC, SnapshotABC, TransactionABC +from .abc import ConfigABC, PropertyABC, SnapshotABC, TransactionABC, TransactionListABC from .command import Command from .i18n import t from .lib import root from .property import Property from .transaction import Transaction +from .transactionlist import TransactionList from .transactionmeta import TransactionMeta # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -109,11 +110,11 @@ def get_cleanup_transaction(self) -> TransactionABC: ).on_side(side=self._side, config=self._config), ) - def get_backup_transaction( + def get_backup_transactions( self, source_dataset: str, target_dataset: str, - ) -> TransactionABC: + ) -> TransactionListABC: assert self._side == "source" @@ -149,7 +150,7 @@ def get_backup_transaction( side="target", config=self._config ) - return Transaction( + return TransactionList(Transaction( meta=TransactionMeta( **{ t("type"): t("transfer_snapshot") @@ -161,7 +162,7 @@ def get_backup_transaction( } ), command=command, - ) + )) @property def name(self) -> str: diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index be15a6e..d6f8e2c 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -183,26 +183,16 @@ def get_backup_transactions( transactions = TransactionList() for dataset_item in zpool_comparison.merged: - backup_transactions = self._get_backup_transactions_from_datasetitem( + transactions.extend(self._get_backup_transactions_from_datasetitem( other, dataset_item - ) - if backup_transactions is None: - continue - transactions.extend(backup_transactions) + )) return transactions def generate_backup_transactions( self, other: ZpoolABC, - ) -> Generator[ - Tuple[ - int, - Union[None, Union[None, Generator[TransactionABC, None, None]]], - ], - None, - None, - ]: + ) -> Generator[Tuple[int, Union[None, TransactionListABC]], None, None]: assert self.side == "source" assert other.side == "target" @@ -220,12 +210,12 @@ def _get_backup_transactions_from_datasetitem( self, other: ZpoolABC, dataset_item: ComparisonItemABC, - ) -> Union[None, Generator[TransactionABC, None, None]]: + ) -> TransactionListABC: if dataset_item.get_item().ignore: - return + return TransactionList() if dataset_item.a is None: - return + return TransactionList() if dataset_item.b is None: snapshots = list(dataset_item.a.snapshots) @@ -236,7 +226,7 @@ def _get_backup_transactions_from_datasetitem( snapshots = dataset_comparison.a_disjoint_head if len(snapshots) == 0: - return + return TransactionList() source_dataset = ( self.root @@ -249,13 +239,15 @@ def _get_backup_transactions_from_datasetitem( else join(other.root, dataset_item.a.subname) ) - return ( - snapshot.get_backup_transaction( + transactions = TransactionList() + + for snapshot in snapshots: + transactions.extend(snapshot.get_backup_transactions( source_dataset, target_dataset, - ) - for snapshot in snapshots - ) + )) + + return transactions def get_snapshot_transactions(self) -> TransactionListABC: From 4687acb0b92eeab227dc699f64444161081af7c6 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 21:15:00 +0200 Subject: [PATCH 131/155] prepare runs on new transaction list api --- src/abgleich/gui/wizard.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/abgleich/gui/wizard.py b/src/abgleich/gui/wizard.py index 9420faa..37ee463 100644 --- a/src/abgleich/gui/wizard.py +++ b/src/abgleich/gui/wizard.py @@ -233,8 +233,7 @@ def _prepare(self, action: str) -> bool: QApplication.processEvents() for number, transactions in gen: - if transactions is not None: - self._transactions.extend(transactions) + self._transactions.extend(transactions) self._ui["progress"].setValue(number + 1) QApplication.processEvents() From a4d0f4e8a4a88cbf1425a11cea36f62c189599aa Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 21:23:54 +0200 Subject: [PATCH 132/155] cleanup runs on new transaction list api --- src/abgleich/core/snapshot.py | 6 +++--- src/abgleich/core/zpool.py | 30 ++++++++++++------------------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index d739cfa..f5ed7f3 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -95,9 +95,9 @@ def get( Property(key, None, None) if default is None else default, ) - def get_cleanup_transaction(self) -> TransactionABC: + def get_cleanup_transactions(self) -> TransactionListABC: - return Transaction( + return TransactionList(Transaction( meta=TransactionMeta( **{ t("type"): t("cleanup_snapshot"), @@ -108,7 +108,7 @@ def get_cleanup_transaction(self) -> TransactionABC: command=Command.from_list( ["zfs", "destroy", f"{self._parent:s}@{self._name:s}"] ).on_side(side=self._side, config=self._config), - ) + )) def get_backup_transactions( self, diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index d6f8e2c..2d10849 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -111,25 +111,14 @@ def get_cleanup_transactions( transactions = TransactionList() for dataset_item in zpool_comparison.merged: - - cleanup_transactions = self._get_cleanup_from_datasetitem(dataset_item) - if cleanup_transactions is None: - continue - transactions.extend(cleanup_transactions) + transactions.extend(self._get_cleanup_from_datasetitem(dataset_item)) return transactions def generate_cleanup_transactions( self, other: ZpoolABC, - ) -> Generator[ - Tuple[ - int, - Union[None, Union[None, Generator[TransactionABC, None, None]]], - ], - None, - None, - ]: + ) -> Generator[Tuple[int, Union[None, TransactionListABC]], None, None]: assert self.side != other.side assert self.side in ("source", "target") @@ -145,14 +134,14 @@ def generate_cleanup_transactions( def _get_cleanup_from_datasetitem( self, dataset_item: ComparisonItemABC, - ) -> Union[None, Generator[TransactionABC, None, None]]: + ) -> TransactionListABC: if dataset_item.get_item().ignore: - return + return TransactionList() if dataset_item.a is None or dataset_item.b is None: - return + return TransactionList() if self.side == "target" and self._config["keep_backlog"].value == True: - return + return TransactionList() dataset_comparison = ComparisonDataset.from_datasets( dataset_item.a, dataset_item.b @@ -169,7 +158,12 @@ def _get_cleanup_from_datasetitem( keep_backlog = -self._config["keep_backlog"].value snapshots = dataset_comparison.a_disjoint_tail[:keep_backlog] - return (snapshot.get_cleanup_transaction() for snapshot in snapshots) + transactions = TransactionList() + + for snapshot in snapshots: + transactions.extend(snapshot.get_cleanup_transactions()) + + return transactions def get_backup_transactions( self, From afdcac1569ce18c11e0c4af1175b46a51443a9ba Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Sat, 29 Aug 2020 21:26:38 +0200 Subject: [PATCH 133/155] black --- src/abgleich/core/snapshot.py | 54 +++++++++++++++++++---------------- src/abgleich/core/zpool.py | 16 ++++++----- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index f5ed7f3..525d20c 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -97,18 +97,20 @@ def get( def get_cleanup_transactions(self) -> TransactionListABC: - return TransactionList(Transaction( - meta=TransactionMeta( - **{ - t("type"): t("cleanup_snapshot"), - t("snapshot_subparent"): self._subparent, - t("snapshot_name"): self._name, - } - ), - command=Command.from_list( - ["zfs", "destroy", f"{self._parent:s}@{self._name:s}"] - ).on_side(side=self._side, config=self._config), - )) + return TransactionList( + Transaction( + meta=TransactionMeta( + **{ + t("type"): t("cleanup_snapshot"), + t("snapshot_subparent"): self._subparent, + t("snapshot_name"): self._name, + } + ), + command=Command.from_list( + ["zfs", "destroy", f"{self._parent:s}@{self._name:s}"] + ).on_side(side=self._side, config=self._config), + ) + ) def get_backup_transactions( self, @@ -150,19 +152,21 @@ def get_backup_transactions( side="target", config=self._config ) - return TransactionList(Transaction( - meta=TransactionMeta( - **{ - t("type"): t("transfer_snapshot") - if ancestor is None - else t("transfer_snapshot_incremental"), - t("snapshot_subparent"): self._subparent, - t("ancestor_name"): "" if ancestor is None else ancestor.name, - t("snapshot_name"): self.name, - } - ), - command=command, - )) + return TransactionList( + Transaction( + meta=TransactionMeta( + **{ + t("type"): t("transfer_snapshot") + if ancestor is None + else t("transfer_snapshot_incremental"), + t("snapshot_subparent"): self._subparent, + t("ancestor_name"): "" if ancestor is None else ancestor.name, + t("snapshot_name"): self.name, + } + ), + command=command, + ) + ) @property def name(self) -> str: diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 2d10849..5c9dc33 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -177,9 +177,9 @@ def get_backup_transactions( transactions = TransactionList() for dataset_item in zpool_comparison.merged: - transactions.extend(self._get_backup_transactions_from_datasetitem( - other, dataset_item - )) + transactions.extend( + self._get_backup_transactions_from_datasetitem(other, dataset_item) + ) return transactions @@ -236,10 +236,12 @@ def _get_backup_transactions_from_datasetitem( transactions = TransactionList() for snapshot in snapshots: - transactions.extend(snapshot.get_backup_transactions( - source_dataset, - target_dataset, - )) + transactions.extend( + snapshot.get_backup_transactions( + source_dataset, + target_dataset, + ) + ) return transactions From 5a022aaee154e3c11bb62830305426ca687aee53 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 9 Sep 2020 19:00:43 +0200 Subject: [PATCH 134/155] new config parameter: tagging snapshots --- src/abgleich/core/configspec.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/abgleich/core/configspec.py b/src/abgleich/core/configspec.py index d49dfe2..8fc818b 100644 --- a/src/abgleich/core/configspec.py +++ b/src/abgleich/core/configspec.py @@ -82,6 +82,11 @@ validate=lambda v: isinstance(v, bool), default=True, ), + ConfigField( + name="compatibility/tagging", + validate=lambda v: isinstance(v, bool), + default=False, + ), ConfigField( name="ssh/compression", validate=lambda v: isinstance(v, bool), From 1072821b598bfca957b1ef1c7206b569b66a9924 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 9 Sep 2020 19:01:28 +0200 Subject: [PATCH 135/155] new snapshots are optionally tagged --- src/abgleich/core/dataset.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index 86e372f..82b0b9c 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -176,6 +176,12 @@ def root(self) -> str: def get_snapshot_transactions(self) -> TransactionListABC: snapshot_name = self._new_snapshot_name() + + command = ["zfs", "snapshot"] + if self._config["compatibility/tagging"].value: + command.extend(["-o", "abgleich:type=backup"]) + command.append(f"{self._name:s}@{snapshot_name:s}") + transactions = TransactionList( Transaction( meta=TransactionMeta( @@ -186,9 +192,9 @@ def get_snapshot_transactions(self) -> TransactionListABC: t("written"): self._properties["written"].value, } ), - command=Command.from_list( - ["zfs", "snapshot", f"{self._name:s}@{snapshot_name:s}"] - ).on_side(side=self._side, config=self._config), + command=Command.from_list(command).on_side( + side=self._side, config=self._config + ), ) ) # TODO append namespace transaction From 29febc77bc1c1cdc5a4cdcfdaa65e1025797f7e8 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 9 Sep 2020 19:05:45 +0200 Subject: [PATCH 136/155] cleanup --- src/abgleich/core/dataset.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index 82b0b9c..cbb7ba1 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -182,7 +182,7 @@ def get_snapshot_transactions(self) -> TransactionListABC: command.extend(["-o", "abgleich:type=backup"]) command.append(f"{self._name:s}@{snapshot_name:s}") - transactions = TransactionList( + return TransactionList( Transaction( meta=TransactionMeta( **{ @@ -197,8 +197,6 @@ def get_snapshot_transactions(self) -> TransactionListABC: ), ) ) - # TODO append namespace transaction - return transactions def _new_snapshot_name(self) -> str: From 0930d761391f4d12fb95df7477101f7abfa6c1ea Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 9 Sep 2020 19:17:54 +0200 Subject: [PATCH 137/155] backup snapshots are optionally tagged on target --- src/abgleich/core/snapshot.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 525d20c..677079e 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -152,7 +152,7 @@ def get_backup_transactions( side="target", config=self._config ) - return TransactionList( + transactions = TransactionList( Transaction( meta=TransactionMeta( **{ @@ -168,6 +168,29 @@ def get_backup_transactions( ) ) + if self._config["compatibility/tagging"].value: + transactions.append( + Transaction( + meta=TransactionMeta( + **{ + t("type"): t("tag_snapshot"), + t("snapshot_subparent"): self._subparent, + t("snapshot_name"): self.name, + } + ), + command=Command.from_list( + [ + "zfs", + "set", + "abgleich:type=backup", + f"{target_dataset:s}@{self.name:s}", + ] + ).on_side(side="target", config=self._config), + ) + ) + + return transactions + @property def name(self) -> str: From 6aad5291117669f0c7628a2cda8a70e2e0bc1308 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 9 Sep 2020 20:06:10 +0200 Subject: [PATCH 138/155] src can be None; values and src can be exported as strings --- src/abgleich/core/property.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/abgleich/core/property.py b/src/abgleich/core/property.py index c14495c..1a13e61 100644 --- a/src/abgleich/core/property.py +++ b/src/abgleich/core/property.py @@ -55,7 +55,7 @@ def __init__( self, name: str, value: PropertyTypes, - src: PropertyTypes, + src: PropertyTypes = None, ): self._name = name @@ -70,12 +70,20 @@ def name(self) -> str: def value(self) -> PropertyTypes: return self._value + @property + def value_export(self) -> str: + return self._export(self._value) + @property def src(self) -> PropertyTypes: return self._src + @property + def src_export(self) -> str: + return self._export(self._src) + @classmethod - def _convert(cls, value: str) -> PropertyTypes: + def _import(cls, value: str) -> PropertyTypes: value = value.strip() @@ -92,11 +100,15 @@ def _convert(cls, value: str) -> PropertyTypes: return value + def _export(self, value: PropertyTypes) -> str: + + return "-" if value is None else str(value) # TODO improve! + @classmethod def from_params(cls, name, value, src) -> PropertyABC: return cls( name=name, - value=cls._convert(value), - src=cls._convert(src), + value=cls._import(value), + src=cls._import(src), ) From 73b05ab85fbeec22a7181c0ff63ff0d30fcdea7f Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 9 Sep 2020 20:07:47 +0200 Subject: [PATCH 139/155] classmethod for generating set_property transactions --- src/abgleich/core/transaction.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/abgleich/core/transaction.py b/src/abgleich/core/transaction.py index d4dd252..64413c5 100644 --- a/src/abgleich/core/transaction.py +++ b/src/abgleich/core/transaction.py @@ -32,7 +32,11 @@ from typeguard import typechecked -from .abc import CommandABC, TransactionABC, TransactionMetaABC +from .abc import CommandABC, ConfigABC, PropertyABC, TransactionABC, TransactionMetaABC +from .command import Command +from .i18n import t +from .transactionmeta import TransactionMeta + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS @@ -112,3 +116,29 @@ def run(self): self._complete = True if self._changed is not None: self._changed() + + @classmethod + def set_property( + cls, + item: str, + property: PropertyABC, + side: str, + config: ConfigABC, + ) -> TransactionABC: + + return cls( + meta=TransactionMeta( + **{ + t("type"): t("set_property"), + t("item"): item, + } + ), + command=Command.from_list( + [ + "zfs", + "set", + f"{property.name:s}={property.value_export:s}", + item, + ] + ).on_side(side=side, config=config), + ) From 99b8124da45d5f587c06f22313d6f2e5e35d6520 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 9 Sep 2020 20:08:17 +0200 Subject: [PATCH 140/155] snapshot uses new set_property transaction --- src/abgleich/core/snapshot.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 677079e..600885b 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -170,22 +170,11 @@ def get_backup_transactions( if self._config["compatibility/tagging"].value: transactions.append( - Transaction( - meta=TransactionMeta( - **{ - t("type"): t("tag_snapshot"), - t("snapshot_subparent"): self._subparent, - t("snapshot_name"): self.name, - } - ), - command=Command.from_list( - [ - "zfs", - "set", - "abgleich:type=backup", - f"{target_dataset:s}@{self.name:s}", - ] - ).on_side(side="target", config=self._config), + Transaction.set_property( + item=f"{target_dataset:s}@{self.name:s}", + property=Property(name="abgleich:type", value="backup"), + side="target", + config=self._config, ) ) From b19f5e82a22eb1462449c6b637b8e36554ced4c8 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 9 Sep 2020 20:26:02 +0200 Subject: [PATCH 141/155] prevent unintentional sharing/exposing of backup datasets on target side with configuration option target_samba_noshare, fixing #4 --- CHANGES.md | 1 + README.md | 10 +++++++++- src/abgleich/core/configspec.py | 5 +++++ src/abgleich/core/zpool.py | 14 ++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1cefc3e..80d79d6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## 0.0.8 (2020-XX-XX) +- FEATURE: Samba can optionally be told to NOT share/expose backup datasets on the target side, see #4. - FEATURE: `ssh`-port on source and target becomes configurable, see #22. - FEATURE: New configuration fields for `source` and `target` each: `processing`. They can carry shell commands for pre- and post-processing of data before and after it is transferred via ssh. This enables the use of e.g. `lzma` or `bzip2` as a custom transfer compression beyond the compression capabilities of `ssh` itself. See #23. - FEATURE: `abgleich clean` can also remove snapshots on `target` but only if they are not part of the current overlap with `source`. The behavior can be controlled via the new `keep_backlog` configuration option, see #24 and #25. diff --git a/README.md b/README.md index 71560f9..0da13d4 100644 --- a/README.md +++ b/README.md @@ -78,9 +78,17 @@ ignore: ssh: compression: no cipher: aes256-gcm@openssh.com +compatibility: + target_samba_noshare: yes ``` -The prefix can be empty on either side. If a `host` is set to `localhost`, the `user` field can be left empty. Both source and target can be remote hosts or localhost at the same time. `include_root` indicates whether `{zpool}{/{prefix}}` should be included in all operations. `keep_snapshots` is an integer and must be greater or equal to `1`. It specifies the number of snapshots that are kept per dataset on the source side when a cleanup operation is triggered. `keep_backlog` is either an integer or a boolean. It specifies if (or how many) snapshots are kept on the target side if the target side be cleaned. Snapshots that are part of the overlap with the source side are never considered for removal. `suffix` contains the name suffix for new snapshots. Setting `always_changed` to `yes` causes `abgleich` to beliefe that all datasets have always changed since the last snapshot, completely ignoring what ZFS actually reports. No diff will be produced & checked for values of `written` lower than `written_threshold`. Checking diffs can be completely deactivated by setting `check_diff` to `no`. `digits` specifies how many digits are used for a decimal number describing the n-th snapshot per dataset per day as part of the name of new snapshots. `ignore` lists stuff underneath the `prefix` which will be ignored by this tool, i.e. no snapshots, backups or cleanups. `ssh` allows to fine-tune the speed of backups. In fast local networks, it is best to set `compression` to `no` because the compression is usually slowing down the transfer. However, for low-bandwidth transmissions, it makes sense to set it to `yes`. For significantly better speed in fast local networks, make sure that both the source and the target system support a common cipher, which is accelerated by [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) on both ends. The `ssh` port can be specified per side via the `port` configuration option, i.e. for source and/or target. Custom pre- and post-processing can be applied after `send` and before `receive` per side via shell commands specified in the `processing` configuration option. This can be useful for a custom transfer compression based on e.g. `lzma` or `bzip2`. +The prefix can be empty on either side. If a `host` is set to `localhost`, the `user` field can be left empty. Both source and target can be remote hosts or localhost at the same time. `include_root` indicates whether `{zpool}{/{prefix}}` should be included in all operations. `keep_snapshots` is an integer and must be greater or equal to `1`. It specifies the number of snapshots that are kept per dataset on the source side when a cleanup operation is triggered. `keep_backlog` is either an integer or a boolean. It specifies if (or how many) snapshots are kept on the target side if the target side be cleaned. Snapshots that are part of the overlap with the source side are never considered for removal. `suffix` contains the name suffix for new snapshots. Setting `always_changed` to `yes` causes `abgleich` to beliefe that all datasets have always changed since the last snapshot, completely ignoring what ZFS actually reports. No diff will be produced & checked for values of `written` lower than `written_threshold`. Checking diffs can be completely deactivated by setting `check_diff` to `no`. `digits` specifies how many digits are used for a decimal number describing the n-th snapshot per dataset per day as part of the name of new snapshots. `ignore` lists stuff underneath the `prefix` which will be ignored by this tool, i.e. no snapshots, backups or cleanups. + +`ssh` allows to fine-tune the speed of backups. In fast local networks, it is best to set `compression` to `no` because the compression is usually slowing down the transfer. However, for low-bandwidth transmissions, it makes sense to set it to `yes`. For significantly better speed in fast local networks, make sure that both the source and the target system support a common cipher, which is accelerated by [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) on both ends. The `ssh` port can be specified per side via the `port` configuration option, i.e. for source and/or target. + +Custom pre- and post-processing can be applied after `send` and before `receive` per side via shell commands specified in the `processing` configuration option (underneath `source` and `target`). This can be useful for a custom transfer compression based on e.g. `lzma` or `bzip2`. + +`compatibility` add options for making `abgleich` more compatible with other tools. If `target_samba_noshare` is active, for all new datasets on the target side the `sharesmb` property will - as part of backup operations - be set to `off`, preventing sharing/exposing backup datasets by accident. ## USAGE diff --git a/src/abgleich/core/configspec.py b/src/abgleich/core/configspec.py index 8fc818b..4c1bdab 100644 --- a/src/abgleich/core/configspec.py +++ b/src/abgleich/core/configspec.py @@ -87,6 +87,11 @@ validate=lambda v: isinstance(v, bool), default=False, ), + ConfigField( + name="compatibility/target_samba_noshare", + validate=lambda v: isinstance(v, bool), + default=False, + ), ConfigField( name="ssh/compression", validate=lambda v: isinstance(v, bool), diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 5c9dc33..0ecfd9a 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -51,6 +51,7 @@ from .io import colorize, humanize_size from .lib import join, root from .property import Property +from .transaction import Transaction from .transactionlist import TransactionList # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -243,6 +244,19 @@ def _get_backup_transactions_from_datasetitem( ) ) + if ( + dataset_item.b is None + and self._config["compatibility/target_samba_noshare"].value + ): + transactions.append( + Transaction.set_property( + item=target_dataset, + property=Property(name="sharesmb", value="off"), + side="target", + config=self._config, + ) + ) + return transactions def get_snapshot_transactions(self) -> TransactionListABC: From e7de6de3c88835e1e96d6fda98a5deb6485f514e Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 9 Sep 2020 20:57:00 +0200 Subject: [PATCH 142/155] link to translation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0da13d4..58bba3e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## SYNOPSIS -`abgleich` is a simple ZFS sync tool. It displays source and target ZFS zpool, dataset and snapshot trees. It creates meaningful snapshots only if datasets have actually been changed. It compares a source zpool tree to a target, backup zpool tree. It pushes backups from a source to a target. It cleanes up older snapshots on the source side if they are present on the target side. It runs on a command line and produces nice, user-friendly, human-readable, colorized output. It also includes a GUI. +[`abgleich`](https://dict.leo.org/englisch-deutsch/abgleich?side=right) is a simple ZFS sync tool. It displays source and target ZFS zpool, dataset and snapshot trees. It creates meaningful snapshots only if datasets have actually been changed. It compares a source zpool tree to a target, backup zpool tree. It pushes backups from a source to a target. It cleanes up older snapshots on the source side if they are present on the target side. It runs on a command line and produces nice, user-friendly, human-readable, colorized output. It also includes a GUI. ## CLI EXAMPLE From eeea1ab48e1bb344d5bf6588d785a114e57bd57e Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 9 Sep 2020 21:05:10 +0200 Subject: [PATCH 143/155] prevent zfs-autosnapshot from generating snapshots on backup datasets on target side with configuration option target_autosnapshot_ignore, fixing #3 --- CHANGES.md | 3 ++- README.md | 3 ++- src/abgleich/core/configspec.py | 5 +++++ src/abgleich/core/zpool.py | 13 +++++++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 80d79d6..f42dce2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,8 @@ ## 0.0.8 (2020-XX-XX) -- FEATURE: Samba can optionally be told to NOT share/expose backup datasets on the target side, see #4. +- FEATURE: `zfs-auto-snapshot` can be told to ignore backup datasets on the target side, see #3. +- FEATURE: `samba` can optionally be told to NOT share/expose backup datasets on the target side, see #4. - FEATURE: `ssh`-port on source and target becomes configurable, see #22. - FEATURE: New configuration fields for `source` and `target` each: `processing`. They can carry shell commands for pre- and post-processing of data before and after it is transferred via ssh. This enables the use of e.g. `lzma` or `bzip2` as a custom transfer compression beyond the compression capabilities of `ssh` itself. See #23. - FEATURE: `abgleich clean` can also remove snapshots on `target` but only if they are not part of the current overlap with `source`. The behavior can be controlled via the new `keep_backlog` configuration option, see #24 and #25. diff --git a/README.md b/README.md index 58bba3e..09d0c4d 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ ssh: cipher: aes256-gcm@openssh.com compatibility: target_samba_noshare: yes + target_autosnapshot_ignore: yes ``` The prefix can be empty on either side. If a `host` is set to `localhost`, the `user` field can be left empty. Both source and target can be remote hosts or localhost at the same time. `include_root` indicates whether `{zpool}{/{prefix}}` should be included in all operations. `keep_snapshots` is an integer and must be greater or equal to `1`. It specifies the number of snapshots that are kept per dataset on the source side when a cleanup operation is triggered. `keep_backlog` is either an integer or a boolean. It specifies if (or how many) snapshots are kept on the target side if the target side be cleaned. Snapshots that are part of the overlap with the source side are never considered for removal. `suffix` contains the name suffix for new snapshots. Setting `always_changed` to `yes` causes `abgleich` to beliefe that all datasets have always changed since the last snapshot, completely ignoring what ZFS actually reports. No diff will be produced & checked for values of `written` lower than `written_threshold`. Checking diffs can be completely deactivated by setting `check_diff` to `no`. `digits` specifies how many digits are used for a decimal number describing the n-th snapshot per dataset per day as part of the name of new snapshots. `ignore` lists stuff underneath the `prefix` which will be ignored by this tool, i.e. no snapshots, backups or cleanups. @@ -88,7 +89,7 @@ The prefix can be empty on either side. If a `host` is set to `localhost`, the ` Custom pre- and post-processing can be applied after `send` and before `receive` per side via shell commands specified in the `processing` configuration option (underneath `source` and `target`). This can be useful for a custom transfer compression based on e.g. `lzma` or `bzip2`. -`compatibility` add options for making `abgleich` more compatible with other tools. If `target_samba_noshare` is active, for all new datasets on the target side the `sharesmb` property will - as part of backup operations - be set to `off`, preventing sharing/exposing backup datasets by accident. +`compatibility` adds options for making `abgleich` more compatible with other tools. If `target_samba_noshare` is active, for all new datasets on the target side the `sharesmb` property will - as part of backup operations - be set to `off`, preventing sharing/exposing backup datasets by accident. If `target_autosnapshot_ignore` is active, for all new datasets on the target side the `com.sun:auto-snapshot` property will - similarly as part of backup operations - be set to `false`, telling `zfs-auto-snapshot` to ignore the dataset. ## USAGE diff --git a/src/abgleich/core/configspec.py b/src/abgleich/core/configspec.py index 4c1bdab..4f04776 100644 --- a/src/abgleich/core/configspec.py +++ b/src/abgleich/core/configspec.py @@ -92,6 +92,11 @@ validate=lambda v: isinstance(v, bool), default=False, ), + ConfigField( + name="compatibility/target_autosnapshot_ignore", + validate=lambda v: isinstance(v, bool), + default=False, + ), ConfigField( name="ssh/compression", validate=lambda v: isinstance(v, bool), diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 0ecfd9a..ba653a0 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -257,6 +257,19 @@ def _get_backup_transactions_from_datasetitem( ) ) + if ( + dataset_item.b is None + and self._config["compatibility/target_autosnapshot_ignore"].value + ): + transactions.append( + Transaction.set_property( + item=target_dataset, + property=Property(name="com.sun:auto-snapshot", value="false"), + side="target", + config=self._config, + ) + ) + return transactions def get_snapshot_transactions(self) -> TransactionListABC: From 2066f3d8191bfa19fdb032f0aa01918c4d071ba3 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 9 Sep 2020 21:26:44 +0200 Subject: [PATCH 144/155] do not set compatibility options for every dataset on target side - use inheritance instead --- README.md | 2 +- src/abgleich/core/zpool.py | 25 +++++++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 09d0c4d..5261042 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ The prefix can be empty on either side. If a `host` is set to `localhost`, the ` Custom pre- and post-processing can be applied after `send` and before `receive` per side via shell commands specified in the `processing` configuration option (underneath `source` and `target`). This can be useful for a custom transfer compression based on e.g. `lzma` or `bzip2`. -`compatibility` adds options for making `abgleich` more compatible with other tools. If `target_samba_noshare` is active, for all new datasets on the target side the `sharesmb` property will - as part of backup operations - be set to `off`, preventing sharing/exposing backup datasets by accident. If `target_autosnapshot_ignore` is active, for all new datasets on the target side the `com.sun:auto-snapshot` property will - similarly as part of backup operations - be set to `false`, telling `zfs-auto-snapshot` to ignore the dataset. +`compatibility` adds options for making `abgleich` more compatible with other tools. If `target_samba_noshare` is active, the `sharesmb` property will - as part of backup operations - be set to `off` for `{zpool}{/{prefix}}` on the target side, preventing sharing/exposing backup datasets by accident. If `target_autosnapshot_ignore` is active, the `com.sun:auto-snapshot` property will - similarly as part of backup operations - be set to `false` for `{zpool}{/{prefix}}` on the target side, telling `zfs-auto-snapshot` to ignore the dataset. ## USAGE diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index ba653a0..3ec0044 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -182,6 +182,8 @@ def get_backup_transactions( self._get_backup_transactions_from_datasetitem(other, dataset_item) ) + transactions.extend(self._get_backup_propery_transactions(other)) + return transactions def generate_backup_transactions( @@ -201,6 +203,9 @@ def generate_backup_transactions( other, dataset_item ) + for transaction in self._get_backup_propery_transactions(other): + yield len(zpool_comparison) - 1, transaction + def _get_backup_transactions_from_datasetitem( self, other: ZpoolABC, @@ -244,26 +249,26 @@ def _get_backup_transactions_from_datasetitem( ) ) - if ( - dataset_item.b is None - and self._config["compatibility/target_samba_noshare"].value - ): + return transactions + + def _get_backup_propery_transactions(self, other: ZpoolABC) -> TransactionListABC: + + transactions = TransactionList() + + if self._config["compatibility/target_samba_noshare"].value: transactions.append( Transaction.set_property( - item=target_dataset, + item=other.root, property=Property(name="sharesmb", value="off"), side="target", config=self._config, ) ) - if ( - dataset_item.b is None - and self._config["compatibility/target_autosnapshot_ignore"].value - ): + if self._config["compatibility/target_autosnapshot_ignore"].value: transactions.append( Transaction.set_property( - item=target_dataset, + item=other.root, property=Property(name="com.sun:auto-snapshot", value="false"), side="target", config=self._config, From 53c5e255eb7d5cb2a26cef0a570957257c4bdcfe Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 9 Sep 2020 21:38:30 +0200 Subject: [PATCH 145/155] black --- src/abgleich/core/property.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abgleich/core/property.py b/src/abgleich/core/property.py index 1a13e61..def84f8 100644 --- a/src/abgleich/core/property.py +++ b/src/abgleich/core/property.py @@ -102,7 +102,7 @@ def _import(cls, value: str) -> PropertyTypes: def _export(self, value: PropertyTypes) -> str: - return "-" if value is None else str(value) # TODO improve! + return "-" if value is None else str(value) # TODO improve! @classmethod def from_params(cls, name, value, src) -> PropertyABC: From 3a16f015ba9972b6364a561b4130a6b8da192a9a Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Wed, 9 Sep 2020 22:35:50 +0200 Subject: [PATCH 146/155] cleanup --- src/abgleich/core/zpool.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index 3ec0044..ca623e5 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -39,7 +39,6 @@ ConfigABC, DatasetABC, SnapshotABC, - TransactionABC, TransactionListABC, ZpoolABC, ) From aa837403a59333ed26910e38c1c8895d14d9bd7d Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 10 Sep 2020 12:27:11 +0200 Subject: [PATCH 147/155] manual --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5261042..4357d88 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,13 @@ source: prefix: host: localhost user: + port: target: zpool: tank_hdd prefix: BACKUP_SOMEMACHINE host: bigdata user: zfsadmin + port: include_root: yes keep_snapshots: 2 keep_backlog: True @@ -83,7 +85,25 @@ compatibility: target_autosnapshot_ignore: yes ``` -The prefix can be empty on either side. If a `host` is set to `localhost`, the `user` field can be left empty. Both source and target can be remote hosts or localhost at the same time. `include_root` indicates whether `{zpool}{/{prefix}}` should be included in all operations. `keep_snapshots` is an integer and must be greater or equal to `1`. It specifies the number of snapshots that are kept per dataset on the source side when a cleanup operation is triggered. `keep_backlog` is either an integer or a boolean. It specifies if (or how many) snapshots are kept on the target side if the target side be cleaned. Snapshots that are part of the overlap with the source side are never considered for removal. `suffix` contains the name suffix for new snapshots. Setting `always_changed` to `yes` causes `abgleich` to beliefe that all datasets have always changed since the last snapshot, completely ignoring what ZFS actually reports. No diff will be produced & checked for values of `written` lower than `written_threshold`. Checking diffs can be completely deactivated by setting `check_diff` to `no`. `digits` specifies how many digits are used for a decimal number describing the n-th snapshot per dataset per day as part of the name of new snapshots. `ignore` lists stuff underneath the `prefix` which will be ignored by this tool, i.e. no snapshots, backups or cleanups. +`zpool` defines the name of the zpools on source and target sides. The `prefix` value defines a "path" to a dataset underneath the `zpool`, so the name of the zpool itself is not part of the `prefix`. The `prefix` can be empty on either side. Prefixes can differ between source and target side. `host` specifies a value used by `ssh`. It does not have to be an actual host name. It can also be an alias from ssh's configuration. If a `host` is set to `localhost`, `ssh` wont be used and the `user` field can be left empty or omitted. Both source and target can be remote hosts or `localhost` at the same time. The `port` parameter specifies a custom `ssh` port. It can be left empty or omitted. `ssh` will then use its defaults or configuration to determine the correct port. + +`include_root` indicates whether `{zpool}{/{prefix}}` should be included in all operations. `keep_snapshots` is an integer and must be greater or equal to `1`. It specifies the number of snapshots that are kept per dataset on the source side when a cleanup operation is triggered. `keep_backlog` is either an integer or a boolean. It specifies if (or how many) snapshots are kept on the target side if the target side is cleaned. Snapshots that are part of the overlap with the source side are never considered for removal. `suffix` contains the name suffix for new snapshots. + +Whether or not snapshots are generated is based on the following sequence of checks: + +- Dataset is ignored: NO +- Dataset has no snapshot: YES +- If the `always_changed` configuration option is set to `yes`: YES +- `written` property of dataset equals `0`: NO +- Dataset is a volume: YES +- If the `written_threshold` configuration is set and the `written` property of dataset is larger than the value of `written_threshold`: YES +- If the `check_diff` configuration option is set to `no`: YES +- If `zfs diff` produces any output relative to the last snapshot: YES +- Otherwise: NO + +Setting `always_changed` to `yes` causes `abgleich` to beliefe that all datasets have always changed since the last snapshot, completely ignoring what ZFS actually reports. No diff will be produced & checked for values of `written` lower than `written_threshold`. Checking diffs can be completely deactivated by setting `check_diff` to `no`. + +`digits` specifies how many digits are used for a decimal number describing the n-th snapshot per dataset per day as part of the name of new snapshots. `ignore` lists stuff underneath the `prefix` which will be ignored by this tool, i.e. no snapshots, backups or cleanups. `ssh` allows to fine-tune the speed of backups. In fast local networks, it is best to set `compression` to `no` because the compression is usually slowing down the transfer. However, for low-bandwidth transmissions, it makes sense to set it to `yes`. For significantly better speed in fast local networks, make sure that both the source and the target system support a common cipher, which is accelerated by [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) on both ends. The `ssh` port can be specified per side via the `port` configuration option, i.e. for source and/or target. From 8fe46d859dff0271409d04d3eb4f51fb6fbcc5de Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 10 Sep 2020 12:51:58 +0200 Subject: [PATCH 148/155] create snapshots of tagging is active and last snapshot in dataset is untagged --- README.md | 1 + src/abgleich/core/dataset.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4357d88..fbcf468 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ Whether or not snapshots are generated is based on the following sequence of che - Dataset is ignored: NO - Dataset has no snapshot: YES - If the `always_changed` configuration option is set to `yes`: YES +- If the `tagging` configuration option underneath `compatibility` is set to yes and the last snapshot of the dataset has not been tagged by `abgleich` as a backup: YES - `written` property of dataset equals `0`: NO - Dataset is a volume: YES - If the `written_threshold` configuration is set and the `written` property of dataset is larger than the value of `written_threshold`: YES diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index cbb7ba1..b6e47e3 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -118,12 +118,15 @@ def get( @property def changed(self) -> bool: - # TODO namespace: if last snapshot is from other namespace, make one ... - if len(self) == 0: return True if self._config["always_changed"].value: return True + + if self._config["compatibility/tagging"].value: + if self._snapshots[-1].get("abgleich:type").value != "backup": + return True + if self._properties["written"].value == 0: return False if self._properties["type"].value == "volume": From 87d071e8d283f247c8cbc2214d6f6afb0933ede9 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 10 Sep 2020 14:31:36 +0200 Subject: [PATCH 149/155] expose config to snapshot merge --- src/abgleich/core/comparisondataset.py | 14 ++++++++++++-- src/abgleich/core/zpool.py | 10 +++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/abgleich/core/comparisondataset.py b/src/abgleich/core/comparisondataset.py index 2d297f2..1d43dee 100644 --- a/src/abgleich/core/comparisondataset.py +++ b/src/abgleich/core/comparisondataset.py @@ -33,7 +33,13 @@ from typeguard import typechecked -from .abc import ComparisonDatasetABC, ComparisonItemABC, DatasetABC, SnapshotABC +from .abc import ( + ComparisonDatasetABC, + ComparisonItemABC, + ConfigABC, + DatasetABC, + SnapshotABC, +) from .comparisonitem import ComparisonItem, ComparisonItemType, ComparisonStrictItemType # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -271,6 +277,7 @@ def _merge_snapshots( cls, items_a: Generator[SnapshotABC, None, None], items_b: Generator[SnapshotABC, None, None], + config: ConfigABC, ) -> List[ComparisonItemABC]: items_a, items_b = list(items_a), list(items_b) @@ -320,6 +327,7 @@ def from_datasets( cls, dataset_a: Union[DatasetABC, None], dataset_b: Union[DatasetABC, None], + config: ConfigABC, ) -> ComparisonDatasetABC: assert dataset_a is not None or dataset_b is not None @@ -340,5 +348,7 @@ def from_datasets( return cls( a=dataset_a, b=dataset_b, - merged=cls._merge_snapshots(dataset_a.snapshots, dataset_b.snapshots), + merged=cls._merge_snapshots( + dataset_a.snapshots, dataset_b.snapshots, config + ), ) diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index ca623e5..fb43a48 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -144,7 +144,7 @@ def _get_cleanup_from_datasetitem( return TransactionList() dataset_comparison = ComparisonDataset.from_datasets( - dataset_item.a, dataset_item.b + dataset_item.a, dataset_item.b, self._config ) # TODO namespace if self.side == "source": @@ -220,7 +220,7 @@ def _get_backup_transactions_from_datasetitem( snapshots = list(dataset_item.a.snapshots) else: dataset_comparison = ComparisonDataset.from_datasets( - dataset_item.a, dataset_item.b + dataset_item.a, dataset_item.b, self._config ) # TODO namespace snapshots = dataset_comparison.a_disjoint_head @@ -364,15 +364,15 @@ def print_comparison_table(self, other: ZpoolABC): ) if dataset_item.complete: dataset_comparison = ComparisonDataset.from_datasets( - dataset_item.a, dataset_item.b + dataset_item.a, dataset_item.b, self._config ) elif dataset_item.a is not None: dataset_comparison = ComparisonDataset.from_datasets( - dataset_item.a, None + dataset_item.a, None, self._config ) else: dataset_comparison = ComparisonDataset.from_datasets( - None, dataset_item.b + None, dataset_item.b, self._config ) for snapshot_item in dataset_comparison.merged: table.append( From 934acd7f78bc97eca67f0a38101c1d0f9b384bba Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 10 Sep 2020 15:16:23 +0200 Subject: [PATCH 150/155] cleanup --- src/abgleich/core/comparisondataset.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/abgleich/core/comparisondataset.py b/src/abgleich/core/comparisondataset.py index 1d43dee..c701e72 100644 --- a/src/abgleich/core/comparisondataset.py +++ b/src/abgleich/core/comparisondataset.py @@ -272,6 +272,14 @@ def _test_names(items: List[ComparisonItemABC]): if item.a.name != item.b.name: raise ValueError("inconsistent snapshot names") + @staticmethod + def _find_name(items: List[SnapshotABC], name: str) -> Union[int, None]: + + return next( + (index for (index, item) in enumerate(items) if item.name == name), + None, # if nothing is found, return None + ) + @classmethod def _merge_snapshots( cls, @@ -282,21 +290,19 @@ def _merge_snapshots( items_a, items_b = list(items_a), list(items_b) - names_a = [item.name for item in items_a] - names_b = [item.name for item in items_b] - - assert len(set(names_a)) == len(items_a) # unique names - assert len(set(names_b)) == len(items_b) # unique names - if len(items_a) == 0 and len(items_b) == 0: return [] + + assert len(set({item.name for item in items_a})) == len(items_a) # unique names + assert len(set({item.name for item in items_b})) == len(items_b) # unique names + if len(items_a) == 0: return [ComparisonItem(None, item) for item in items_b] if len(items_b) == 0: return [ComparisonItem(item, None) for item in items_a] - start_b = names_a.index(names_b[0]) if names_b[0] in names_a else None - start_a = names_b.index(names_a[0]) if names_a[0] in names_b else None + start_b = cls._find_name(items_a, items_b[0].name) + start_a = cls._find_name(items_b, items_a[0].name) assert start_a is not None or start_b is not None # overlap From fe3117478a12cc119bba0db21066ebaed8b88df3 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 10 Sep 2020 15:33:47 +0200 Subject: [PATCH 151/155] snapshot can have intermediates, which can be squached --- src/abgleich/core/comparisondataset.py | 24 ++++++++++++++++++++++-- src/abgleich/core/snapshot.py | 7 +++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/abgleich/core/comparisondataset.py b/src/abgleich/core/comparisondataset.py index c701e72..7c6e88c 100644 --- a/src/abgleich/core/comparisondataset.py +++ b/src/abgleich/core/comparisondataset.py @@ -273,13 +273,30 @@ def _test_names(items: List[ComparisonItemABC]): raise ValueError("inconsistent snapshot names") @staticmethod - def _find_name(items: List[SnapshotABC], name: str) -> Union[int, None]: + def _find_name(snapshots: List[SnapshotABC], name: str) -> Union[int, None]: return next( - (index for (index, item) in enumerate(items) if item.name == name), + (index for (index, snapshot) in enumerate(snapshots) if snapshot.name == name), None, # if nothing is found, return None ) + @staticmethod + def _squash(snapshots: List[SnapshotABC]) -> List[SnapshotABC]: + + squashed, buffer = [], [] + + for snapshot in snapshots: + + if snapshot.get("abgleich:type").value != "backup": + buffer.append(snapshot) + else: + snapshot.intermediates.clear() + snapshot.intermediates.extend(buffer) + squashed.append(snapshot) + buffer.clear() + + return squashed + @classmethod def _merge_snapshots( cls, @@ -290,6 +307,9 @@ def _merge_snapshots( items_a, items_b = list(items_a), list(items_b) + if config['compatibility/tagging'].value: + items_a, items_b = cls._squash(items_a), cls._squash(items_b) + if len(items_a) == 0 and len(items_b) == 0: return [] diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 600885b..67441c5 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -76,6 +76,8 @@ def __init__( assert self._parent.startswith(self._root) self._subparent = self._parent[len(self._root) :].strip("/") + self._intermediates = [] # for namespaces / tagging + def __eq__(self, other: SnapshotABC) -> bool: return self.subparent == other.subparent and self.name == other.name @@ -210,6 +212,11 @@ def root(self) -> str: return self._root + @property + def intermediates(self) -> List[SnapshotABC]: + + return self._intermediates + @classmethod def from_entity( cls, From 0db7724e3af9b81aae302be2a87ee06aca0b740b Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Thu, 10 Sep 2020 15:35:18 +0200 Subject: [PATCH 152/155] black --- src/abgleich/core/comparisondataset.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/abgleich/core/comparisondataset.py b/src/abgleich/core/comparisondataset.py index 7c6e88c..88a6365 100644 --- a/src/abgleich/core/comparisondataset.py +++ b/src/abgleich/core/comparisondataset.py @@ -276,7 +276,11 @@ def _test_names(items: List[ComparisonItemABC]): def _find_name(snapshots: List[SnapshotABC], name: str) -> Union[int, None]: return next( - (index for (index, snapshot) in enumerate(snapshots) if snapshot.name == name), + ( + index + for (index, snapshot) in enumerate(snapshots) + if snapshot.name == name + ), None, # if nothing is found, return None ) @@ -307,7 +311,7 @@ def _merge_snapshots( items_a, items_b = list(items_a), list(items_b) - if config['compatibility/tagging'].value: + if config["compatibility/tagging"].value: items_a, items_b = cls._squash(items_a), cls._squash(items_b) if len(items_a) == 0 and len(items_b) == 0: From c5e3422dc783076b70bbec60568600919008e881 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Fri, 21 Jan 2022 19:52:59 +0100 Subject: [PATCH 153/155] prep for release --- CHANGES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f42dce2..c80e866 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Changes -## 0.0.8 (2020-XX-XX) +## 0.0.8 (2022-01-21) - FEATURE: `zfs-auto-snapshot` can be told to ignore backup datasets on the target side, see #3. - FEATURE: `samba` can optionally be told to NOT share/expose backup datasets on the target side, see #4. @@ -10,6 +10,7 @@ - FEATURE: Configuration module contains default values for parameters, making it much easier to write lightweight configuration files, see #28. The configuration parser now also provides much more useful output. - FEATURE: `abgleich tree` and `abgleich compare` highlight ignored datasets. - FEATURE: Significantly more flexible shell command wrapper and, as a result, cleaned up transaction handling. +- FEATURE: Python 3.9 and 3.10 compatibility. - FIX: Many cleanups in code base, enabling future developments. ## 0.0.7 (2020-08-05) From 078bfa95aac7c622b36296dd4c51d4c44efc8c08 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Fri, 21 Jan 2022 19:53:20 +0100 Subject: [PATCH 154/155] python 3.9+3.10 compat --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b2e77d2..f208192 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ # List all versions of Python which are supported python_minor_min = 6 -python_minor_max = 8 +python_minor_max = 10 confirmed_python_versions = [ "Programming Language :: Python :: 3.{MINOR:d}".format(MINOR=minor) for minor in range(python_minor_min, python_minor_max + 1) From 9d1378a1bbd7239bb3a55546540cbb68622ec899 Mon Sep 17 00:00:00 2001 From: "Sebastian M. Ernst" Date: Fri, 21 Jan 2022 19:56:21 +0100 Subject: [PATCH 155/155] 2022 update --- setup.py | 2 +- src/abgleich/__init__.py | 2 +- src/abgleich/cli/__init__.py | 2 +- src/abgleich/cli/_main_.py | 2 +- src/abgleich/cli/backup.py | 2 +- src/abgleich/cli/cleanup.py | 2 +- src/abgleich/cli/compare.py | 2 +- src/abgleich/cli/snap.py | 2 +- src/abgleich/cli/tree.py | 2 +- src/abgleich/cli/wizard.py | 2 +- src/abgleich/core/__init__.py | 2 +- src/abgleich/core/abc.py | 2 +- src/abgleich/core/command.py | 2 +- src/abgleich/core/comparisondataset.py | 2 +- src/abgleich/core/comparisonitem.py | 2 +- src/abgleich/core/comparisonzpool.py | 2 +- src/abgleich/core/config.py | 2 +- src/abgleich/core/configfield.py | 2 +- src/abgleich/core/configspec.py | 2 +- src/abgleich/core/dataset.py | 2 +- src/abgleich/core/i18n.py | 2 +- src/abgleich/core/io.py | 2 +- src/abgleich/core/lib.py | 2 +- src/abgleich/core/property.py | 2 +- src/abgleich/core/snapshot.py | 2 +- src/abgleich/core/transaction.py | 2 +- src/abgleich/core/transactionlist.py | 2 +- src/abgleich/core/transactionmeta.py | 2 +- src/abgleich/core/zpool.py | 2 +- src/abgleich/gui/__init__.py | 2 +- src/abgleich/gui/abc.py | 2 +- src/abgleich/gui/lib.py | 2 +- src/abgleich/gui/transaction.py | 2 +- src/abgleich/gui/wizard.py | 2 +- src/abgleich/gui/wizard_base.py | 2 +- 35 files changed, 35 insertions(+), 35 deletions(-) diff --git a/setup.py b/setup.py index f208192..013557c 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup.py: Used for package distribution - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/__init__.py b/src/abgleich/__init__.py index 3f1118f..2e3442d 100644 --- a/src/abgleich/__init__.py +++ b/src/abgleich/__init__.py @@ -8,7 +8,7 @@ src/abgleich/__init__.py: Package root - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/cli/__init__.py b/src/abgleich/cli/__init__.py index 3dee88d..148d5b8 100644 --- a/src/abgleich/cli/__init__.py +++ b/src/abgleich/cli/__init__.py @@ -8,7 +8,7 @@ src/abgleich/cli/__init__.py: CLI package root - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/cli/_main_.py b/src/abgleich/cli/_main_.py index 4512286..a9aa004 100644 --- a/src/abgleich/cli/_main_.py +++ b/src/abgleich/cli/_main_.py @@ -8,7 +8,7 @@ src/abgleich/cli/_main_.py: CLI auto-detection - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/cli/backup.py b/src/abgleich/cli/backup.py index 33c3faf..d2c028a 100644 --- a/src/abgleich/cli/backup.py +++ b/src/abgleich/cli/backup.py @@ -8,7 +8,7 @@ src/abgleich/cli/backup.py: backup command entry point - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/cli/cleanup.py b/src/abgleich/cli/cleanup.py index 7861aed..dc97bdb 100644 --- a/src/abgleich/cli/cleanup.py +++ b/src/abgleich/cli/cleanup.py @@ -8,7 +8,7 @@ src/abgleich/cli/cleanup.py: cleanup command entry point - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/cli/compare.py b/src/abgleich/cli/compare.py index d74588c..fb5b969 100644 --- a/src/abgleich/cli/compare.py +++ b/src/abgleich/cli/compare.py @@ -8,7 +8,7 @@ src/abgleich/cli/compare.py: compare command entry point - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/cli/snap.py b/src/abgleich/cli/snap.py index b228341..09b00f2 100644 --- a/src/abgleich/cli/snap.py +++ b/src/abgleich/cli/snap.py @@ -8,7 +8,7 @@ src/abgleich/cli/snap.py: snap command entry point - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/cli/tree.py b/src/abgleich/cli/tree.py index 7348b59..ea2d897 100644 --- a/src/abgleich/cli/tree.py +++ b/src/abgleich/cli/tree.py @@ -8,7 +8,7 @@ src/abgleich/cli/tree.py: tree command entry point - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/cli/wizard.py b/src/abgleich/cli/wizard.py index 616a344..c28884b 100644 --- a/src/abgleich/cli/wizard.py +++ b/src/abgleich/cli/wizard.py @@ -8,7 +8,7 @@ src/abgleich/cli/wizard.py: wizard command entry point - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/__init__.py b/src/abgleich/core/__init__.py index edf8425..b9189ed 100644 --- a/src/abgleich/core/__init__.py +++ b/src/abgleich/core/__init__.py @@ -8,7 +8,7 @@ src/abgleich/core/__init__.py: Core package root - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/abc.py b/src/abgleich/core/abc.py index 255289c..ac298e5 100644 --- a/src/abgleich/core/abc.py +++ b/src/abgleich/core/abc.py @@ -8,7 +8,7 @@ src/abgleich/core/abc.py: Abstract base classes - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/command.py b/src/abgleich/core/command.py index fa907d9..684e5bd 100644 --- a/src/abgleich/core/command.py +++ b/src/abgleich/core/command.py @@ -8,7 +8,7 @@ src/abgleich/core/command.py: Sub-process wrapper for commands - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/comparisondataset.py b/src/abgleich/core/comparisondataset.py index 88a6365..06b642b 100644 --- a/src/abgleich/core/comparisondataset.py +++ b/src/abgleich/core/comparisondataset.py @@ -8,7 +8,7 @@ src/abgleich/core/comparisondataset.py: ZFS dataset comparison - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/comparisonitem.py b/src/abgleich/core/comparisonitem.py index d7e217c..ec63d96 100644 --- a/src/abgleich/core/comparisonitem.py +++ b/src/abgleich/core/comparisonitem.py @@ -8,7 +8,7 @@ src/abgleich/core/comparisonitem.py: ZFS comparison item - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/comparisonzpool.py b/src/abgleich/core/comparisonzpool.py index 70f2154..f976e4d 100644 --- a/src/abgleich/core/comparisonzpool.py +++ b/src/abgleich/core/comparisonzpool.py @@ -8,7 +8,7 @@ src/abgleich/core/comparisonzpool.py: ZFS zpool comparison - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/config.py b/src/abgleich/core/config.py index 4d8e8e2..903c248 100644 --- a/src/abgleich/core/config.py +++ b/src/abgleich/core/config.py @@ -8,7 +8,7 @@ src/abgleich/core/config.py: Handles configuration data - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/configfield.py b/src/abgleich/core/configfield.py index c0a5287..60de58a 100644 --- a/src/abgleich/core/configfield.py +++ b/src/abgleich/core/configfield.py @@ -8,7 +8,7 @@ src/abgleich/core/configfield.py: Handles configuration fields - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/configspec.py b/src/abgleich/core/configspec.py index 4f04776..c81125e 100644 --- a/src/abgleich/core/configspec.py +++ b/src/abgleich/core/configspec.py @@ -8,7 +8,7 @@ src/abgleich/core/configspec.py: Defines configuration fields - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/dataset.py b/src/abgleich/core/dataset.py index b6e47e3..4507b04 100644 --- a/src/abgleich/core/dataset.py +++ b/src/abgleich/core/dataset.py @@ -8,7 +8,7 @@ src/abgleich/core/dataset.py: ZFS dataset - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/i18n.py b/src/abgleich/core/i18n.py index a98585e..9620386 100644 --- a/src/abgleich/core/i18n.py +++ b/src/abgleich/core/i18n.py @@ -8,7 +8,7 @@ src/abgleich/core/i18n.py: Translations - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/io.py b/src/abgleich/core/io.py index 458f5b8..947fc5b 100644 --- a/src/abgleich/core/io.py +++ b/src/abgleich/core/io.py @@ -8,7 +8,7 @@ src/abgleich/core/io.py: Command line IO - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/lib.py b/src/abgleich/core/lib.py index 2e342bb..d54787d 100644 --- a/src/abgleich/core/lib.py +++ b/src/abgleich/core/lib.py @@ -8,7 +8,7 @@ src/abgleich/core/lib.py: ZFS library - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/property.py b/src/abgleich/core/property.py index def84f8..dee76e8 100644 --- a/src/abgleich/core/property.py +++ b/src/abgleich/core/property.py @@ -8,7 +8,7 @@ src/abgleich/core/filesystem.py: ZFS filesystem - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/snapshot.py b/src/abgleich/core/snapshot.py index 67441c5..c14c5f3 100644 --- a/src/abgleich/core/snapshot.py +++ b/src/abgleich/core/snapshot.py @@ -8,7 +8,7 @@ src/abgleich/core/snapshot.py: ZFS snapshot - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/transaction.py b/src/abgleich/core/transaction.py index 64413c5..d5293c1 100644 --- a/src/abgleich/core/transaction.py +++ b/src/abgleich/core/transaction.py @@ -8,7 +8,7 @@ src/abgleich/core/transaction.py: ZFS transactions - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/transactionlist.py b/src/abgleich/core/transactionlist.py index 4ccb8d1..ab6ffe1 100644 --- a/src/abgleich/core/transactionlist.py +++ b/src/abgleich/core/transactionlist.py @@ -8,7 +8,7 @@ src/abgleich/core/transactionlist.py: ZFS transaction list - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/transactionmeta.py b/src/abgleich/core/transactionmeta.py index cae4ed6..bba64f6 100644 --- a/src/abgleich/core/transactionmeta.py +++ b/src/abgleich/core/transactionmeta.py @@ -8,7 +8,7 @@ src/abgleich/core/transactionmeta.py: ZFS transaction meta - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/core/zpool.py b/src/abgleich/core/zpool.py index fb43a48..bf7b2b3 100644 --- a/src/abgleich/core/zpool.py +++ b/src/abgleich/core/zpool.py @@ -8,7 +8,7 @@ src/abgleich/core/zpool.py: ZFS zpool - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/gui/__init__.py b/src/abgleich/gui/__init__.py index d82915c..83c7d20 100644 --- a/src/abgleich/gui/__init__.py +++ b/src/abgleich/gui/__init__.py @@ -8,7 +8,7 @@ src/abgleich/gui/__init__.py: GUI package root - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/gui/abc.py b/src/abgleich/gui/abc.py index c73b29c..0b008b0 100644 --- a/src/abgleich/gui/abc.py +++ b/src/abgleich/gui/abc.py @@ -8,7 +8,7 @@ src/abgleich/gui/abc.py: Abstract base classes - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/gui/lib.py b/src/abgleich/gui/lib.py index 0f189cf..c8ffb9f 100644 --- a/src/abgleich/gui/lib.py +++ b/src/abgleich/gui/lib.py @@ -8,7 +8,7 @@ src/abgleich/gui/lib.py: gui library - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/gui/transaction.py b/src/abgleich/gui/transaction.py index 15609e5..62af09e 100644 --- a/src/abgleich/gui/transaction.py +++ b/src/abgleich/gui/transaction.py @@ -8,7 +8,7 @@ src/abgleich/gui/transaction.py: ZFS transactions - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/gui/wizard.py b/src/abgleich/gui/wizard.py index 37ee463..7cfa014 100644 --- a/src/abgleich/gui/wizard.py +++ b/src/abgleich/gui/wizard.py @@ -8,7 +8,7 @@ src/abgleich/gui/wizard.py: wizard gui - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License diff --git a/src/abgleich/gui/wizard_base.py b/src/abgleich/gui/wizard_base.py index 95cb92d..1696199 100644 --- a/src/abgleich/gui/wizard_base.py +++ b/src/abgleich/gui/wizard_base.py @@ -8,7 +8,7 @@ src/abgleich/gui/wizard_base.py: wizard gui base - Copyright (C) 2019-2020 Sebastian M. Ernst + Copyright (C) 2019-2022 Sebastian M. Ernst The contents of this file are subject to the GNU Lesser General Public License