virt-back 0.2.4__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: virt-back
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: virt-back: A backup utility for QEMU, KVM, XEN, and Virtualbox guests
5
5
  Home-page: https://git.unturf.com/python/virt-back
6
6
  Author: Russell Ballestrini
@@ -9,6 +9,17 @@ License: Public Domain
9
9
  Keywords: backup virtual hypervisor QEMU KVM XEN Virtualbox
10
10
  Platform: All
11
11
  Description-Content-Type: text/markdown
12
+ Requires-Dist: libvirt-python
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: description
16
+ Dynamic: description-content-type
17
+ Dynamic: home-page
18
+ Dynamic: keywords
19
+ Dynamic: license
20
+ Dynamic: platform
21
+ Dynamic: requires-dist
22
+ Dynamic: summary
12
23
 
13
24
  A backup utility for QEMU, KVM, XEN, and Virtualbox guests.
14
25
 
@@ -39,6 +50,7 @@ options:
39
50
  -g, --no-gzip do not gzip the resulting tar file
40
51
  -a amount, --retention amount
41
52
  backups to retain [default: 3]
53
+ (applies to both rotated backup files AND ZFS @backup-* snapshots)
42
54
  -p 'PATH', --path 'PATH'
43
55
  backup path [default: '/KVMBACK']
44
56
  -u 'URI', --uri 'URI'
@@ -27,6 +27,7 @@ options:
27
27
  -g, --no-gzip do not gzip the resulting tar file
28
28
  -a amount, --retention amount
29
29
  backups to retain [default: 3]
30
+ (applies to both rotated backup files AND ZFS @backup-* snapshots)
30
31
  -p 'PATH', --path 'PATH'
31
32
  backup path [default: '/KVMBACK']
32
33
  -u 'URI', --uri 'URI'
@@ -0,0 +1,71 @@
1
+ from setuptools import setup
2
+ import os
3
+
4
+ # Read the contents of your README file
5
+ with open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf-8") as f:
6
+ long_description = f.read()
7
+
8
+ setup(
9
+ name="virt-back",
10
+ version="0.2.6",
11
+ description="virt-back: A backup utility for QEMU, KVM, XEN, and Virtualbox guests",
12
+ keywords="backup virtual hypervisor QEMU KVM XEN Virtualbox",
13
+ long_description=long_description,
14
+ long_description_content_type="text/markdown",
15
+ author="Russell Ballestrini",
16
+ author_email="russell@ballestrini.net",
17
+ url="https://git.unturf.com/python/virt-back",
18
+ platforms=["All"],
19
+ license="Public Domain",
20
+ py_modules=["virtback"],
21
+ include_package_data=True,
22
+ scripts=["virt-back"],
23
+ install_requires=[
24
+ "libvirt-python",
25
+ ],
26
+ )
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Release process (automated via .gitlab-ci.yml on tag push)
30
+ # ---------------------------------------------------------------------------
31
+ #
32
+ # Cutting a release:
33
+ # 1. Bump `version=` above
34
+ # 2. git commit -m "X.Y.Z: <one-line summary>"
35
+ # 3. git tag -a X.Y.Z -m "X.Y.Z"
36
+ # 4. git push origin master X.Y.Z
37
+ #
38
+ # The CI pipeline builds (python -m build) and uploads (twine upload) on the
39
+ # tag push. No manual `python setup.py sdist && twine upload` needed.
40
+ #
41
+ # ---------------------------------------------------------------------------
42
+ # Maintainer setup (one-time, per project) — required for CI uploads to work
43
+ # ---------------------------------------------------------------------------
44
+ #
45
+ # git.unturf.com -> python/<this-project> -> Settings -> Repository ->
46
+ # Protected Tags -> "Protect tag":
47
+ # - Pattern: *
48
+ # - Allowed to Create: Maintainers (match erldistpy's setting)
49
+ #
50
+ # This makes tags Protected refs, which lets the Protected/Masked
51
+ # TWINE_USERNAME and TWINE_PASSWORD variables (set once at the python/
52
+ # group level) reach the tag pipeline. Without Protected Tags, twine 6
53
+ # falls through to OIDC trusted publishing and fails because PyPI's
54
+ # self-hosted GitLab support is hardcoded to gitlab.com.
55
+ #
56
+ # Group variables live at:
57
+ # git.unturf.com -> python (group) -> Settings -> CI/CD -> Variables
58
+ # TWINE_USERNAME = __token__
59
+ # TWINE_PASSWORD = <pypi-AgEI...> (Masked + Protected)
60
+ #
61
+ # setuptools keyword args: http://peak.telecommunity.com/DevCenter/setuptools
62
+
63
+ # Installation Instructions:
64
+ # To install virt-back, you can use pip:
65
+ # * pip install virt-back
66
+ #
67
+ # Note: You need to have the libvirt development libraries installed.
68
+ # On Fedora/RedHat, you can install it with:
69
+ # * sudo dnf install libvirt-devel
70
+ # On Ubuntu, you can install it with:
71
+ # * sudo apt-get install libvirt-dev
@@ -23,6 +23,7 @@ import libvirt
23
23
  import tarfile
