Skip to content

Commit

Permalink
Implement sync.btrfssnap recursive=true
Browse files Browse the repository at this point in the history
* factorize the command passing
* store snaps in each <subvol>/.snap/<rfc3339 utc datetime>[,<name>]
  so users can access the snaps even if shown only the subvol
* skip snaps for ro subvolumes (can't create a .snap, and makes no
  sense to snapshot a immutable dataset)
  • Loading branch information
cvaroqui committed Mar 20, 2023
1 parent 6f03dd1 commit 4069669
Showing 1 changed file with 85 additions and 45 deletions.
130 changes: 85 additions & 45 deletions opensvc/drivers/resource/sync/btrfssnap/__init__.py
Expand Up @@ -8,6 +8,7 @@
from env import Env
from core.objects.svcdict import KEYS
from utilities.proc import justcall
from utilities.files import makedirs

DRIVER_GROUP = "sync"
DRIVER_BASENAME = "btrfssnap"
Expand All @@ -34,6 +35,14 @@
"example": "3",
"text": "The maximum number of snapshots to retain."
},
{
"keyword": "recursive",
"at": True,
"default": False,
"convert": "boolean",
"candidates": [True, False],
"text": "Also replicate subvolumes in the src tree."
},
]

KEYS.register_driver(
Expand All @@ -55,6 +64,7 @@ def __init__(self,
name=None,
subvol=None,
keep=1,
recursive=False,
**kwargs):
super(SyncBtrfssnap, self).__init__(type="sync.btrfssnap", **kwargs)

Expand All @@ -67,6 +77,7 @@ def __init__(self,
self.subvol = subvol
self.keep = keep
self.name = name
self.recursive = recursive
self.btrfs = {}

def on_add(self):
Expand Down Expand Up @@ -118,52 +129,80 @@ def get_btrfs(self, label):
raise ex.Error(str(e))
return self.btrfs[label]

def src(self, label, path):
return os.path.join(self.btrfs[label].rootdir, path)

def subvols(self, label, path):
"""
sort by path so the subvols are sorted by path depth
"""
btr = self.get_btrfs(label)
src = self.src(label, path)
if not src:
return []
if not self.recursive:
sub = btr.get_subvol(src)
if not sub:
return []
return [sub]
subvols = []
for subvol in btr.get_subvols().values():
if subvol["path"] == path:
subvols.append(subvol)
elif subvol["path"].startswith(path + "/"):
subvols.append(subvol)
subvols = sorted(subvols, key=lambda x: x["path"])
return subvols

def create_snaps(self, label, subvol):
cmds = []
for sv in self.subvols(label, subvol):
if "/.snap/" in sv["path"]:
continue
cmds += self.create_snap(label, sv["path"])
return cmds

def create_snap(self, label, subvol):
btrfs = self.get_btrfs(label)
orig = os.path.join(btrfs.rootdir, subvol)
snap = os.path.join(btrfs.rootdir, subvol)
snap += "/.snap/"
snap += datetime.datetime.utcnow().isoformat("T")+"Z"
if self.name:
suffix = "."+self.name
else:
suffix = ""
suffix += ".snap.%Y-%m-%d.%H:%M:%S"
snap += datetime.datetime.now().strftime(suffix)
snap += "," + self.name
try:
btrfs.snapshot(orig, snap, readonly=True)
except utilities.subsystems.btrfs.ExistError:
raise ex.Error('%s should not exist'%snap)
except utilities.subsystems.btrfs.ExecError:
raise ex.Error
makedirs(os.path.dirname(snap))
except OSError as e:
self.log.debug("skip %s snap: readonly", subvol)
return []
cmd = btrfs.snapshot_cmd(orig, snap, readonly=True)
if not cmd:
return []
return [" ".join(cmd)]

def remove_snap(self, label, subvol):
def remove_snaps(self, label, subvol):
btrfs = self.get_btrfs(label)
btrfs.get_subvols(refresh=True)
btrfs.get_subvols()
snaps = {}
for sv in btrfs.subvols.values():
if not sv["path"].startswith(subvol):
continue
s = sv["path"].replace(subvol, "")
l = s.split('.')
if len(l) < 2:
continue
if l[1] not in ("snap", self.name):
continue
for sv in self.subvols(label, subvol+"/.snap"):
ds = sv["path"].replace(subvol+"/.snap/", "")
ds = ds.split(",")[0] # discard optional name
try:
ds = sv["path"].split(".snap.")[-1]
d = datetime.datetime.strptime(ds, "%Y-%m-%d.%H:%M:%S")
d = datetime.datetime.strptime(ds, "%Y-%m-%dT%H:%M:%S.%fZ")
snaps[ds] = sv["path"]
except Exception as e:
pass
if len(snaps) <= self.keep:
return
return []
sorted_snaps = []
for ds in sorted(snaps.keys(), reverse=True):
sorted_snaps.append(snaps[ds])
cmds = []
for path in sorted_snaps[self.keep:]:
try:
btrfs.subvol_delete(os.path.join(btrfs.rootdir, path))
except utilities.subsystems.btrfs.ExecError:
raise ex.Error
cmd = btrfs.subvol_delete_cmd(os.path.join(btrfs.rootdir, path))
if cmd:
cmds.append(" ".join(cmd))
return cmds

def _status_one(self, label, subvol):
if self.test_btrfs(label) != 0:
Expand All @@ -174,23 +213,14 @@ def _status_one(self, label, subvol):
except Exception as e:
self.status_log("%s:%s %s" % (label, subvol, str(e)))
return
try:
btrfs.get_subvols()
except:
return
snaps = []
for sv in btrfs.subvols.values():
if not sv["path"].startswith(subvol + '.'):
continue
s = sv["path"].replace(subvol, "")
l = s.split('.')
if len(l) < 2:
continue
if l[1] not in ("snap", self.name):
for sv in self.subvols(label, subvol):
if "/.snap/" not in subvol["path"]:
continue
ds = sv["path"].replace(subvol+"/.snap/", "")
ds = ds.split(",")[0] # discard optional name
try:
ds = sv["path"].split(".snap.")[-1]
d = datetime.datetime.strptime(ds, "%Y-%m-%d.%H:%M:%S")
d = datetime.datetime.strptime(ds, "%Y-%m-%dT%H:%M:%S.%fZ")
snaps.append(d)
except Exception as e:
pass
Expand All @@ -202,7 +232,7 @@ def _status_one(self, label, subvol):
last = sorted(snaps, reverse=True)[0]
limit = datetime.datetime.now() - datetime.timedelta(seconds=self.sync_max_delay)
if last < limit:
self.status_log("%s:%s last snap is too old (%s)" % (label, subvol, last.strftime("%Y-%m-%d %H:%M:%S")))
self.status_log("%s:%s last snap is too old (%s)" % (label, subvol, last.strftime("%Y-%m-%dT%H:%M:%S.%fZ")))

def _status(self, verbose=False):
for s in self.subvol:
Expand Down Expand Up @@ -231,8 +261,18 @@ def _sync_update(self, s):
if self.test_btrfs(label) != 0:
self.log.info("skip snap of %s while the btrfs is no writable"%label)
return
self.create_snap(label, subvol)
self.remove_snap(label, subvol)
cmds = []
cmds += self.create_snaps(label, subvol)
cmds += self.remove_snaps(label, subvol)
if not cmds:
return
self.do_cmds(label, cmds)

def do_cmds(self, label, cmds):
o = self.get_btrfs(label)
ret, out, err = o.vcall(" && ".join(cmds), shell=True)
if ret != 0:
raise ex.Error

@notify
def sync_update(self):
Expand Down

0 comments on commit 4069669

Please sign in to comment.