Unmount and remove mounted snapshot directories, not just for the current process but for previous borgmatic runs as well (#261).

This commit is contained in:
Dan Helfman 2024-11-21 19:09:30 -08:00
parent 1b2b0c3020
commit ab7acceff6
2 changed files with 51 additions and 9 deletions

View File

@ -33,6 +33,32 @@ def get_borgmatic_source_directory(config):
TEMPORARY_DIRECTORY_PREFIX = 'borgmatic-'
def replace_temporary_subdirectory_with_glob(path):
'''
Given an absolute temporary directory path, look for a subdirectory within it starting with the
temporary directory prefix and replace it with an appropriate glob. For instance, given:
/tmp/borgmatic-aet8kn93/borgmatic
... replace it with:
/tmp/borgmatic-*/borgmatic
This is useful for finding previous temporary directories from prior borgmatic runs.
'''
return os.path.join(
'/',
*(
(
f'{TEMPORARY_DIRECTORY_PREFIX}*'
if subdirectory.startswith(TEMPORARY_DIRECTORY_PREFIX)
else subdirectory
)
for subdirectory in path.split(os.path.sep)
)
)
class Runtime_directory:
'''
A Python context manager for creating and cleaning up the borgmatic runtime directory used for

View File

@ -1,6 +1,8 @@
import glob
import logging
import os
import shlex
import shutil
import subprocess
import borgmatic.config.paths
@ -65,8 +67,7 @@ def dump_data_sources(
(dataset_name, mount_point)
for line in list_output.splitlines()
for (dataset_name, mount_point, user_property_value) in (line.rstrip().split('\t'),)
if mount_point in source_directories_set
or user_property_value == 'auto'
if mount_point in source_directories_set or user_property_value == 'auto'
)
# Snapshot each dataset, rewriting source directories to use the snapshot paths.
@ -97,7 +98,9 @@ def dump_data_sources(
mount_point.lstrip(os.path.sep),
)
snapshot_path = os.path.normpath(snapshot_path_for_borg)
logger.debug(f'{log_prefix}: Mounting ZFS snapshot {full_snapshot_name} at {snapshot_path}{dry_run_label}')
logger.debug(
f'{log_prefix}: Mounting ZFS snapshot {full_snapshot_name} at {snapshot_path}{dry_run_label}'
)
if not dry_run:
os.makedirs(snapshot_path, mode=0o700, exist_ok=True)
@ -154,15 +157,26 @@ def remove_data_source_dumps(hook_config, config, log_prefix, borgmatic_runtime_
for line in list_datasets_output.splitlines()
for (dataset_name, mount_point) in (line.rstrip().split('\t'),)
)
# FIXME: This doesn't necessarily find snapshot mounts from previous borgmatic runs, because
# borgmatic_runtime_directory could be in a tempfile-created directory that has a random name.
snapshots_directory = os.path.join(
os.path.normpath(borgmatic_runtime_directory),
snapshots_glob = os.path.join(
borgmatic.config.paths.replace_temporary_subdirectory_with_glob(
os.path.normpath(borgmatic_runtime_directory)
),
'zfs_snapshots',
)
logger.debug(f'{log_prefix}: Looking for snapshots to remove in {snapshots_directory}{dry_run_label}')
logger.debug(
f'{log_prefix}: Looking for snapshots to remove in {snapshots_glob}{dry_run_label}'
)
for snapshots_directory in glob.glob(snapshots_glob):
if not os.path.isdir(snapshots_directory):
continue
# This might fail if the directory is already mounted, but we swallow errors here since
# we'll try again below. The point of doing it here is that we don't want to try to unmount
# a non-mounted directory (which *will* fail), and probing for whether a directory is
# mounted is tough to do in a cross-platform way.
shutil.rmtree(snapshots_directory, ignore_errors=True)
if os.path.isdir(snapshots_directory):
for mount_point in mount_points:
snapshot_path = os.path.join(snapshots_directory, mount_point.lstrip(os.path.sep))
logger.debug(f'{log_prefix}: Unmounting ZFS snapshot at {snapshot_path}{dry_run_label}')
@ -176,6 +190,8 @@ def remove_data_source_dumps(hook_config, config, log_prefix, borgmatic_runtime_
output_log_level=logging.DEBUG,
)
shutil.rmtree(snapshots_directory)
# Destroy snapshots.
list_snapshots_command = (
zfs_command,