24
24
  import syslog
25
25
  import re
26
+ import glob
26
27
  from time import sleep
27
28
  from datetime import date
28
29
  from sys import exit
@@ -195,6 +196,9 @@ def backup(doms):
195
196
  except subprocess.CalledProcessError as e:
196
197
  logit("error", f"Failed to send ZFS snapshot {zfs_snapshot}: {e}")
197
198
  continue
199
+
200
+ # Prune old @backup-* snapshots, keep newest `retention`
201
+ prune_zfs_snapshots(zfs_dataset, options.retention)
198
202
  else:
199
203
  # Handle QCOW2 or other file-based disk
200
204
  logit("backup", f"{disk_source} is not a ZFS dataset")
@@ -281,6 +285,48 @@ def is_zfs_dataset(disk_source):
281
285
  return False
282
286
 
283
287
 
288
+ def prune_zfs_snapshots(zfs_dataset, retention):
289
+ """List @backup-* snapshots on dataset, destroy oldest until count <= retention.
290
+
291
+ Only touches snapshots virt-back created (@backup-* prefix). Never destroys
292
+ user or TrueNAS periodic snapshots. Never destroys more than necessary —
293
+ `retention` snapshots always survive as the recovery floor.
294
+ """
295
+ if retention < 1:
296
+ logit("error", f"retention must be >= 1, got {retention} — skipping prune")
297
+ return
298
+
299
+ # INVARIANT: reclamation eats from the tail (oldest first).
300
+ # `-s creation` sorts ascending by the ZFS creation timestamp (epoch
301
+ # precision, immune to TODAY timezone drift), so snaps[:surplus] below
302
+ # always slices the OLDEST entries. Never reorder this — destroying
303
+ # newer snapshots first would wipe the recovery floor while leaving
304
+ # stale tails behind.
305
+ try:
306
+ result = subprocess.run(
307
+ ["zfs", "list", "-t", "snapshot", "-H", "-o", "name",
308
+ "-s", "creation", zfs_dataset],
309
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, text=True,
310
+ )
311
+ except subprocess.CalledProcessError as e:
312
+ logit("error", f"Failed to list snapshots for {zfs_dataset}: {e}")
313
+ return
314
+
315
+ prefix = f"{zfs_dataset}@backup-"
316
+ snaps = [line for line in result.stdout.splitlines() if line.startswith(prefix)]
317
+
318
+ surplus = len(snaps) - retention
319
+ if surplus <= 0:
320
+ return
321
+
322
+ for snap in snaps[:surplus]:
323
+ logit("backup", f"pruning old ZFS snapshot {snap} (retention={retention})")
324
+ try:
325
+ subprocess.run(["zfs", "destroy", snap], check=True)
326
+ except subprocess.CalledProcessError as e:
327
+ logit("error", f"Failed to destroy snapshot {snap}: {e}")
328
+
329
+
284
330
  def shutdown(doms, wait=180):
285
331
  """Accept a list of dom objects, attempt to shutdown the active ones"""
286
332
  # get all running guests from list and invoke shutdown
@@ -401,7 +447,13 @@ def logit(context, message, quiet=False):
401
447
 
402
448
 
403
449
  def rotate(target, retention=3):
404
- """file rotation routine"""
450
+ """file rotation routine — shift within window then reap any stale files
451
+ at indices >= retention.
452
+
453
+ Without the reap step, lowering retention (e.g. 5 -> 2) leaves orphan
454
+ .2, .3, .4 around forever — the same accumulation pattern that ZFS
455
+ snapshots had before 0.2.5.
456
+ """
405
457
  for i in range(retention - 2, 0, -1): # count backwards
406
458
  old_name = "%s.%s" % (target, i)
407
459
  new_name = "%s.%s" % (target, i + 1)
