255 lines
8.4 KiB
Python
Executable File
255 lines
8.4 KiB
Python
Executable File
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from subprocess import Popen, PIPE, check_output
|
|
import config
|
|
from sys import exit
|
|
import re
|
|
|
|
|
|
"""
|
|
Erstes Backup muss für alle notwendigen Datasets durchgeführt werden mit
|
|
# zfs send -Rv rpool/ROOT@zfs-auto-snap_daily-2018-06-14-1648 | zfs receive -dvF backup1
|
|
"""
|
|
|
|
|
|
class Command:
|
|
def __init__(self, command, strict=True):
|
|
process = Popen(command, stdout=PIPE)
|
|
self.stdout, self.stderr = process.communicate()
|
|
self.exit_code = process.wait()
|
|
if strict and self.exit_code != 0:
|
|
raise Exception("Kritisches Kommando fehlgeschlagen. Breche ab.")
|
|
|
|
|
|
def find_backup_pool():
|
|
print("Suche nach externer Festplatte für Backups")
|
|
present_uuids = Command(["ls", "/dev/disk/by-uuid"])
|
|
for key in list(config.backup_pool.keys()):
|
|
uuid = config.backup_pool[key]
|
|
if uuid in present_uuids.stdout.decode():
|
|
print("Gefundene Platte: " + key)
|
|
return key
|
|
else:
|
|
raise IOError("Konnte keine Backup-Platte finden!")
|
|
|
|
|
|
class Dataset:
|
|
def __init__(self, name: str):
|
|
self.name = name
|
|
|
|
def __repr__(self):
|
|
return self.name
|
|
|
|
@classmethod
|
|
def from_local_pool(cls) -> list[Dataset]:
|
|
datasets_str = (
|
|
Command([config.ZFS, "list", "-H", "-d", "1", "-o", "name", config.local_pool])
|
|
.stdout.strip()
|
|
.decode()
|
|
)
|
|
# we omit the first return value, which is the pool, not the dataset
|
|
datasets_list = datasets_str.split("\n")[1:]
|
|
to_backup = [Dataset(d) for d in datasets_list if d not in config.do_not_backup]
|
|
print("Folgende Datasets und alle darunter befindlichen Kinder werden gesichert:")
|
|
for item in to_backup:
|
|
print(" " + str(item))
|
|
return to_backup
|
|
|
|
|
|
def get_snapshots(pool):
|
|
return (
|
|
Command([config.ZFS, "list", "-r", "-t", "snapshot", "-o", "name", "-H", pool])
|
|
.stdout.strip()
|
|
.decode()
|
|
.split("\n")
|
|
)
|
|
|
|
|
|
def last_snapshot(pool):
|
|
snapshots = get_snapshots(pool)
|
|
# check if the snapshot_tags appear in the snapshot name in order they are provided
|
|
relevant = [
|
|
s for s in snapshots if re.search("@" + ".*".join(config.snapshot_tag), s)
|
|
]
|
|
# sort chronologically
|
|
relevant.sort()
|
|
# the most recent snapshot is the last one
|
|
try:
|
|
last_entry = relevant[-1]
|
|
# didn't find any snapshots at all
|
|
except IndexError:
|
|
return
|
|
last_snapshot = last_entry.strip().split("@")[-1]
|
|
return last_snapshot
|
|
|
|
|
|
def export_pool(pool):
|
|
container_name = "crypt-" + pool
|
|
print("Exportiere Pool " + pool)
|
|
Command([config.ZPOOL, "export", pool])
|
|
print("Schließe verschlüsselten Container")
|
|
Command([config.CRYPTSETUP, "close", container_name])
|
|
print("Fertig!")
|
|
print("Sie können die externe Festplatte jetzt entfernen.")
|
|
|
|
|
|
def import_pool(pool):
|
|
uuid = config.backup_pool[pool]
|
|
container_name = "crypt-" + pool
|
|
present_mappers = Command(["ls", "/dev/mapper/"]).stdout.decode().strip()
|
|
if container_name in present_mappers:
|
|
msg = (
|
|
"verschlüsselter Container "
|
|
+ container_name
|
|
+ " scheint bereits in Verwendung zu sein. Breche ab."
|
|
)
|
|
raise Exception(msg)
|
|
print("Öffne verschlüsselten Container\n")
|
|
Command([config.CRYPTSETUP, "open", "/dev/disk/by-uuid/" + uuid, container_name])
|
|
print("")
|
|
print("Importiere Pool " + pool)
|
|
# -N: no mount
|
|
# -d: directory to search the pool in
|
|
Command([config.ZPOOL, "import", "-N", "-d", "/dev/mapper", pool])
|
|
|
|
|
|
def present_locally(snapshot):
|
|
local_snapshots = get_snapshots(config.local_pool)
|
|
for each_snapshot in local_snapshots:
|
|
if snapshot in each_snapshot:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def do_backup(dataset, last_local_snapshot, last_remote_snapshot, backup_pool):
|
|
print("Sichere Dataset " + dataset)
|
|
# print(dataset, last_local_snapshot, backup_pool, last_remote_snapshot)
|
|
# return
|
|
if not snapshot_present_in_dataset(dataset, last_remote_snapshot, backup_pool):
|
|
print(" Dieses Dataset wurde nicht zur Sicherung eingerichtet.")
|
|
print(" Verwende ein Kommando in der Art")
|
|
print(
|
|
" # zfs send -Rv rpool/[DATASET]@zfs-auto-snap_daily-yyyy-mm-dd-hhmm | zfs receive -dvF backupN"
|
|
)
|
|
return
|
|
if last_local_snapshot == last_remote_snapshot:
|
|
print(
|
|
" Letzter lokaler Snapshot ist identisch mit letzten entferntem Snapshot"
|
|
)
|
|
print(" Führe kein Backup für dieses Dataset durch")
|
|
return
|
|
send = Popen(
|
|
[
|
|
config.ZFS,
|
|
"send",
|
|
"-R",
|
|
"-I",
|
|
last_remote_snapshot,
|
|
dataset + "@" + last_local_snapshot,
|
|
],
|
|
stdout=PIPE,
|
|
)
|
|
# -d Discard the first element of the sent snapshot's file system name
|
|
# -F Force a rollback of the file system to the most recent snapshot before performing the receive operation.
|
|
# -u File system that is associated with the received stream is not mounted.
|
|
check_output([config.ZFS, "receive", "-d", "-F", "-u", backup_pool], stdin=send.stdout)
|
|
exit_code = send.wait()
|
|
if exit_code != 0:
|
|
raise Exception("Kritisches Kommando fehlgeschlagen. Breche ab.")
|
|
|
|
|
|
def snapshot_present_in_dataset(dataset, last_snapshot, pool):
|
|
snapshots = get_snapshots(pool)
|
|
# get just the dataset name without the pool
|
|
actual_dataset = dataset.split("/")[-1]
|
|
for snapshot in snapshots:
|
|
if (actual_dataset + "@" + last_snapshot) in snapshot:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def clean_old_snapshots(dataset, pool):
|
|
snapshots = get_snapshots(pool) # full name like 'pool/dataset@snapshot'
|
|
# get just the dataset name without the pool
|
|
actual_dataset = dataset.split("/")[-1]
|
|
# print('actual_dataset', actual_dataset)
|
|
for interval in config.keep.keys():
|
|
# print('interval', interval)
|
|
# e.g. 'data@zfs-auto-snap_daily', 'ubuntu@zfs-auto-snap_hourly', ...
|
|
pattern = actual_dataset + "@" + config.snapshot_tag[0] + ".*" + interval
|
|
snapshots_in_interval = []
|
|
for snapshot in snapshots:
|
|
result = re.search(pattern, snapshot)
|
|
if result is not None:
|
|
snapshots_in_interval.append(snapshot)
|
|
snapshots_in_interval.sort()
|
|
for i in range(len(snapshots_in_interval) - config.keep[interval]):
|
|
to_delete = snapshots_in_interval[i]
|
|
print(" Lösche veralteten Snapshot {}".format(to_delete))
|
|
zfs_destroy(to_delete, recursive=True)
|
|
|
|
|
|
def zfs_destroy(full_name, recursive=False):
|
|
if config.local_pool in full_name:
|
|
msg = "Wollte in falschem Pool löschen! Dies sollte niemals passieren! Abbruch!"
|
|
raise Exception(msg)
|
|
else:
|
|
cmdline = [config.ZFS, "destroy"]
|
|
if recursive:
|
|
cmdline.append("-r")
|
|
cmdline.append(full_name)
|
|
Command(cmdline)
|
|
|
|
|
|
def main():
|
|
verify_running_as_root()
|
|
datasets = Dataset.from_local_pool()
|
|
backup_pool = find_backup_pool()
|
|
import_pool(backup_pool)
|
|
try:
|
|
send_datasets_to_backup_pool(backup_pool, datasets)
|
|
finally:
|
|
export_pool(backup_pool)
|
|
|
|
|
|
def send_datasets_to_backup_pool(backup_pool, datasets):
|
|
last_local_snapshot = last_snapshot(config.local_pool)
|
|
if not last_local_snapshot:
|
|
print("Es scheint keine Snapshots im Speicherpool zu geben")
|
|
print("Breche ab.")
|
|
export_pool(backup_pool)
|
|
exit()
|
|
last_remote_snapshot = last_snapshot(backup_pool)
|
|
if not last_remote_snapshot:
|
|
print("Es scheint keine Snapshots auf der Backup-Festplatte zu geben")
|
|
print("Breche ab.")
|
|
export_pool(backup_pool)
|
|
exit()
|
|
if not present_locally(last_remote_snapshot):
|
|
print(
|
|
"""
|
|
Kann keine inkrementelle Sicherung durchführen, da es im Backup und auf dem Server
|
|
keinen gemeinsamen Snapshot gibt. Vermutlich wurde diese externe Festplatte vor
|
|
zu langer Zeit das letzte mal als Backup verwendet"""
|
|
)
|
|
return
|
|
for dataset in datasets:
|
|
do_backup(dataset, last_local_snapshot, last_remote_snapshot, backup_pool)
|
|
clean_old_snapshots(dataset, backup_pool)
|
|
|
|
|
|
def verify_running_as_root():
|
|
if os.getuid() > 0:
|
|
print("Muss als root ausgeführt werden.")
|
|
exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|