mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-30 05:13:01 +02:00
Initial WIP-level work on supporting snapshots of "raw" Btrfs subvolumes.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import collections
|
||||
import glob
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -21,9 +22,36 @@ def use_streaming(hook_config, config): # pragma: no cover
|
||||
return False
|
||||
|
||||
|
||||
def get_subvolume_mount_points(findmnt_command):
|
||||
def list_subvolume_paths(btrfs_command, subvolume_path):
|
||||
'''
|
||||
Given a findmnt command to run, get all sorted Btrfs subvolume mount points.
|
||||
Given the path of a Btrfs subvolume, return it in a sequence with the paths of its contained
|
||||
subvolumes.
|
||||
|
||||
If the Btrfs command errors, log that error and return an empty sequence.
|
||||
'''
|
||||
try:
|
||||
btrfs_output = borgmatic.execute.execute_command_and_capture_output(
|
||||
tuple(btrfs_command.split(' ')) + ('subvolume', 'list', subvolume_path,),
|
||||
close_fds=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as error:
|
||||
logger.debug(error)
|
||||
|
||||
return ()
|
||||
|
||||
return (subvolume_path,) + tuple(
|
||||
os.path.join(subvolume_path, line.split(' ')[-1])
|
||||
for line in btrfs_output.splitlines()
|
||||
)
|
||||
|
||||
|
||||
FINDMNT_BTRFS_ROOT_SUBVOLUME_OPTION = 'subvolid=5'
|
||||
|
||||
|
||||
def get_all_subvolume_paths(btrfs_command, findmnt_command):
|
||||
'''
|
||||
Given btrfs and findmnt commands to run, get the sorted paths for all Btrfs subvolumes on the
|
||||
system.
|
||||
'''
|
||||
findmnt_output = borgmatic.execute.execute_command_and_capture_output(
|
||||
tuple(findmnt_command.split(' '))
|
||||
@@ -38,7 +66,20 @@ def get_subvolume_mount_points(findmnt_command):
|
||||
|
||||
try:
|
||||
return tuple(
|
||||
sorted(filesystem['target'] for filesystem in json.loads(findmnt_output)['filesystems'])
|
||||
sorted(
|
||||
itertools.chain.from_iterable(
|
||||
# If findmnt gave us a Btrfs root filesystem, list the subvolumes within it.
|
||||
# This is necessary because findmnt only returns a subvolume's mount point
|
||||
# rather than its original subvolume path (which can differ). For instance,
|
||||
# a subvolume might exist at /mnt/subvolume but be mounted at /home/myuser.
|
||||
# findmnt is still useful though because it's a global way to discover all
|
||||
# Btrfs subvolumes—even if we have to do some additional legwork ourselves.
|
||||
list_subvolume_paths(btrfs_command, filesystem['target']) if
|
||||
FINDMNT_BTRFS_ROOT_SUBVOLUME_OPTION in filesystem['options'].split(',')
|
||||
else (filesystem['target'],)
|
||||
for filesystem in json.loads(findmnt_output)['filesystems']
|
||||
)
|
||||
)
|
||||
)
|
||||
except json.JSONDecodeError as error:
|
||||
raise ValueError(f'Invalid {findmnt_command} JSON output: {error}')
|
||||
@@ -74,11 +115,11 @@ def get_subvolume_property(btrfs_command, subvolume_path, property_name):
|
||||
}.get(value, value)
|
||||
|
||||
|
||||
def omit_read_only_subvolume_mount_points(btrfs_command, subvolume_paths):
|
||||
def omit_read_only_subvolume_paths(btrfs_command, subvolume_paths):
|
||||
'''
|
||||
Given a Btrfs command to run and a sequence of Btrfs subvolume mount points, filter them down to
|
||||
just those that are read-write. The idea is that Btrfs can't actually snapshot a read-only
|
||||
subvolume, so we should just ignore them.
|
||||
Given a Btrfs command to run and a sequence of Btrfs subvolume paths, filter them down to just
|
||||
those that are read-write. The idea is that Btrfs can't actually snapshot a read-only subvolume,
|
||||
so we should just ignore them.
|
||||
'''
|
||||
retained_subvolume_paths = []
|
||||
|
||||
@@ -94,32 +135,32 @@ def omit_read_only_subvolume_mount_points(btrfs_command, subvolume_paths):
|
||||
def get_subvolumes(btrfs_command, findmnt_command, patterns=None):
|
||||
'''
|
||||
Given a Btrfs command to run and a sequence of configured patterns, find the intersection
|
||||
between the current Btrfs filesystem and subvolume mount points and the paths of any patterns.
|
||||
The idea is that these pattern paths represent the requested subvolumes to snapshot.
|
||||
between the current Btrfs filesystem and subvolume paths and the paths of any patterns. The
|
||||
idea is that these pattern paths represent the requested subvolumes to snapshot.
|
||||
|
||||
Only include subvolumes that contain at least one root pattern sourced from borgmatic
|
||||
configuration (as opposed to generated elsewhere in borgmatic). But if patterns is None, then
|
||||
return all subvolumes instead, sorted by path.
|
||||
|
||||
Return the result as a sequence of matching subvolume mount points.
|
||||
Return the result as a sequence of matching Subvolume instances.
|
||||
'''
|
||||
candidate_patterns = set(patterns or ())
|
||||
subvolumes = []
|
||||
|
||||
# For each subvolume mount point, match it against the given patterns to find the subvolumes to
|
||||
# For each subvolume path, match it against the given patterns to find the subvolumes to
|
||||
# backup. Sort the subvolumes from longest to shortest mount points, so longer mount points get
|
||||
# a whack at the candidate pattern piñata before their parents do. (Patterns are consumed during
|
||||
# this process, so no two subvolumes end up with the same contained patterns.)
|
||||
for mount_point in reversed(
|
||||
omit_read_only_subvolume_mount_points(
|
||||
btrfs_command, get_subvolume_mount_points(findmnt_command)
|
||||
for subvolume_path in reversed(
|
||||
omit_read_only_subvolume_paths(
|
||||
btrfs_command, get_all_subvolume_paths(btrfs_command, findmnt_command)
|
||||
)
|
||||
):
|
||||
subvolumes.extend(
|
||||
Subvolume(mount_point, contained_patterns)
|
||||
Subvolume(subvolume_path, contained_patterns)
|
||||
for contained_patterns in (
|
||||
borgmatic.hooks.data_source.snapshot.get_contained_patterns(
|
||||
mount_point, candidate_patterns
|
||||
subvolume_path, candidate_patterns
|
||||
),
|
||||
)
|
||||
if patterns is None
|
||||
@@ -244,9 +285,9 @@ def dump_data_sources(
|
||||
'''
|
||||
Given a Btrfs configuration dict, a configuration dict, the borgmatic configuration file paths,
|
||||
the borgmatic runtime directory, the configured patterns, and whether this is a dry run,
|
||||
auto-detect and snapshot any Btrfs subvolume mount points listed in the given patterns. Also
|
||||
update those patterns, replacing subvolume mount points with corresponding snapshot directories
|
||||
so they get stored in the Borg archive instead.
|
||||
auto-detect and snapshot any Btrfs subvolume paths listed in the given patterns. Also update
|
||||
those patterns, replacing subvolume paths with corresponding snapshot directories so they get
|
||||
stored in the Borg archive instead.
|
||||
|
||||
Return an empty sequence, since there are no ongoing dump processes from this hook.
|
||||
|
||||
@@ -328,8 +369,8 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d
|
||||
logger.debug(error)
|
||||
return
|
||||
|
||||
# Reversing the sorted subvolumes ensures that we remove longer mount point paths of child
|
||||
# subvolumes before the shorter mount point paths of parent subvolumes.
|
||||
# Reversing the sorted subvolumes ensures that we remove longer paths of child subvolumes before
|
||||
# the shorter paths of parent subvolumes.
|
||||
for subvolume in reversed(all_subvolumes):
|
||||
subvolume_snapshots_glob = borgmatic.config.paths.replace_temporary_subdirectory_with_glob(
|
||||
os.path.normpath(make_snapshot_path(subvolume.path)),
|
||||
|
||||
Reference in New Issue
Block a user