@@ -411,6 +463,22 @@ def rotate(target, retention=3):
411
463
  pass
412
464
  move(target, target + ".1")
413
465
 
466
+ # Reap files at indices >= retention. INVARIANT: never touches the
467
+ # current target (no numeric suffix) and never touches indices in
468
+ # [1, retention-1] (the active rotation window).
469
+ for stale in glob.glob("%s.*" % target):
470
+ suffix = stale.rsplit(".", 1)[-1]
471
+ try:
472
+ idx = int(suffix)
473
+ except ValueError:
474
+ continue # not a rotation index (e.g., backup.tar.gz where .gz isn't int)
475
+ if idx >= retention:
476
+ logit("backup", "removing stale backup file %s (retention=%s)" % (stale, retention))
477
+ try:
478
+ remove(stale)
479
+ except OSError as e:
480
+ logit("error", "failed to remove %s: %s" % (stale, e))
481
+
414
482
 
415
483
  def getoptions():
416
484
  """Fetch cli args, parse and map to python, test sanity"""
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: virt-back
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: virt-back: A backup utility for QEMU, KVM, XEN, and Virtualbox guests
5
5
  Home-page: https://git.unturf.com/python/virt-back
6
6
  Author: Russell Ballestrini
@@ -9,6 +9,17 @@ License: Public Domain
9
9
  Keywords: backup virtual hypervisor QEMU KVM XEN Virtualbox
10
10
  Platform: All
11
11
  Description-Content-Type: text/markdown
12
+ Requires-Dist: libvirt-python
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: description
16
+ Dynamic: description-content-type
17
+ Dynamic: home-page
18
+ Dynamic: keywords
19
+ Dynamic: license
20
+ Dynamic: platform
21
+ Dynamic: requires-dist
22
+ Dynamic: summary
12
23
 
13
24
  A backup utility for QEMU, KVM, XEN, and Virtualbox guests.
14
25
 
@@ -39,6 +50,7 @@ options:
39
50
  -g, --no-gzip do not gzip the resulting tar file
40
51
  -a amount, --retention amount
41
52
  backups to retain [default: 3]
53
+ (applies to both rotated backup files AND ZFS @backup-* snapshots)
42
54
  -p 'PATH', --path 'PATH'
43
55
  backup path [default: '/KVMBACK']
44
56
  -u 'URI', --uri 'URI'
@@ -23,6 +23,7 @@ import libvirt
23
23
  import tarfile
24
24
  import syslog
25
25
  import re
26
+ import glob
26
27
  from time import sleep
27
28
  from datetime import date
28
29
  from sys import exit
@@ -195,6 +196,9 @@ def backup(doms):
195
196
  except subprocess.CalledProcessError as e:
196
197
  logit("error", f"Failed to send ZFS snapshot {zfs_snapshot}: {e}")
197
198
  continue
199
+
200
+ # Prune old @backup-* snapshots, keep newest `retention`
201
+ prune_zfs_snapshots(zfs_dataset, options.retention)
198
202
  else:
199
203
  # Handle QCOW2 or other file-based disk
200
204
  logit("backup", f"{disk_source} is not a ZFS dataset")
@@ -281,6 +285,48 @@ def is_zfs_dataset(disk_source):
281
285
  return False
282
286
 
283
287
 
288
+ def prune_zfs_snapshots(zfs_dataset, retention):
289
+ """List @backup-* snapshots on dataset, destroy oldest until count <= retention.
290
+
291
+ Only touches snapshots virt-back created (@backup-* prefix). Never destroys
292
+ user or TrueNAS periodic snapshots. Never destroys more than necessary —
293
+ `retention` snapshots always survive as the recovery floor.
294
+ """
295
+ if retention < 1:
296
+ logit("error", f"retention must be >= 1, got {retention} — skipping prune")
297
+ return
298
+
299
+ # INVARIANT: reclamation eats from the tail (oldest first).
300
+ # `-s creation` sorts ascending by the ZFS creation timestamp (epoch
301
+ # precision, immune to TODAY timezone drift), so snaps[:surplus] below
302
+ # always slices the OLDEST entries. Never reorder this — destroying
303
+ # newer snapshots first would wipe the recovery floor while leaving
304
+ # stale tails behind.
305
+ try:
306
+ result = subprocess.run(
307
+ ["zfs", "list", "-t", "snapshot", "-H", "-o", "name",
308
+ "-s", "creation", zfs_dataset],
309
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, text=True,
310
+ )
311
+ except subprocess.CalledProcessError as e:
312
+ logit("error", f"Failed to list snapshots for {zfs_dataset}: {e}")
313
+ return
314
+
315
+ prefix = f"{zfs_dataset}@backup-"
316
+ snaps = [line for line in result.stdout.splitlines() if line.startswith(prefix)]
317
+
318
+ surplus = len(snaps) - retention
319
+ if surplus <= 0:
320
+ return
321
+
322
+ for snap in snaps[:surplus]:
323
+ logit("backup", f"pruning old ZFS snapshot {snap} (retention={retention})")
324
+ try:
325
+ subprocess.run(["zfs", "destroy", snap], check=True)
326
+ except subprocess.CalledProcessError as e:
327
+ logit("error", f"Failed to destroy snapshot {snap}: {e}")
328
+
329
+
284
330
  def shutdown(doms, wait=180):
