diff options
Diffstat (limited to 'modules')
-rw-r--r-- | modules/yggdrasil-wg/default.nix | 4 | ||||
-rw-r--r-- | modules/zfssnap/default.nix | 42 | ||||
-rw-r--r-- | modules/zfssnap/zfssnap/setup.py | 10 | ||||
-rw-r--r-- | modules/zfssnap/zfssnap/zfssnap/__main__.py (renamed from modules/zfssnap/zfssnap.py) | 130 |
4 files changed, 109 insertions, 77 deletions
diff --git a/modules/yggdrasil-wg/default.nix b/modules/yggdrasil-wg/default.nix index c27eb286..8525cea0 100644 --- a/modules/yggdrasil-wg/default.nix +++ b/modules/yggdrasil-wg/default.nix | |||
@@ -82,7 +82,7 @@ let | |||
82 | mkPrivateKeyPath = family: host: ./hosts + "/${family}" + "/${host}.priv"; | 82 | mkPrivateKeyPath = family: host: ./hosts + "/${family}" + "/${host}.priv"; |
83 | 83 | ||
84 | kernel = config.boot.kernelPackages; | 84 | kernel = config.boot.kernelPackages; |
85 | 85 | ||
86 | publicKeyPath = family: mkPublicKeyPath family hostName; | 86 | publicKeyPath = family: mkPublicKeyPath family hostName; |
87 | privateKeyPath = family: mkPrivateKeyPath family hostName; | 87 | privateKeyPath = family: mkPrivateKeyPath family hostName; |
88 | inNetwork' = family: pathExists (privateKeyPath family) && pathExists (publicKeyPath family); | 88 | inNetwork' = family: pathExists (privateKeyPath family) && pathExists (publicKeyPath family); |
@@ -221,7 +221,7 @@ in { | |||
221 | }; | 221 | }; |
222 | } | 222 | } |
223 | ] ++ (concatMap (router: map (rAddr: { routeConfig = { Destination = "::/0"; Gateway = stripSubnet rAddr; GatewayOnLink = true; Table = "yggdrasil"; }; }) batHostIPs.${router}) (filter (router: router != hostName) routers)); | 223 | ] ++ (concatMap (router: map (rAddr: { routeConfig = { Destination = "::/0"; Gateway = stripSubnet rAddr; GatewayOnLink = true; Table = "yggdrasil"; }; }) batHostIPs.${router}) (filter (router: router != hostName) routers)); |
224 | routingPolicyRules = map (addr: { routingPolicyRuleConfig = { Table = "yggdrasil"; From = stripSubnet addr; Priority = 1; }; }) batHostIPs.${hostName}; | 224 | routingPolicyRules = map (addr: { routingPolicyRuleConfig = { Table = "yggdrasil"; From = addr; Priority = 1; }; }) batHostIPs.${hostName}; |
225 | linkConfig = { | 225 | linkConfig = { |
226 | MACAddress = "${batHostMACs.${hostName}}"; | 226 | MACAddress = "${batHostMACs.${hostName}}"; |
227 | RequiredForOnline = false; | 227 | RequiredForOnline = false; |
diff --git a/modules/zfssnap/default.nix b/modules/zfssnap/default.nix index 42cdf46f..735e73ec 100644 --- a/modules/zfssnap/default.nix +++ b/modules/zfssnap/default.nix | |||
@@ -1,32 +1,20 @@ | |||
1 | { config, pkgs, lib, ... }: | 1 | { config, pkgs, lib, flakeInputs, ... }: |
2 | 2 | ||
3 | with lib; | 3 | with lib; |
4 | 4 | ||
5 | let | 5 | let |
6 | zfssnap = pkgs.stdenv.mkDerivation rec { | 6 | zfssnap = flakeInputs.mach-nix.lib.${config.nixpkgs.system}.buildPythonPackage rec { |
7 | name = "zfssnap"; | 7 | pname = "zfssnap"; |
8 | src = ./zfssnap.py; | 8 | src = ./zfssnap; |
9 | version = "0.0.0"; | ||
10 | ignoreDataOutdated = true; | ||
9 | 11 | ||
10 | phases = [ "buildPhase" "checkPhase" "installPhase" ]; | 12 | requirements = '' |
11 | 13 | pyxdg | |
12 | buildInputs = with pkgs; [makeWrapper]; | 14 | pytimeparse |
13 | 15 | python-dateutil | |
14 | python = pkgs.python39.withPackages (ps: with ps; [pyxdg pytimeparse python-dateutil]); | ||
15 | |||
16 | buildPhase = '' | ||
17 | substitute $src zfssnap \ | ||
18 | --subst-var-by python ${escapeShellArg python} | ||
19 | ''; | ||
20 | |||
21 | doCheck = true; | ||
22 | checkPhase = '' | ||
23 | ${python}/bin/python -m py_compile zfssnap | ||
24 | ''; | 16 | ''; |
25 | 17 | postInstall = '' | |
26 | installPhase = '' | ||
27 | install -m 0755 -D -t $out/bin \ | ||
28 | zfssnap | ||
29 | |||
30 | wrapProgram $out/bin/zfssnap \ | 18 | wrapProgram $out/bin/zfssnap \ |
31 | --prefix PATH : ${makeBinPath [config.boot.zfs.package]} | 19 | --prefix PATH : ${makeBinPath [config.boot.zfs.package]} |
32 | ''; | 20 | ''; |
@@ -71,7 +59,9 @@ in { | |||
71 | before = [ "zfssnap-prune.service" ]; | 59 | before = [ "zfssnap-prune.service" ]; |
72 | serviceConfig = { | 60 | serviceConfig = { |
73 | Type = "oneshot"; | 61 | Type = "oneshot"; |
74 | ExecStart = "${zfssnap}/bin/zfssnap -v"; | 62 | ExecStart = "${zfssnap}/bin/zfssnap -vv"; |
63 | |||
64 | LogRateLimitIntervalSec = 0; | ||
75 | }; | 65 | }; |
76 | }; | 66 | }; |
77 | systemd.services."zfssnap-prune" = { | 67 | systemd.services."zfssnap-prune" = { |
@@ -82,7 +72,9 @@ in { | |||
82 | ExecStart = let | 72 | ExecStart = let |
83 | mkSectionName = name: strings.escape [ "[" "]" ] (strings.toUpper name); | 73 | mkSectionName = name: strings.escape [ "[" "]" ] (strings.toUpper name); |
84 | zfssnapConfig = generators.toINI { inherit mkSectionName; } cfg.config; | 74 | zfssnapConfig = generators.toINI { inherit mkSectionName; } cfg.config; |
85 | in "${zfssnap}/bin/zfssnap -vv prune --config=${pkgs.writeText "zfssnap.ini" zfssnapConfig}"; | 75 | in "${zfssnap}/bin/zfssnap -vv prune --exec-newest --config=${pkgs.writeText "zfssnap.ini" zfssnapConfig}"; # DEBUG |
76 | |||
77 | LogRateLimitIntervalSec = 0; | ||
86 | }; | 78 | }; |
87 | }; | 79 | }; |
88 | 80 | ||
diff --git a/modules/zfssnap/zfssnap/setup.py b/modules/zfssnap/zfssnap/setup.py new file mode 100644 index 00000000..6c58757d --- /dev/null +++ b/modules/zfssnap/zfssnap/setup.py | |||
@@ -0,0 +1,10 @@ | |||
1 | from setuptools import setup | ||
2 | |||
3 | setup(name='zfssnap', | ||
4 | packages=['zfssnap'], | ||
5 | entry_points={ | ||
6 | 'console_scripts': [ | ||
7 | 'zfssnap=zfssnap.__main__:main', | ||
8 | ], | ||
9 | } | ||
10 | ) | ||
diff --git a/modules/zfssnap/zfssnap.py b/modules/zfssnap/zfssnap/zfssnap/__main__.py index a8dae75f..a0eade78 100644 --- a/modules/zfssnap/zfssnap.py +++ b/modules/zfssnap/zfssnap/zfssnap/__main__.py | |||
@@ -45,6 +45,9 @@ TIME_PATTERNS = OrderedDict([ | |||
45 | ("yearly", lambda t: t.strftime('%Y')), | 45 | ("yearly", lambda t: t.strftime('%Y')), |
46 | ]) | 46 | ]) |
47 | 47 | ||
48 | PROP_DO_AUTO_SNAPSHOT = 'li.yggdrasil:auto-snapshot' | ||
49 | PROP_IS_AUTO_SNAPSHOT = 'li.yggdrasil:is-auto-snapshot' | ||
50 | |||
48 | @dataclass(eq=True, order=True, frozen=True) | 51 | @dataclass(eq=True, order=True, frozen=True) |
49 | class Snap: | 52 | class Snap: |
50 | name: str | 53 | name: str |
@@ -68,25 +71,28 @@ def _snap_name(item, time=_now()): | |||
68 | 71 | ||
69 | def _log_cmd(*args): | 72 | def _log_cmd(*args): |
70 | fmt_args = ' '.join(map(shlex.quote, args)) | 73 | fmt_args = ' '.join(map(shlex.quote, args)) |
71 | logger.debug(f'Running command: {fmt_args}') | 74 | logger.debug('Running command: %s', fmt_args) |
72 | 75 | ||
73 | def _get_items(): | 76 | def _get_items(): |
74 | items = {} | 77 | items = {} |
75 | 78 | ||
76 | args = ['zfs', 'get', '-H', '-p', '-o', 'name,value', '-t', 'filesystem,volume', '-s', 'local,default,inherited,temporary,received', 'li.yggdrasil:auto-snapshot'] | 79 | args = ['zfs', 'get', '-H', '-p', '-o', 'name,value', '-t', 'filesystem,volume', PROP_DO_AUTO_SNAPSHOT] |
77 | _log_cmd(*args) | 80 | _log_cmd(*args) |
78 | with subprocess.Popen(args, stdout=subprocess.PIPE) as proc: | 81 | with subprocess.Popen(args, stdout=subprocess.PIPE) as proc: |
79 | text_stdout = io.TextIOWrapper(proc.stdout) | 82 | text_stdout = io.TextIOWrapper(proc.stdout) |
80 | reader = csv.DictReader(text_stdout, fieldnames=['name', 'setting'], delimiter='\t', quoting=csv.QUOTE_NONE) | 83 | reader = csv.DictReader(text_stdout, fieldnames=['name', 'value'], delimiter='\t', quoting=csv.QUOTE_NONE) |
81 | Row = namedtuple('Row', reader.fieldnames) | 84 | Row = namedtuple('Row', reader.fieldnames) |
82 | for row in [Row(**data) for data in reader]: | 85 | for row in [Row(**data) for data in reader]: |
83 | items[row.name] = bool(strtobool(row.setting)) | 86 | if not row.value or row.value == '-': |
87 | continue | ||
88 | |||
89 | items[row.name] = bool(strtobool(row.value)) | ||
84 | 90 | ||
85 | return items | 91 | return items |
86 | 92 | ||
87 | def _get_snaps(only_auto=True): | 93 | def _get_snaps(only_auto=True): |
88 | snapshots = defaultdict(list) | 94 | snapshots = defaultdict(list) |
89 | args = ['zfs', 'list', '-H', '-p', '-t', 'snapshot', '-o', 'name,li.yggdrasil:is-auto-snapshot,creation'] | 95 | args = ['zfs', 'list', '-H', '-p', '-t', 'snapshot', '-o', f'name,{PROP_IS_AUTO_SNAPSHOT},creation'] |
90 | _log_cmd(*args) | 96 | _log_cmd(*args) |
91 | with subprocess.Popen(args, stdout=subprocess.PIPE) as proc: | 97 | with subprocess.Popen(args, stdout=subprocess.PIPE) as proc: |
92 | text_stdout = io.TextIOWrapper(proc.stdout) | 98 | text_stdout = io.TextIOWrapper(proc.stdout) |
@@ -102,17 +108,26 @@ def _get_snaps(only_auto=True): | |||
102 | 108 | ||
103 | return snapshots | 109 | return snapshots |
104 | 110 | ||
105 | def prune(config, dry_run, keep_newest, do_exec): | 111 | def prune(config, dry_run, keep_newest, do_exec, exec_newest): |
106 | do_exec = do_exec and 'EXEC' in config | 112 | do_exec = do_exec and 'EXEC' in config |
107 | prune_timezone = config.gettimezone('KEEP', 'timezone', fallback=tzutc()) | 113 | prune_timezone = config.gettimezone('KEEP', 'timezone', fallback=tzutc()) |
108 | logger.debug(f'prune timezone: {prune_timezone}') | 114 | logger.debug('prune timezone: %s', prune_timezone) |
109 | 115 | ||
110 | items = _get_snaps() | 116 | items = _get_snaps() |
111 | 117 | ||
118 | kept_count = defaultdict(lambda: defaultdict(lambda: 0)) | ||
119 | kept_because = OrderedDict() | ||
120 | def keep_because(base, snap, rule, period=None): | ||
121 | nonlocal kept_count, kept_because | ||
122 | kept_count[rule][base] += 1 | ||
123 | if snap not in kept_because: | ||
124 | kept_because[snap] = deque() | ||
125 | kept_because[snap].append(KeptBecause(rule=rule, ix=kept_count[rule][base], base=base, period=period)) | ||
126 | |||
112 | exec_candidates = set() | 127 | exec_candidates = set() |
113 | if do_exec: | 128 | if do_exec: |
114 | exec_timezone = config.gettimezone('EXEC', 'timezone', fallback=prune_timezone) | 129 | exec_timezone = config.gettimezone('EXEC', 'timezone', fallback=prune_timezone) |
115 | logger.debug(f'exec timezone: {exec_timezone}') | 130 | logger.debug('exec timezone: %s', exec_timezone) |
116 | 131 | ||
117 | for rule, pattern in TIME_PATTERNS.items(): | 132 | for rule, pattern in TIME_PATTERNS.items(): |
118 | desired_count = config.getint('EXEC', rule, fallback=0) | 133 | desired_count = config.getint('EXEC', rule, fallback=0) |
@@ -120,7 +135,7 @@ def prune(config, dry_run, keep_newest, do_exec): | |||
120 | for base, snaps in items.items(): | 135 | for base, snaps in items.items(): |
121 | periods = OrderedDict() | 136 | periods = OrderedDict() |
122 | 137 | ||
123 | for snap in sorted(snaps, key=lambda snap: snap.creation): | 138 | for snap in sorted(snaps, key=lambda snap: snap.creation, reverse=exec_newest): |
124 | period = pattern(snap.creation.astimezone(exec_timezone)) | 139 | period = pattern(snap.creation.astimezone(exec_timezone)) |
125 | if period not in periods: | 140 | if period not in periods: |
126 | periods[period] = deque() | 141 | periods[period] = deque() |
@@ -134,15 +149,16 @@ def prune(config, dry_run, keep_newest, do_exec): | |||
134 | 149 | ||
135 | for snap in period_snaps: | 150 | for snap in period_snaps: |
136 | exec_candidates.add(snap) | 151 | exec_candidates.add(snap) |
137 | logger.debug(f'{snap.name} is exec candidate') | 152 | logger.debug('‘%s’ is exec candidate', snap.name) |
138 | to_exec -= 1 | 153 | to_exec -= 1 |
139 | break | 154 | break |
140 | 155 | ||
141 | if to_exec > 0: | 156 | if to_exec > 0: |
142 | logger.debug(f'Missing {to_exec} to fulfill exec {rule}={desired_count} for ‘{base}’') | 157 | logger.debug('Missing %d to fulfill exec %s=%d for ‘%s’', to_exec, rule, desired_count, base) |
143 | 158 | ||
144 | check_cmd = config.get('EXEC', 'check', fallback=None) | 159 | check_cmd = config.get('EXEC', 'check', fallback=None) |
145 | if check_cmd: | 160 | if check_cmd: |
161 | logger.debug('exec_candidates=%s', exec_candidates) | ||
146 | already_execed = set() | 162 | already_execed = set() |
147 | for snap in exec_candidates: | 163 | for snap in exec_candidates: |
148 | args = [] | 164 | args = [] |
@@ -152,7 +168,20 @@ def prune(config, dry_run, keep_newest, do_exec): | |||
152 | check_res = subprocess.run(args) | 168 | check_res = subprocess.run(args) |
153 | if check_res.returncode == 0: | 169 | if check_res.returncode == 0: |
154 | already_execed.add(snap) | 170 | already_execed.add(snap) |
155 | logger.debug(f'{snap.name} already execed') | 171 | logger.debug('‘%s’ already execed', snap.name) |
172 | elif check_res.returncode == 124: | ||
173 | already_execed.add(snap) | ||
174 | logger.warn('‘%s’ ignored', snap.name) | ||
175 | pass | ||
176 | elif check_res.returncode == 125: | ||
177 | already_execed.add(snap) | ||
178 | logger.info('‘%s’ ignored but specified for keeping, doing so...', snap.name) | ||
179 | base_name, _, _ = snap.name.rpartition('@') | ||
180 | keep_because(base_name, snap.name, 'exec-ignored') | ||
181 | elif check_res.returncode == 126: | ||
182 | logger.debug('‘%s’ to exec', snap.name) | ||
183 | else: | ||
184 | check_res.check_returncode() | ||
156 | exec_candidates -= already_execed | 185 | exec_candidates -= already_execed |
157 | 186 | ||
158 | exec_cmd = config.get('EXEC', 'cmd', fallback=None) | 187 | exec_cmd = config.get('EXEC', 'cmd', fallback=None) |
@@ -160,28 +189,26 @@ def prune(config, dry_run, keep_newest, do_exec): | |||
160 | if exec_cmd: | 189 | if exec_cmd: |
161 | execed = set() | 190 | execed = set() |
162 | for snap in sorted(exec_candidates, key=lambda snap: snap.creation): | 191 | for snap in sorted(exec_candidates, key=lambda snap: snap.creation): |
163 | if len(execed) >= exec_count: | 192 | if exec_count > 0 and len(execed) >= exec_count: |
164 | logger.debug(f'exc_count of {exec_count} reached') | 193 | logger.debug('exec_count of %d reached', exec_count) |
165 | break | 194 | break |
166 | 195 | ||
167 | args = [] | 196 | args = [] |
168 | args += shlex.split(exec_cmd) | 197 | args += shlex.split(exec_cmd) |
169 | args += [snap.name] | 198 | args += [snap.name] |
170 | _log_cmd(*args) | 199 | _log_cmd(*args) |
171 | subprocess.run(args).check_returncode() | 200 | p = subprocess.run(args) |
201 | if p.returncode == 125: | ||
202 | logger.warn('got dry-run returncode for ‘%s’, keeping...', snap.name) | ||
203 | base_name, _, _ = snap.name.rpartition('@') | ||
204 | keep_because(base_name, snap.name, 'exec-dryrun') | ||
205 | pass | ||
206 | else: | ||
207 | p.check_returncode() | ||
172 | execed.add(snap) | 208 | execed.add(snap) |
173 | 209 | ||
174 | exec_candidates -= execed | 210 | exec_candidates -= execed |
175 | 211 | ||
176 | kept_count = defaultdict(lambda: defaultdict(lambda: 0)) | ||
177 | kept_because = OrderedDict() | ||
178 | def keep_because(base, snap, rule, period=None): | ||
179 | nonlocal kept_count, kept_because | ||
180 | kept_count[rule][base] += 1 | ||
181 | if snap not in kept_because: | ||
182 | kept_because[snap] = deque() | ||
183 | kept_because[snap].append(KeptBecause(rule=rule, ix=kept_count[rule][base], base=base, period=period)) | ||
184 | |||
185 | for candidate in exec_candidates: | 212 | for candidate in exec_candidates: |
186 | base_name, _, _ = candidate.name.rpartition('@') | 213 | base_name, _, _ = candidate.name.rpartition('@') |
187 | keep_because(base_name, candidate.name, 'exec-candidate') | 214 | keep_because(base_name, candidate.name, 'exec-candidate') |
@@ -191,10 +218,10 @@ def prune(config, dry_run, keep_newest, do_exec): | |||
191 | for base, snaps in items.items(): | 218 | for base, snaps in items.items(): |
192 | time_ref = max(snaps, key=lambda snap: snap.creation, default=None) | 219 | time_ref = max(snaps, key=lambda snap: snap.creation, default=None) |
193 | if not time_ref: | 220 | if not time_ref: |
194 | logger.warn(f'Nothing to keep for ‘{base}’') | 221 | logger.warn('Nothing to keep for ‘%s’', base) |
195 | continue | 222 | continue |
196 | 223 | ||
197 | logger.info(f'Using ‘{time_ref.name}’ as time reference for ‘{base}’') | 224 | logger.info('Using ‘%s’ as time reference for ‘%s’', time_ref.name, base) |
198 | within_cutoff = time_ref.creation - within | 225 | within_cutoff = time_ref.creation - within |
199 | 226 | ||
200 | for snap in snaps: | 227 | for snap in snaps: |
@@ -227,11 +254,10 @@ def prune(config, dry_run, keep_newest, do_exec): | |||
227 | break | 254 | break |
228 | 255 | ||
229 | if to_keep > 0: | 256 | if to_keep > 0: |
230 | logger.debug(f'Missing {to_keep} to fulfill prune {rule}={desired_count} for ‘{base}’') | 257 | logger.debug('Missing %d to fulfill prune %s=%d for ‘%s’', to_keep, rule, desired_count, base) |
231 | 258 | ||
232 | for snap, reasons in kept_because.items(): | 259 | for snap, reasons in kept_because.items(): |
233 | reasons_str = ', '.join(map(str, reasons)) | 260 | logger.info('Keeping ‘%s’ because: %s', snap, ', '.join(map(str, reasons))) |
234 | logger.info(f'Keeping ‘{snap}’ because: {reasons_str}') | ||
235 | all_snaps = {snap.name for _, snaps in items.items() for snap in snaps} | 261 | all_snaps = {snap.name for _, snaps in items.items() for snap in snaps} |
236 | to_destroy = all_snaps - {*kept_because} | 262 | to_destroy = all_snaps - {*kept_because} |
237 | if not to_destroy: | 263 | if not to_destroy: |
@@ -245,9 +271,9 @@ def prune(config, dry_run, keep_newest, do_exec): | |||
245 | _log_cmd(*args) | 271 | _log_cmd(*args) |
246 | subprocess.run(args, check=True) | 272 | subprocess.run(args, check=True) |
247 | if dry_run: | 273 | if dry_run: |
248 | logger.info(f'Would have pruned ‘{snap}’') | 274 | logger.info('Would have pruned ‘%s’', snap) |
249 | else: | 275 | else: |
250 | logger.info(f'Pruned ‘{snap}’') | 276 | logger.info('Pruned ‘%s’', snap) |
251 | 277 | ||
252 | def rename(snapshots, destroy=False, set_is_auto=False): | 278 | def rename(snapshots, destroy=False, set_is_auto=False): |
253 | args = ['zfs', 'get', '-H', '-p', '-o', 'name,value', 'creation', *snapshots] | 279 | args = ['zfs', 'get', '-H', '-p', '-o', 'name,value', 'creation', *snapshots] |
@@ -262,29 +288,29 @@ def rename(snapshots, destroy=False, set_is_auto=False): | |||
262 | base_name, _, _ = row.name.rpartition('@') | 288 | base_name, _, _ = row.name.rpartition('@') |
263 | new_name = _snap_name(base_name, time=creation) | 289 | new_name = _snap_name(base_name, time=creation) |
264 | if new_name == row.name: | 290 | if new_name == row.name: |
265 | logger.debug(f'Not renaming ‘{row.name}’ since name is already correct') | 291 | logger.debug('Not renaming ‘%s’ since name is already correct', row.name) |
266 | continue | 292 | continue |
267 | 293 | ||
268 | if new_name in renamed_to: | 294 | if new_name in renamed_to: |
269 | if destroy: | 295 | if destroy: |
270 | logger.warning(f'Destroying ‘{row.name}’ since ‘{new_name}’ was already renamed to') | 296 | logger.warning('Destroying ‘%s’ since ‘%s’ was already renamed to', row.name, new_name) |
271 | args = ['zfs', 'destroy', row.name] | 297 | args = ['zfs', 'destroy', row.name] |
272 | _log_cmd(*args) | 298 | _log_cmd(*args) |
273 | subprocess.run(args, check=True) | 299 | subprocess.run(args, check=True) |
274 | else: | 300 | else: |
275 | logger.info(f'Skipping ‘{row.name}’ since ‘{new_name}’ was already renamed to') | 301 | logger.info('Skipping ‘%s’ since ‘%s’ was already renamed to', row.name, new_name) |
276 | 302 | ||
277 | continue | 303 | continue |
278 | 304 | ||
279 | logger.info(f'Renaming ‘{row.name}’ to ‘{new_name}’') | 305 | logger.info('Renaming ‘%s’ to ‘%s’', row.name, new_name) |
280 | args = ['zfs', 'rename', row.name, new_name] | 306 | args = ['zfs', 'rename', row.name, new_name] |
281 | _log_cmd(*args) | 307 | _log_cmd(*args) |
282 | subprocess.run(args, check=True) | 308 | subprocess.run(args, check=True) |
283 | renamed_to.add(new_name) | 309 | renamed_to.add(new_name) |
284 | 310 | ||
285 | if set_is_auto: | 311 | if set_is_auto: |
286 | logger.info(f'Setting is-auto-snapshot on ‘{new_name}’') | 312 | logger.info('Setting is-auto-snapshot on ‘%s’', new_name) |
287 | args = ['zfs', 'set', 'li.yggdrasil:is-auto-snapshot=true', new_name] | 313 | args = ['zfs', 'set', f'{PROP_IS_AUTO_SNAPSHOT}=true', new_name] |
288 | _log_cmd(*args) | 314 | _log_cmd(*args) |
289 | subprocess.run(args, check=True) | 315 | subprocess.run(args, check=True) |
290 | 316 | ||
@@ -301,7 +327,7 @@ def autosnap(): | |||
301 | else: | 327 | else: |
302 | all_snap_names |= snap_names | 328 | all_snap_names |= snap_names |
303 | 329 | ||
304 | args = ['zfs', 'snapshot', '-o', 'li.yggdrasil:is-auto-snapshot=true'] | 330 | args = ['zfs', 'snapshot', '-o', f'{PROP_IS_AUTO_SNAPSHOT}=true'] |
305 | if recursive: | 331 | if recursive: |
306 | args += ['-r'] | 332 | args += ['-r'] |
307 | args += snap_names | 333 | args += snap_names |
@@ -324,7 +350,7 @@ def autosnap(): | |||
324 | await asyncio.gather(*tasks) | 350 | await asyncio.gather(*tasks) |
325 | asyncio.run(run_tasks()) | 351 | asyncio.run(run_tasks()) |
326 | for snap in all_snap_names: | 352 | for snap in all_snap_names: |
327 | logger.info(f'Created ‘{snap}’') | 353 | logger.info('Created ‘%s’', snap) |
328 | if all_snap_names: | 354 | if all_snap_names: |
329 | rename(snapshots=all_snap_names) | 355 | rename(snapshots=all_snap_names) |
330 | 356 | ||
@@ -347,7 +373,8 @@ def main(): | |||
347 | sys.excepthook = log_exceptions | 373 | sys.excepthook = log_exceptions |
348 | 374 | ||
349 | parser = argparse.ArgumentParser(prog='zfssnap') | 375 | parser = argparse.ArgumentParser(prog='zfssnap') |
350 | parser.add_argument('--verbose', '-v', action='count', default=0) | 376 | parser.add_argument('--verbose', '-v', dest='log_level', action='append_const', const=-1) |
377 | parser.add_argument('--quiet', '-q', dest='log_level', action='append_const', const=1) | ||
351 | subparsers = parser.add_subparsers() | 378 | subparsers = parser.add_subparsers() |
352 | parser.set_defaults(cmd=autosnap) | 379 | parser.set_defaults(cmd=autosnap) |
353 | rename_parser = subparsers.add_parser('rename') | 380 | rename_parser = subparsers.add_parser('rename') |
@@ -359,26 +386,29 @@ def main(): | |||
359 | prune_parser.add_argument('--config', '-c', dest='config_files', nargs='*', default=list()) | 386 | prune_parser.add_argument('--config', '-c', dest='config_files', nargs='*', default=list()) |
360 | prune_parser.add_argument('--dry-run', '-n', action='store_true', default=False) | 387 | prune_parser.add_argument('--dry-run', '-n', action='store_true', default=False) |
361 | prune_parser.add_argument('--keep-newest', action='store_true', default=False) | 388 | prune_parser.add_argument('--keep-newest', action='store_true', default=False) |
389 | prune_parser.add_argument('--exec-newest', action='store_true', default=False) | ||
362 | prune_parser.add_argument('--no-exec', dest='do_exec', action='store_false', default=True) | 390 | prune_parser.add_argument('--no-exec', dest='do_exec', action='store_false', default=True) |
363 | prune_parser.set_defaults(cmd=prune) | 391 | prune_parser.set_defaults(cmd=prune) |
364 | args = parser.parse_args() | 392 | args = parser.parse_args() |
365 | 393 | ||
366 | if args.verbose <= 0: | 394 | |
367 | logger.setLevel(logging.WARNING) | 395 | LOG_LEVELS = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL] |
368 | elif args.verbose <= 1: | 396 | DEFAULT_LOG_LEVEL = logging.ERROR |
369 | logger.setLevel(logging.INFO) | 397 | log_level = LOG_LEVELS.index(DEFAULT_LOG_LEVEL) |
370 | else: | 398 | |
371 | logger.setLevel(logging.DEBUG) | 399 | for adjustment in args.log_level or (): |
400 | log_level = min(len(LOG_LEVELS) - 1, max(log_level + adjustment, 0)) | ||
401 | logger.setLevel(LOG_LEVELS[log_level]) | ||
372 | 402 | ||
373 | cmdArgs = {} | 403 | cmdArgs = {} |
374 | for copy in {'snapshots', 'dry_run', 'destroy', 'keep_newest', 'set_is_auto', 'do_exec'}: | 404 | for copy in {'snapshots', 'dry_run', 'destroy', 'keep_newest', 'exec_newest', 'set_is_auto', 'do_exec'}: |
375 | if copy in vars(args): | 405 | if copy in vars(args): |
376 | cmdArgs[copy] = vars(args)[copy] | 406 | cmdArgs[copy] = vars(args)[copy] |
377 | if 'config_files' in vars(args): | 407 | if 'config_files' in vars(args): |
378 | def convert_timedelta(secs_str): | 408 | def convert_timedelta(secs_str): |
379 | secs=pytimeparse.parse(secs_str) | 409 | secs=pytimeparse.parse(secs_str) |
380 | if secs is None: | 410 | if secs is None: |
381 | raise ValueError(f'Could not parse timedelta expression ‘{secs_str}’') | 411 | raise ValueError('Could not parse timedelta expression ‘%s’', secs_str) |
382 | return timedelta(seconds=secs) | 412 | return timedelta(seconds=secs) |
383 | config = configparser.ConfigParser(converters={ | 413 | config = configparser.ConfigParser(converters={ |
384 | 'timedelta': convert_timedelta, | 414 | 'timedelta': convert_timedelta, |
@@ -393,9 +423,9 @@ def main(): | |||
393 | return ', '.join(map(lambda file: f'‘{file}’', files)) | 423 | return ', '.join(map(lambda file: f'‘{file}’', files)) |
394 | 424 | ||
395 | if not read_files: | 425 | if not read_files: |
396 | raise Exception(f'Found no config files. Tried: {format_config_files(search_files)}') | 426 | raise Exception('Found no config files. Tried: %s', format_config_files(search_files)) |
397 | 427 | ||
398 | logger.debug(f'Read following config files: {format_config_files(read_files)}') | 428 | logger.debug('Read following config files: %s', format_config_files(read_files)) |
399 | 429 | ||
400 | cmdArgs['config'] = config | 430 | cmdArgs['config'] = config |
401 | 431 | ||