285
331
  """Accept a list of dom objects, attempt to shutdown the active ones"""
286
332
  # get all running guests from list and invoke shutdown
@@ -401,7 +447,13 @@ def logit(context, message, quiet=False):
401
447
 
402
448
 
403
449
  def rotate(target, retention=3):
404
- """file rotation routine"""
450
+ """file rotation routine — shift within window then reap any stale files
451
+ at indices >= retention.
452
+
453
+ Without the reap step, lowering retention (e.g. 5 -> 2) leaves orphan
454
+ .2, .3, .4 around forever — the same accumulation pattern that ZFS
455
+ snapshots had before 0.2.5.
456
+ """
405
457
  for i in range(retention - 2, 0, -1): # count backwards
406
458
  old_name = "%s.%s" % (target, i)
407
459
  new_name = "%s.%s" % (target, i + 1)
@@ -411,6 +463,22 @@ def rotate(target, retention=3):
411
463
  pass
412
464
  move(target, target + ".1")
413
465
 
466
+ # Reap files at indices >= retention. INVARIANT: never touches the
467
+ # current target (no numeric suffix) and never touches indices in
468
+ # [1, retention-1] (the active rotation window).
469
+ for stale in glob.glob("%s.*" % target):
470
+ suffix = stale.rsplit(".", 1)[-1]
471
+ try:
472
+ idx = int(suffix)
473
+ except ValueError:
474
+ continue # not a rotation index (e.g., backup.tar.gz where .gz isn't int)
475
+ if idx >= retention:
476
+ logit("backup", "removing stale backup file %s (retention=%s)" % (stale, retention))
477
+ try:
478
+ remove(stale)
479
+ except OSError as e:
480
+ logit("error", "failed to remove %s: %s" % (stale, e))
481
+
414
482
 
415
483
  def getoptions():
416
484
  """Fetch cli args, parse and map to python, test sanity"""
virt-back-0.2.4/setup.py DELETED
@@ -1,47 +0,0 @@
1
- from setuptools import setup
2
- import os
3
-
4
- # Read the contents of your README file
5
- with open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf-8") as f:
6
- long_description = f.read()
7
-
8
- setup(
9
- name="virt-back",
10
- version="0.2.4",
11
- description="virt-back: A backup utility for QEMU, KVM, XEN, and Virtualbox guests",
12
- keywords="backup virtual hypervisor QEMU KVM XEN Virtualbox",
13
- long_description=long_description,
14
- long_description_content_type="text/markdown",
15
- author="Russell Ballestrini",
16
- author_email="russell@ballestrini.net",
17
- url="https://git.unturf.com/python/virt-back",
18
- platforms=["All"],
19
- license="Public Domain",
20
- py_modules=["virtback"],
21
- include_package_data=True,
22
- scripts=["virt-back"],
23
- install_requires=[
24
- "libvirt-python",
25
- ],
26
- )
27
-
28
- """
29
- setup()
30
- keyword args: http://peak.telecommunity.com/DevCenter/setuptools
31
-
32
- # built and uploaded to pypi with this:
33
-
34
- python setup.py sdist
35
- twine upload dist/*
36
-
37
- """
38
-
39
- # Installation Instructions:
40
- # To install virt-back, you can use pip:
41
- # * pip install virt-back
42
- #
43
- # Note: You need to have the libvirt development libraries installed.
44
- # On Fedora/RedHat, you can install it with:
45
- # * sudo dnf install libvirt-devel
46
- # On Ubuntu, you can install it with:
47
- # * sudo apt-get install libvirt-dev
File without changes