From e7f6993ef81ee6e50c616dcf9b20f185b044301b Mon Sep 17 00:00:00 2001 From: Juergen Kellerer Date: Fri, 13 Aug 2021 20:50:36 +0200 Subject: [PATCH 1/8] Added default config for unix packages --- contrib/posix/README.md | 40 ++++ contrib/posix/conf.d/check.conf | 58 ++++++ contrib/posix/conf.d/hooks.conf | 19 ++ contrib/posix/conf.d/metrics.conf | 15 ++ contrib/posix/conf.d/prune.conf | 32 +++ contrib/posix/conf.d/repository.conf | 67 +++++++ contrib/posix/profiles.conf | 183 ++++++++++++++++++ .../posix/profiles.d/fs-snapshot.yaml.sample | 54 ++++++ contrib/posix/profiles.d/system.toml | 38 ++++ 9 files changed, 506 insertions(+) create mode 100644 contrib/posix/README.md create mode 100644 contrib/posix/conf.d/check.conf create mode 100644 contrib/posix/conf.d/hooks.conf create mode 100644 contrib/posix/conf.d/metrics.conf create mode 100644 contrib/posix/conf.d/prune.conf create mode 100644 contrib/posix/conf.d/repository.conf create mode 100644 contrib/posix/profiles.conf create mode 100644 contrib/posix/profiles.d/fs-snapshot.yaml.sample create mode 100644 contrib/posix/profiles.d/system.toml diff --git a/contrib/posix/README.md b/contrib/posix/README.md new file mode 100644 index 000000000..a5b4e102f --- /dev/null +++ b/contrib/posix/README.md @@ -0,0 +1,40 @@ +# Default configuration for POSIX systems + +**Layout for `/etc/resticprofile`**: + +* `profiles.conf` - host centric default configuration +* `profiles.d/*` - host centric backup profiles (`*.toml` & `*.yaml`) +* `conf.d/*` - overrides & extra configuration + +The layout is used in `deb`, `rpm` and `apk` packages of `resticprofile` + +**Generated files**: +* `conf.d/default-repository.secret` - during installation, only if missing + +**Referenced files and paths**: +* `conf.d/default-repository-self-signed-pub.pem` - TLS public cert (self-signed only) +* `conf.d/default-repository-client.pem` - TLS client cert +* `/var/lib/prometheus/node-exporter/resticprofile-*.prom` - Prometheus files +* `$TMPDIR/resticprofile-*` - Status and lock files + +# Quick Start + +## Installation + +* RPM: `rpm -i "resticprofile-VERSION-ARCH.rpm"` +* DEB: `dpkg -i "resticprofile-VERSION-ARCH.deb"` + +## Configuration +Setup repository and validate system backup profile: +```shell +cd /etc/resticprofile/ +vim conf.d/repository.conf +vim profiles.d/system.toml +``` + +## Test config and backup +```shell +resticprofile -n root show +resticprofile -n root --dry-run backup +resticprofile -n root backup +``` diff --git a/contrib/posix/conf.d/check.conf b/contrib/posix/conf.d/check.conf new file mode 100644 index 000000000..e9ca13702 --- /dev/null +++ b/contrib/posix/conf.d/check.conf @@ -0,0 +1,58 @@ + +## +# Groups for profiles that check & verify repositories +# Usage +# - `resticprofile -n check-all` +# - `resticprofile -n check-all schedule` +# - `resticprofile -n check-all unschedule` +# - `resticprofile -n verify-all` +# - `resticprofile -n verify-all schedule` +# - `resticprofile -n verify-all unschedule` +[groups] +check-all = [ "check" ] +verify-all = [ "verify" ] + + +## +# Profile "check" may be used to schedule repository checks +# +# Usage +# - `resticprofile -n check` +# - `resticprofile -n check schedule` +# - `resticprofile -n check unschedule` +# +[check] +# Operate on the pepository defined in the "base" profile +inherit = "base" +initialize = false +default-command = "check" + +# Configuring the "check" command in profile "check" +[check.check] +schedule = "daily" +schedule-lock-wait = "4h" + + +## +# Profile "verify" may be used to schedule deep repository checks +# +# Usage +# - `resticprofile -n verify` +# - `resticprofile -n verify schedule` +# - `resticprofile -n verify unschedule` +# +[verify] +# Operate on the pepository defined in the "base" profile +inherit = "base" +initialize = false +default-command = "check" + +# Configuring the "check" command in profile "verify" +[verify.check] +schedule = "monthly" +schedule-lock-wait = "48h" +# Read the entire repository for verification +read-data = true +# Read a subset of the repository for verification +#read-data-subset = "15%" + diff --git a/contrib/posix/conf.d/hooks.conf b/contrib/posix/conf.d/hooks.conf new file mode 100644 index 000000000..0c5725e3a --- /dev/null +++ b/contrib/posix/conf.d/hooks.conf @@ -0,0 +1,19 @@ + +## +# Action hooks for profiles that derive from "base" +[base] +# Actions to run before any profile task +#run-before = [ +# 'echo ">>> ${PROFILE_NAME} - BEGIN ${PROFILE_COMMAND}"', +#] + +# Actions to run after a profile task +#run-after = [ +# 'echo "<<< ${PROFILE_NAME} - END ${PROFILE_COMMAND}"', +#] + +# Actions to run when a profile task has failed +run-after-fail = [ + 'echo "!!! ${PROFILE_NAME} - FAILED ${PROFILE_COMMAND}" - ERROR: ${ERROR}"', + # 'resticprofile-send-error admin@localhost', +] diff --git a/contrib/posix/conf.d/metrics.conf b/contrib/posix/conf.d/metrics.conf new file mode 100644 index 000000000..8d99b48d0 --- /dev/null +++ b/contrib/posix/conf.d/metrics.conf @@ -0,0 +1,15 @@ + + +## +# Metric collection for profiles that derive from "base" +[base.backup] +# Toggles full "restic" output capture to allow collecting backup metrics +# for "status-file" and "prometheus-(save-to-file|push)" +#extended-status = true + +# Write backup metrics as JSON (requires extended-status = true) +#status-file = "{{.TempDir}}/resticprofile-{{.Profile.Name}}-status.json" + +# Export backup metrics to Prometheus (requires extended-status = true) +#prometheus-save-to-file = "/var/lib/prometheus/node-exporter/{{.Profile.Name}}.prom" +#prometheus-push = "http://host:9091/" diff --git a/contrib/posix/conf.d/prune.conf b/contrib/posix/conf.d/prune.conf new file mode 100644 index 000000000..0397a855d --- /dev/null +++ b/contrib/posix/conf.d/prune.conf @@ -0,0 +1,32 @@ + +## +# Group for profiles that prune repositories +# Usage +# - `resticprofile -n prune-all` +# - `resticprofile -n prune-all schedule` +# - `resticprofile -n prune-all unschedule` +[groups] +prune-all = [ "prune" ] + + +## +# Profile "prune" may be used to schedule pruning of the default repository +# which reclaims space that is no longer occupied by removed snapshots from +# profiles that use this repository. +# +# Usage +# - `resticprofile -n prune` +# - `resticprofile -n prune schedule` +# - `resticprofile -n prune unschedule` +# +[prune] +# Operate on the pepository defined in the "base" profile +inherit = "base" +initialize = false +default-command = "prune" + +# Configuring the "prune" command in profile "prune" +[prune.prune] +schedule = "daily" +schedule-lock-wait = "4h" + diff --git a/contrib/posix/conf.d/repository.conf b/contrib/posix/conf.d/repository.conf new file mode 100644 index 000000000..a43dfa4fd --- /dev/null +++ b/contrib/posix/conf.d/repository.conf @@ -0,0 +1,67 @@ + +## +# Repository configuration +# See https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html +# + +## +# Default repository (used in all derived profiles unless redefined) +[default] +# Local: Repository mounted to local folder +repository = "local:/backup" +#run-before = [ 'mountpoint -q /backup' ] + +# SFTP: (requires password-less public-key auth for the user running restic) +#repository = "sftp:user@host:/restic-repo" +#repository = "sftp://user@[::1]:2222//restic-repo" + +# REST server: (https://github.com/restic/rest-server) +#repository = "rest:https://user:pass@host:8000/my_backup_repo/" +#cacert = "conf.d/default-repository-self-signed-pub.pem" +#tls-client-cert = "conf.d/default-repository-client.pem" + +# S3 storage (see [default.env]) +#repository = "s3:s3.amazonaws.com/bucket_name" +#repository = "s3:http://host:9000/bucket_name" +#repository = "s3:https://host/bucket_name" +#cacert = "conf.d/default-repository-self-signed-pub.pem" + +# Azure storage (see [default.env]) +#repository = "azure:container_name:/" + +# Repository password file +password-file = "conf.d/default-repository.secret" + +## +# Environment variables to pass to "restic" +[default.env] +# S3 Storage (AWS, Minio, etc.) +#AWS_ACCESS_KEY_ID = "id" +#AWS_SECRET_ACCESS_KEY = "key" + +# Azure Blob Storage +#AZURE_ACCOUNT_NAME = "storage_account" +#AZURE_ACCOUNT_KEY = "key" + + +## +# Initialize the repository (if empty) for profiles deriving from "base" +[base] +# Initialize a repository if none exists at the specified location +initialize = true + + +## +# Example: Secondary repository +# Other repository for profiles inheriting from "other-repository-base": +# +# [other-repository-base] +# inherit = "base" +# repository = "local:/backup-other" +# +# Usage: +# [my-profile-other] +# inherit = "other-repository-base" +# [my-profile-other.backup] +# source = "/path" +# diff --git a/contrib/posix/profiles.conf b/contrib/posix/profiles.conf new file mode 100644 index 000000000..5fe96997b --- /dev/null +++ b/contrib/posix/profiles.conf @@ -0,0 +1,183 @@ + +# ----------------------------------------------------------------------------- +# Resticprofile backup profiles for "restic" backups +# See https://github.com/creativeprojects/resticprofile +# +# Note: This configuration file should not be changed directly. +# Add overrides to "conf.d" and profiles to "profiles.d". +# ----------------------------------------------------------------------------- + +## +# Loading config overrides from "conf.d" and profiles from "profiles.d": +includes = [ + 'conf.d/*.conf', + 'profiles.d/*.toml', + 'profiles.d/*.yaml', +] + + +## +# The global section +[global] +# initialize a repository if none exists at the specified location +# (can be overriden in individual profiles) +initialize = false + +# restic IO priority +ionice = false +#ionice-class = 2 +#ionice-level = 6 + +# restic CPU-priority ( idle | background | low | normal | high ) +priority = "low" + +# scheduler to use: "systemd" (default) or "crond" +#scheduler = "systemd" + +# what to run when no command is specified +default-command = "snapshots" + +# Optional: Specify path to restic +#restic-binary = "/usr/local/bin/restic" + + +## +# The "default" profile +# +# This profile is choosen when no profile is explicitly specified and +# is configured to support commands on the default repository like +# "snapshots", "check", "prune" & "mount". +# +# It is also the parent to the "base" profile which means that profiles +# inheriting from "base" also inherit all settings from "default" +# +# The default profile should not be used directly for running backups +# nor should it contain schedules. +[default] +## Locks +# Prevent concurrent invocation of a profile +lock = "{{.TempDir}}/resticprofile-{{.Profile.Name}}.lock" +# Detect stale locks and unlock automatically +force-inactive-lock = true + + +## +# Backup defaults +[default.backup] + +# Hostname and tags to identify backup snapshots in the repository +#### {{block "conf:default-backup-tags" .}} +# Multiple tags can be used, defaulting to profile name. +# Important: Set the same tags for "backup" and "retention" +tag = [ "{{.Profile.Name}}" ] +#### {{end}} +#### {{block "conf:default-backup-host" .}} +# Specify a hostname or leave it at 'true' for the current hostname +host = true +#### {{end}} + +# Exclude known cache files & folders from backups +exclude-caches = true + +# Exclude nested filesystems +one-file-system = true + +# Toggle whether a failure in reading a backup source is considered an error +#no-error-on-warning = false + +# Wait on acquiring locks when running the profile on a schedule +schedule-lock-wait = "30m" + +# Specify the user that runs profile tasks on a schedule +# "system" - root runs the profile tasks +# "user" - user that created the schedule runs the profile tasks +schedule-permission = "system" + +# Toggle verbose output for troubleshooting +#verbose = false + + +## +# Snapshot retention defaults +[default.retention] +# Remove obsolete snapshots prior to starting a backup +before-backup = false +# Remove obsolete snapshots after a successful backup +after-backup = true + +# Copying "host" and "tags" blocks to identify snapshots to retain or remove +# Note: "host" and "tag" must be in sync between "backup" and "retention" +{{template "conf:default-backup-host" .}} +# In retention, tags can be copied from backup with `true`: +tags = true + +# Copying backup source paths to identify snapshots to retain or remove +# Set to "false" or a list of paths to disable or customize the path filter +path = true + +# Specify the snapshots to keep when checking for obsolete snapshots +# Snapshots that do not match any condition are removed +keep-tag = [ "forever" ] +keep-last = 3 +#keep-hourly = 1 +#keep-daily = 1 +#keep-weekly = 1 +#keep-monthly = 1 +#keep-yearly = 1 +#keep-within = "30d" + +# Use compact format for listing snapshots +#compact = false + +# Enable to prune the repository immediatelly as snapshots are removed +# Prune can be expensive. Consider scheduling prune (see "conf.d/prune.conf") +#prune = true + + +## +# Defaults for showing snapshots of this host +# Usage: +# - `resticprofile snapshots` to view snapshots of this host +[default.snapshots] +# Copying "host" block to identify snapshots to list +{{template "conf:default-backup-host" .}} + + +## +# Defaults for mounting snapshot of this host +# Usage: +# - `resticprofile mount /mnt/restore` to mount snapshots of this host +[default.mount] +# Copying "host" block to identify snapshots to mount +{{template "conf:default-backup-host" .}} + + + +## +# The "base" profile is the base for all other profiles to inherit from +# +# Note: Profiles that do not inherit from "base" will run on built-in +# defaults instead and have to take care of a full profile setup. +[base] +# Inherit all settings from the "default" profile +inherit = "default" + + +## +# Configures the "snapshots" command for profiles inheriting from "base" +# Usage: +# - `resticprofile -n profileName snapshots` to view snapshots of one profile +[base.snapshots] +# Copying "host" and "tags" blocks to identify snapshots to list +{{template "conf:default-backup-host" .}} +{{template "conf:default-backup-tags" .}} + + +## +# Configures the "mount" command for profiles inheriting from "base" +# Usage: +# - `resticprofile -n profileName mount /mnt/restore` to mount snapshots +[base.mount] +# Copying "host" and "tags" block to identify snapshots to mount +{{template "conf:default-backup-host" .}} +{{template "conf:default-backup-tags" .}} \ No newline at end of file diff --git a/contrib/posix/profiles.d/fs-snapshot.yaml.sample b/contrib/posix/profiles.d/fs-snapshot.yaml.sample new file mode 100644 index 000000000..d37c9d57a --- /dev/null +++ b/contrib/posix/profiles.d/fs-snapshot.yaml.sample @@ -0,0 +1,54 @@ +## +# Example profile for applications whose files must be backed-up from a readonly snapshot. +# +applications: + inherit: base + + backup: + source: + - /opt/apps/_backup + - /mnt/data_backup + - /opt/vms/my-vm1.xml + - /opt/vms/my-vm1.qcow2 + + # Create snapshots of supported sources prior to running a backup + run-before: + # Snapshot on Btrfs (mounted on /opt/apps/_backup) + - btrfs subvolume snapshot -r /opt/apps/ /opt/apps/_backup + + # Snapshot on LVM (mounted on /opt/apps/_backup) + - lvcreate -l100%FREE -s -n data_backup /dev/vg00/data && mount /dev/vg00/data_backup /mnt/data_backup + + # Snapshot & config dump for libvirt VMs (VM disk images are readonly during backup) + - virsh dumpxml "my-vm1" > /opt/vms/my-vm1.xml + - {{template "libvirt-create-snapshot" "my-vm1"}} + + # Release snapshots after backup + run-finally: + # Cleanup on Btrfs + - btrfs subvolume delete /opt/apps/_backup + + # Cleanup on LVM + - umount /mnt/data_backup && lvremove -f /dev/vg00/data_backup + + # Cleanup for libvirt VMs (VM disk image receives changes from livedata file) + - {{template "libvirt-delete-snapshot" "my-vm1"}} + + +##### +# Config Templates +# Use +# - `resticprofile -n applications show` to see rendered result +# +{{define "libvirt-create-snapshot" -}} + virsh snapshot-create-as --domain "{{.}}" --name "{{.}}-backup" {{- " " -}} + --diskspec "vda,file=/var/db/{{.}}-livedata.qcow2" {{- " " -}} + --disk-only --atomic --no-metadata --quiesce +{{- end}} +# +{{define "libvirt-delete-snapshot" -}} + virsh blockcommit --domain "{{.}}" vda --wait --active {{- " " -}} + && virsh blockjob --domain "{{.}}" "/var/db/{{.}}-livedata.qcow2" --pivot {{- " " -}} + && rm -f /var/db/{{.}}-livedata.qcow2 +{{- end}} +##### diff --git a/contrib/posix/profiles.d/system.toml b/contrib/posix/profiles.d/system.toml new file mode 100644 index 000000000..70eb089c5 --- /dev/null +++ b/contrib/posix/profiles.d/system.toml @@ -0,0 +1,38 @@ +## +# The "root" profile +# +# Backup $HOME of root and 'etc' folders once a day +# +# Usage: +# - `resticprofile -n root show` +# - `resticprofile -n root --dry-run backup` +# - `resticprofile -n root backup` +# - `resticprofile -n root snapshots` +# - `resticprofile -n root mount /restore` +# - `resticprofile -n root status` +# - `resticprofile -n root schedule` +# - `resticprofile -n root unschedule` +# +[root] +inherit = "base" + +## +# Backup sources and schedule +[root.backup] +source = [ + "/etc", + "/usr/local/etc", + "/opt/local/etc", + "~root/", +] +schedule = "daily" + +## +# Keep last 14 days and the latest from each of last 8 weeks and 6 months +# Total snapshots to keep: 24 +# (14 days, weeks not covered by days and months not covered by weeks) +[root.retention] +keep-last = false +keep-daily = 14 +keep-weekly = 8 +keep-monthly = 6 From 8d33e7e728b10d186dbcee67b0b96c43f7832a66 Mon Sep 17 00:00:00 2001 From: Juergen Kellerer Date: Sun, 22 Aug 2021 19:20:15 +0200 Subject: [PATCH 2/8] nfpms config for linux packages (apk,deb,rpm) --- .goreleaser.yml | 38 +++++++++++++++++++++++++++++++++++ contrib/posix/post-install.sh | 38 +++++++++++++++++++++++++++++++++++ contrib/posix/profiles.conf | 2 +- 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100755 contrib/posix/post-install.sh diff --git a/.goreleaser.yml b/.goreleaser.yml index acbd3c503..8a36a5e17 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -125,3 +125,41 @@ brews: system "#{bin}/resticprofile", "backup", "testfile" system "#{bin}/resticprofile", "restore", "latest", "-t", "#{testpath}/restore" assert compare_file "testfile", "#{testpath}/restore/testfile" + +nfpms: + - + builds: + - resticprofile_targz + formats: + - apk + - deb + - rpm + vendor: "creativeprojects" + homepage: "https://github.com/creativeprojects" + maintainer: "fred@creativeprojects.tech" + description: "Configuration profiles for restic backup" + license: "GPL-3.0-only" + file_name_template: "{{.ProjectName}}_{{.Version}}-{{.Arch}}" + replacements: + amd64: 64bit + 386: 32bit + arm: ARM + arm64: ARM64 + linux: Linux + dependencies: + - restic + bindir: "/usr/local/bin" + scripts: + postinstall: "contrib/posix/post-install.sh" + contents: + - { type: config, src: contrib/posix/profiles.conf, dst: /etc/resticprofile/profiles.conf.dist } + - { type: config, src: contrib/posix/conf.d/check.conf, dst: /etc/resticprofile/conf.d/check.conf.dist } + - { type: config, src: contrib/posix/conf.d/hooks.conf, dst: /etc/resticprofile/conf.d/hooks.conf.dist } + - { type: config, src: contrib/posix/conf.d/metrics.conf, dst: /etc/resticprofile/conf.d/metrics.conf.dist } + - { type: config, src: contrib/posix/conf.d/prune.conf, dst: /etc/resticprofile/conf.d/prune.conf.dist } + - { type: config, src: contrib/posix/conf.d/repository.conf, dst: /etc/resticprofile/conf.d/repository.conf.dist } + - { type: config, src: contrib/posix/profiles.d/fs-snapshot.yaml.sample, dst: /etc/resticprofile/profiles.d/fs-snapshot.yaml.sample } + - { type: config, src: contrib/posix/profiles.d/system.toml, dst: /etc/resticprofile/profiles.d/system.toml.dist } + - src: contrib/systemd/resticprofile-send-error.sh + dst: /usr/local/bin/resticprofile-send-error + file_info: { mode: 0640, owner: root, group: root } diff --git a/contrib/posix/post-install.sh b/contrib/posix/post-install.sh new file mode 100755 index 000000000..c52aaf649 --- /dev/null +++ b/contrib/posix/post-install.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env sh +set -e + +CONFIG_PATH="/etc/resticprofile" +SECRET_FILE="${CONFIG_PATH}/conf.d/default-repository.secret" + +# Fix permissions (only root may edit and read since password +# & tokens can be in any of the files) +if [ -d "${CONFIG_PATH}" ] ; then + chown -R root:root "${CONFIG_PATH}" + chmod -R 0640 "${CONFIG_PATH}" +else + echo "config path (${CONFIG_PATH}) not found" + exit 1 +fi + +# Check installation +if [ ! -e "$(which resticprofile)" ] || ! resticprofile version ; then + echo "resticprofile not found or not executable" + exit 1 +fi + +# Generate default-repo secret (if missing) +if [ ! -f "${SECRET_FILE}" ] ; then + echo "Generating ${SECRET_FILE}" + resticprofile random-key > "${SECRET_FILE}" +fi + +# Unwrap dist files (if target is missing) +cd "${CONFIG_PATH}" +for file in conf.d/*.dist profiles.d/*.dist ; do + target_file="$(dirname "${file}")/$(basename -s ".dist" "${file}")" + if [ -e "${target_file}" ] ; then + echo "Skipping ${target_file}. File already exists" + else + mv -f "${file}" "${target_file}" + fi +done diff --git a/contrib/posix/profiles.conf b/contrib/posix/profiles.conf index 5fe96997b..354b197f5 100644 --- a/contrib/posix/profiles.conf +++ b/contrib/posix/profiles.conf @@ -180,4 +180,4 @@ inherit = "default" [base.mount] # Copying "host" and "tags" block to identify snapshots to mount {{template "conf:default-backup-host" .}} -{{template "conf:default-backup-tags" .}} \ No newline at end of file +{{template "conf:default-backup-tags" .}} From dca8277ac5db6f805564362835afe574bfaf6a0e Mon Sep 17 00:00:00 2001 From: Juergen Kellerer Date: Wed, 29 Sep 2021 21:06:46 +0200 Subject: [PATCH 3/8] Added systemd templates to deb/rpm packages --- .goreleaser.yml | 2 ++ contrib/posix/post-install.sh | 3 ++- contrib/posix/profiles.conf | 4 ++++ contrib/posix/templates/systemd.timer.in | 17 +++++++++++++++++ contrib/posix/templates/systemd.unit.in | 19 +++++++++++++++++++ 5 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 contrib/posix/templates/systemd.timer.in create mode 100644 contrib/posix/templates/systemd.unit.in diff --git a/.goreleaser.yml b/.goreleaser.yml index 8a36a5e17..2b989072d 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -160,6 +160,8 @@ nfpms: - { type: config, src: contrib/posix/conf.d/repository.conf, dst: /etc/resticprofile/conf.d/repository.conf.dist } - { type: config, src: contrib/posix/profiles.d/fs-snapshot.yaml.sample, dst: /etc/resticprofile/profiles.d/fs-snapshot.yaml.sample } - { type: config, src: contrib/posix/profiles.d/system.toml, dst: /etc/resticprofile/profiles.d/system.toml.dist } + - { type: config, src: contrib/posix/templates/systemd.timer.in, dst: /etc/resticprofile/templates/systemd.timer.in.dist } + - { type: config, src: contrib/posix/templates/systemd.unit.in, dst: /etc/resticprofile/templates/systemd.unit.in.dist } - src: contrib/systemd/resticprofile-send-error.sh dst: /usr/local/bin/resticprofile-send-error file_info: { mode: 0640, owner: root, group: root } diff --git a/contrib/posix/post-install.sh b/contrib/posix/post-install.sh index c52aaf649..bb2626238 100755 --- a/contrib/posix/post-install.sh +++ b/contrib/posix/post-install.sh @@ -28,10 +28,11 @@ fi # Unwrap dist files (if target is missing) cd "${CONFIG_PATH}" -for file in conf.d/*.dist profiles.d/*.dist ; do +for file in conf.d/*.dist profiles.d/*.dist templates/*.dist ; do target_file="$(dirname "${file}")/$(basename -s ".dist" "${file}")" if [ -e "${target_file}" ] ; then echo "Skipping ${target_file}. File already exists" + rm "${file}" else mv -f "${file}" "${target_file}" fi diff --git a/contrib/posix/profiles.conf b/contrib/posix/profiles.conf index 354b197f5..802ce9e05 100644 --- a/contrib/posix/profiles.conf +++ b/contrib/posix/profiles.conf @@ -34,6 +34,10 @@ priority = "low" # scheduler to use: "systemd" (default) or "crond" #scheduler = "systemd" +# systemd unit & timer template +systemd-unit-template = "templates/systemd.unit.in" +systemd-timer-template = "templates/systemd.timer.in" + # what to run when no command is specified default-command = "snapshots" diff --git a/contrib/posix/templates/systemd.timer.in b/contrib/posix/templates/systemd.timer.in new file mode 100644 index 000000000..01cbffdfe --- /dev/null +++ b/contrib/posix/templates/systemd.timer.in @@ -0,0 +1,17 @@ +{{/* +# Template for generating systemd timers +# Used to schedule commands when "scheduler=systemd" (default) +*/}} + +[Unit] +Description={{ .TimerDescription }} + +[Timer] +{{ range .OnCalendar -}} +OnCalendar={{ . }} +{{ end -}} +Unit={{ .SystemdProfile }} +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/contrib/posix/templates/systemd.unit.in b/contrib/posix/templates/systemd.unit.in new file mode 100644 index 000000000..be9dd6297 --- /dev/null +++ b/contrib/posix/templates/systemd.unit.in @@ -0,0 +1,19 @@ +{{/* +# Template for generating systemd units +# Used to schedule commands when "scheduler=systemd" (default) +*/}} + +[Unit] +Description={{ .JobDescription }} + +# Send mail when resticprofile fails (requires "unit-status-mail") +#OnFailure=unit-status-mail@%n.service + +[Service] +Type=notify +WorkingDirectory={{ .WorkingDirectory }} +ExecStart={{ .CommandLine }} +{{ if .Nice }}Nice={{ .Nice }}{{ end }} +{{ range .Environment -}} +Environment="{{ . }}" +{{ end -}} From 22b4badb672cc6a9e71b2d269375c6bc2e756058 Mon Sep 17 00:00:00 2001 From: Juergen Kellerer Date: Wed, 10 Nov 2021 11:26:42 +0100 Subject: [PATCH 4/8] Added prune limits to avoid repo runs out of space --- contrib/posix/conf.d/prune.conf | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/contrib/posix/conf.d/prune.conf b/contrib/posix/conf.d/prune.conf index 0397a855d..977fe7abd 100644 --- a/contrib/posix/conf.d/prune.conf +++ b/contrib/posix/conf.d/prune.conf @@ -29,4 +29,16 @@ default-command = "prune" [prune.prune] schedule = "daily" schedule-lock-wait = "4h" - +# Auto remove old cache directories in the local cache +cleanup-cache = true +# Repacking (repository compaction) +# Limited by: +# - `max-unused` (higher precentage, less repacking) +# - `max-repack-size` (lower size value, less repacking) +# +# Repacking requires up to `max-repack-size` temporary space in the repository, +# ensure it has the configured size available at least before running prune. +max-repack-size = "5g" +# Tolerate given limit of unused data (default 5%) +# A higher percentage performs less repacking which will speedup prune. +max-unused = "5%" From f1216293e2d89f41e51c3daca3b6cf79f8430527 Mon Sep 17 00:00:00 2001 From: Juergen Kellerer Date: Wed, 10 Nov 2021 12:43:12 +0100 Subject: [PATCH 5/8] Added explicit check-cache to address out-of-space --- contrib/posix/conf.d/check.conf | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/contrib/posix/conf.d/check.conf b/contrib/posix/conf.d/check.conf index e9ca13702..806058ce5 100644 --- a/contrib/posix/conf.d/check.conf +++ b/contrib/posix/conf.d/check.conf @@ -31,7 +31,13 @@ default-command = "check" [check.check] schedule = "daily" schedule-lock-wait = "4h" - +# +# Check cache +# `check` uses a separate local repo cache to detect problems. The default +# is to create this cache below "/tmp/". While the daily check (without reading +# actual data) uses less space cache-dir may require changing when on tmpfs +cache-dir = "{{.TempDir}}/.restic-temp-check-cache" +with-cache = true ## # Profile "verify" may be used to schedule deep repository checks @@ -55,4 +61,11 @@ schedule-lock-wait = "48h" read-data = true # Read a subset of the repository for verification #read-data-subset = "15%" - +# +# Check cache +# `check` uses a separate local repo cache to detect problems. The built-in +# default location of this cache is "/tmp/" which can cause issues on tmpfs +# as the cache may need several GB of storage. Overridding built-in default +# for the full repo verification check: +cache-dir = "~/.restic-temp-check-cache" +with-cache = true From dba9d091d3f10f66e6eb26cbbb034ca688ca2119 Mon Sep 17 00:00:00 2001 From: Juergen Kellerer Date: Fri, 25 Feb 2022 22:28:03 +0100 Subject: [PATCH 6/8] Refactored error send script to support more cases "resticprofile-send-error" is included in posix packages --- .../README.md | 51 +++------ .../resticprofile-send-error.sh | 105 ++++++++++++++++++ contrib/systemd/resticprofile-send-error.sh | 32 ------ 3 files changed, 118 insertions(+), 70 deletions(-) rename contrib/{systemd => notification-scripts}/README.md (83%) create mode 100755 contrib/notification-scripts/resticprofile-send-error.sh delete mode 100644 contrib/systemd/resticprofile-send-error.sh diff --git a/contrib/systemd/README.md b/contrib/notification-scripts/README.md similarity index 83% rename from contrib/systemd/README.md rename to contrib/notification-scripts/README.md index cd964432d..8edc25e9d 100644 --- a/contrib/systemd/README.md +++ b/contrib/notification-scripts/README.md @@ -1,4 +1,4 @@ -# Send an email on error (systemd schedule) +# Email with failure details - "resticprofile-send-error.sh" In `profiles.yaml` you set: @@ -6,50 +6,25 @@ In `profiles.yaml` you set: default: ... run-after-fail: - - 'resticprofile-send-error.sh name@domain.tl' + - 'resticprofile-send-error.sh -s name@domain.tl' ``` -With `/usr/local/bin/resticprofile-send-error.sh` being: +Usage: -```sh -#!/usr/bin/env bash -[[ -z "${PROFILE_NAME}" ]] || sendmail -t < -Subject: restic failed: ${PROFILE_COMMAND} "${PROFILE_NAME}" -Content-Transfer-Encoding: 8bit -Content-Type: text/plain; charset=UTF-8 - -${ERROR} - ----- -COMMANDLINE: - -${ERROR_COMMANDLINE} - ----- -STDERR: - -${ERROR_STDERR} - ----- -DETAILS: - -$(systemctl status --full "resticprofile-${PROFILE_COMMAND}@profile-${PROFILE_NAME}") - ----- -CONFIG: - -$(resticprofile --name "${PROFILE_NAME}" show) - -ERRMAIL -exit 0 +``` +resticprofile-send-error.sh [options] user1@domain user2@domain ... +Options: + -s Only send mail when operating on schedule (RESTICPROFILE_ON_SCHEDULE=1) + -c command Set the profile command (instead of PROFILE_COMMAND) + -n name Set the profile name (instead of PROFILE_NAME) + -p Print mail to stdout instead of sending it + -f Send mail even when no profile name is specified ``` ## Quick installation ```sh -curl -ssL https://github.com/creativeprojects/resticprofile/raw/master/contrib/systemd/resticprofile-send-error.sh \ +curl -ssL https://github.com/creativeprojects/resticprofile/raw/master/contrib/notification-scripts/resticprofile-send-error.sh \ > /usr/local/bin/resticprofile-send-error.sh \ && chmod +x /usr/local/bin/resticprofile-send-error.sh ``` @@ -64,7 +39,7 @@ In this example, the failure is caused by a custom pre-script complaining about Date: Fri, 23 Apr 2021 23:25:03 +0200 To: admins@domain.tl From: "resticprofile hyper1.domain.tl" -Subject: restic failed: backup "vms" +Subject: restic failed: "backup" in "vms" run-before backup on profile 'vms': exit status 1 diff --git a/contrib/notification-scripts/resticprofile-send-error.sh b/contrib/notification-scripts/resticprofile-send-error.sh new file mode 100755 index 000000000..adc4e96e9 --- /dev/null +++ b/contrib/notification-scripts/resticprofile-send-error.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# +# Error notification sendmail script +# +help() { + cat - < 0 )) || exit 0 ;; + *) help "$0" ; exit 0 ;; + esac +done +shift $((OPTIND-1)) + +# Parameters +MAIL_TO="" +MAIL_FROM="\"resticprofile $(hostname -f)\" <$USER@$(hostname -f)>" +MAIL_SUBJECT="restic failed: \"${PROFILE_COMMAND}\" in \"${PROFILE_NAME}\"" + +SEND_COMMAND="${SEND_COMMAND:-sendmail -t}" + +DETAILS_COMMAND_RESULT="" +DETAILS_COMMAND="" + +# Get command to capture output from scheduler ( if in use ) +if [[ -d /etc/systemd/ ]] \ + && (( ${RESTICPROFILE_ON_SCHEDULE:-0} > 0 )) \ + && resticprofile --name "${PROFILE_NAME}" show | grep -v -q -E "scheduler:\s*cron" ; then + DETAILS_COMMAND="systemctl status --full \"resticprofile-${PROFILE_COMMAND:-*}@profile-${PROFILE_NAME:-*}\"" +fi + +# Load parameter overrides +RC_FILE="/etc/resticprofile/$(basename "$0").rc}" +[[ -f "${RC_FILE}" ]] && source "${RC_FILE}" + +main() { + if [[ -n "${PROFILE_NAME}" || "${FORCE_SENDING}" == "1" ]] ; then + if [[ -n "${DETAILS_COMMAND}" ]] ; then + DETAILS_COMMAND_RESULT="$(${DETAILS_COMMAND})" + fi + + for email in "$@" "${MAIL_TO}" ; do + if [[ "${email}" =~ ^[a-zA-Z0-9_.%+-]+@[a-zA-Z0-9_]+[a-zA-Z0-9_.-]+$ ]] ; then + send_mail "${email}" || echo "Failed sending to \"${email}\"" + elif [[ -n "${email}" ]] ; then + echo "Skipping notification for invalid address \"${email}\"" + fi + done + fi + return 0 +} + +send_mail() { + ${SEND_COMMAND} < -Subject: restic failed: ${PROFILE_COMMAND} "${PROFILE_NAME}" -Content-Transfer-Encoding: 8bit -Content-Type: text/plain; charset=UTF-8 - -${ERROR} - ----- -COMMANDLINE: - -${ERROR_COMMANDLINE} - ----- -STDERR: - -${ERROR_STDERR} - ----- -DETAILS: - -$(systemctl status --full "resticprofile-${PROFILE_COMMAND}@profile-${PROFILE_NAME}") - ----- -CONFIG: - -$(resticprofile --name "${PROFILE_NAME}" show) - -ERRMAIL -exit 0 From 2597405b675f3e9e4ccda9ad483e8383f403310c Mon Sep 17 00:00:00 2001 From: Juergen Kellerer Date: Fri, 25 Feb 2022 22:35:13 +0100 Subject: [PATCH 7/8] Set env RESTICPROFILE_ON_SCHEDULE=1 when scheduled (Schedulers with template support only) --- contrib/posix/templates/systemd.unit.in | 5 ++++- systemd/generate.go | 4 +++- systemd/generate_test.go | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/contrib/posix/templates/systemd.unit.in b/contrib/posix/templates/systemd.unit.in index be9dd6297..22ecfce98 100644 --- a/contrib/posix/templates/systemd.unit.in +++ b/contrib/posix/templates/systemd.unit.in @@ -13,7 +13,10 @@ Description={{ .JobDescription }} Type=notify WorkingDirectory={{ .WorkingDirectory }} ExecStart={{ .CommandLine }} -{{ if .Nice }}Nice={{ .Nice }}{{ end }} +{{ if .Nice -}} +Nice={{ .Nice }} +{{ end -}} +Environment="RESTICPROFILE_ON_SCHEDULE=1" {{ range .Environment -}} Environment="{{ . }}" {{ end -}} diff --git a/systemd/generate.go b/systemd/generate.go index 317be6dfb..5d73c0cc1 100644 --- a/systemd/generate.go +++ b/systemd/generate.go @@ -1,4 +1,5 @@ -//+build !darwin,!windows +//go:build !darwin && !windows +// +build !darwin,!windows package systemd @@ -29,6 +30,7 @@ Type=notify WorkingDirectory={{ .WorkingDirectory }} ExecStart={{ .CommandLine }} {{ if .Nice }}Nice={{ .Nice }}{{ end }} +Environment="RESTICPROFILE_ON_SCHEDULE=1" {{ range .Environment -}} Environment="{{ . }}" {{ end -}} diff --git a/systemd/generate_test.go b/systemd/generate_test.go index 76b35ab84..d276d3c58 100644 --- a/systemd/generate_test.go +++ b/systemd/generate_test.go @@ -1,4 +1,5 @@ -//+build !darwin,!windows +//go:build !darwin && !windows +// +build !darwin,!windows package systemd @@ -51,6 +52,7 @@ Type=notify WorkingDirectory=workdir ExecStart=commandLine Nice=5 +Environment="RESTICPROFILE_ON_SCHEDULE=1" Environment="HOME=%s" ` const expectedTimer = `[Unit] From d3d9ea15ac0972acacd80dfba5f7876850107cb1 Mon Sep 17 00:00:00 2001 From: Juergen Kellerer Date: Fri, 25 Feb 2022 22:44:20 +0100 Subject: [PATCH 8/8] Updated to config v2 --- .goreleaser.yml | 17 +- contrib/notification-scripts/README.md | 1 + .../resticprofile-send-error.sh | 25 ++- contrib/posix/README.md | 32 ++- contrib/posix/conf.d/backup.conf | 211 ++++++++++++++++++ contrib/posix/conf.d/check.conf | 103 ++++----- contrib/posix/conf.d/hooks.conf | 58 ++++- contrib/posix/conf.d/metrics.conf | 25 ++- contrib/posix/conf.d/prune.conf | 80 ++++--- contrib/posix/conf.d/z_overrides.conf | 64 ++++++ contrib/posix/post-install.sh | 163 ++++++++++++-- contrib/posix/post-install_test.sh | 34 +++ contrib/posix/profiles.conf | 161 ++++--------- .../posix/profiles.d/fs-snapshot.yaml.sample | 77 ++++--- contrib/posix/profiles.d/minimal.conf.sample | 27 +++ contrib/posix/profiles.d/minimal.yaml.sample | 28 +++ contrib/posix/profiles.d/system.conf | 55 +++++ contrib/posix/profiles.d/system.toml | 38 ---- .../default.conf} | 49 ++-- .../posix/repositories.d/other.conf.sample | 69 ++++++ contrib/posix/resticprofile-send-error.rc | 39 ++++ contrib/posix/templates/default-host.conf | 7 + 22 files changed, 1002 insertions(+), 361 deletions(-) create mode 100644 contrib/posix/conf.d/backup.conf create mode 100644 contrib/posix/conf.d/z_overrides.conf create mode 100755 contrib/posix/post-install_test.sh create mode 100644 contrib/posix/profiles.d/minimal.conf.sample create mode 100644 contrib/posix/profiles.d/minimal.yaml.sample create mode 100644 contrib/posix/profiles.d/system.conf delete mode 100644 contrib/posix/profiles.d/system.toml rename contrib/posix/{conf.d/repository.conf => repositories.d/default.conf} (57%) create mode 100644 contrib/posix/repositories.d/other.conf.sample create mode 100644 contrib/posix/resticprofile-send-error.rc create mode 100644 contrib/posix/templates/default-host.conf diff --git a/.goreleaser.yml b/.goreleaser.yml index 2b989072d..9cb364ec6 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -153,15 +153,24 @@ nfpms: postinstall: "contrib/posix/post-install.sh" contents: - { type: config, src: contrib/posix/profiles.conf, dst: /etc/resticprofile/profiles.conf.dist } + - { type: config, src: contrib/posix/conf.d/backup.conf, dst: /etc/resticprofile/conf.d/backup.conf.dist } - { type: config, src: contrib/posix/conf.d/check.conf, dst: /etc/resticprofile/conf.d/check.conf.dist } - { type: config, src: contrib/posix/conf.d/hooks.conf, dst: /etc/resticprofile/conf.d/hooks.conf.dist } - { type: config, src: contrib/posix/conf.d/metrics.conf, dst: /etc/resticprofile/conf.d/metrics.conf.dist } - { type: config, src: contrib/posix/conf.d/prune.conf, dst: /etc/resticprofile/conf.d/prune.conf.dist } - - { type: config, src: contrib/posix/conf.d/repository.conf, dst: /etc/resticprofile/conf.d/repository.conf.dist } + - { type: config, src: contrib/posix/conf.d/z_overrides.conf, dst: /etc/resticprofile/conf.d/z_overrides.conf.dist } - { type: config, src: contrib/posix/profiles.d/fs-snapshot.yaml.sample, dst: /etc/resticprofile/profiles.d/fs-snapshot.yaml.sample } - - { type: config, src: contrib/posix/profiles.d/system.toml, dst: /etc/resticprofile/profiles.d/system.toml.dist } + - { type: config, src: contrib/posix/profiles.d/minimal.conf.sample, dst: /etc/resticprofile/profiles.d/minimal.conf.sample } + - { type: config, src: contrib/posix/profiles.d/minimal.yaml.sample, dst: /etc/resticprofile/profiles.d/minimal.yaml.sample } + - { type: config, src: contrib/posix/profiles.d/system.conf, dst: /etc/resticprofile/profiles.d/system.conf.dist } + - { type: config, src: contrib/posix/repository.d/default.conf, dst: /etc/resticprofile/repository.d/default.conf.dist } + - { type: config, src: contrib/posix/repository.d/other.conf.sample, dst: /etc/resticprofile/repository.d/other.conf.sample } + - { type: config, src: contrib/posix/templates/default-host.conf, dst: /etc/resticprofile/templates/default-host.conf.dist } + - { type: config, src: contrib/posix/templates/default-tags.conf, dst: /etc/resticprofile/templates/default-tags.conf.dist } - { type: config, src: contrib/posix/templates/systemd.timer.in, dst: /etc/resticprofile/templates/systemd.timer.in.dist } - { type: config, src: contrib/posix/templates/systemd.unit.in, dst: /etc/resticprofile/templates/systemd.unit.in.dist } - - src: contrib/systemd/resticprofile-send-error.sh + - src: contrib/posix/resticprofile-send-error.rc + dst: /etc/resticprofile/resticprofile-send-error.rc.dist + - src: contrib/notification-scripts/resticprofile-send-error.sh dst: /usr/local/bin/resticprofile-send-error - file_info: { mode: 0640, owner: root, group: root } + file_info: { mode: 0755, owner: root, group: root } diff --git a/contrib/notification-scripts/README.md b/contrib/notification-scripts/README.md index 8edc25e9d..0bcb62325 100644 --- a/contrib/notification-scripts/README.md +++ b/contrib/notification-scripts/README.md @@ -15,6 +15,7 @@ Usage: resticprofile-send-error.sh [options] user1@domain user2@domain ... Options: -s Only send mail when operating on schedule (RESTICPROFILE_ON_SCHEDULE=1) + -o name,.. Only send mail when PROFILE_NAME is in the list of specified names -c command Set the profile command (instead of PROFILE_COMMAND) -n name Set the profile name (instead of PROFILE_NAME) -p Print mail to stdout instead of sending it diff --git a/contrib/notification-scripts/resticprofile-send-error.sh b/contrib/notification-scripts/resticprofile-send-error.sh index adc4e96e9..8ad179409 100755 --- a/contrib/notification-scripts/resticprofile-send-error.sh +++ b/contrib/notification-scripts/resticprofile-send-error.sh @@ -7,6 +7,7 @@ help() { Usage $1 [options] user1@domain user2@domain ... Options: -s Only send mail when operating on schedule (RESTICPROFILE_ON_SCHEDULE=1) + -o name,.. Only send mail when PROFILE_COMMAND is in the list of specified names -c command Set the profile command (instead of PROFILE_COMMAND) -n name Set the profile name (instead of PROFILE_NAME) -p Print mail to stdout instead of sending it @@ -17,9 +18,11 @@ HELP # Parse CLI args FORCE_SENDING=0 SEND_COMMAND="" -while getopts 'c:fhn:ps' flag ; do +LIMIT_COMMAND_NAMES="" +while getopts 'c:fhn:o:ps' flag ; do case "${flag}" in c) PROFILE_COMMAND="${OPTARG}" ;; + o) LIMIT_COMMAND_NAMES="${OPTARG}" ;; f) FORCE_SENDING=1 ;; n) PROFILE_NAME="${OPTARG}" ;; p) SEND_COMMAND="cat -" ;; @@ -51,14 +54,14 @@ RC_FILE="/etc/resticprofile/$(basename "$0").rc}" [[ -f "${RC_FILE}" ]] && source "${RC_FILE}" main() { - if [[ -n "${PROFILE_NAME}" || "${FORCE_SENDING}" == "1" ]] ; then + if can_send ; then if [[ -n "${DETAILS_COMMAND}" ]] ; then DETAILS_COMMAND_RESULT="$(${DETAILS_COMMAND})" fi for email in "$@" "${MAIL_TO}" ; do if [[ "${email}" =~ ^[a-zA-Z0-9_.%+-]+@[a-zA-Z0-9_]+[a-zA-Z0-9_.-]+$ ]] ; then - send_mail "${email}" || echo "Failed sending to \"${email}\"" + send_mail "${email}" || echo "Failed sending to \"${email}\" using '${SEND_COMMAND}' exit code $?" elif [[ -n "${email}" ]] ; then echo "Skipping notification for invalid address \"${email}\"" fi @@ -67,6 +70,22 @@ main() { return 0 } +can_send() { + if [[ -n "${PROFILE_NAME}" ]] ; then + if [[ -n "${LIMIT_COMMAND_NAMES}" ]] ; then + local IFS=",; " + for cmd in ${LIMIT_COMMAND_NAMES} ; do + [[ "${PROFILE_COMMAND}" == "$cmd" ]] && return 0 + done + else + return 0 + fi + fi + + [[ "${FORCE_SENDING}" == "1" ]] + return $? +} + send_mail() { ${SEND_COMMAND} <>> "profiles.d/example.conf" +# +# [profiles.example] +# description = "Backup example" +# inherit = "base" +# +# [profiles.example.backup] +# schedule = "daily" +# source = [ +# "/path/to/backup", +# "/other/path/to/backup", +# ] +# +# <<< +# + + +## +# Backup defaults +[profiles.default.backup] + +# Hostname to identify backup snapshots in the repository from this host +{{ template "conf:default-host" . }} + +# +# Tags (besides host & path) are used to identify snapshots belonging to +# a certain backup. +# +# Multiple tags can be defined, but profile name should always be included, +# the expression "{{ .Profile.Name }}" resolves to the currently active +# profile name. +# +# Notes: +# +# - Set the same tags for "backup" and "retention" unless you know what you +# are doing. Retention uses the tags defined in the backup section when +# "tag = true" is set in the retention configuration and this default +# setup should not be changed. +# +# - Most of the command sections should use "tag = true" to copy tags from +# backup so that restore, mount, etc. relate to the selected profile. +# +tag = [ "{{ .Profile.Name }}" ] + +# Exclude known cache files & folders from backups +exclude-caches = true + +# Exclude nested filesystems +# Prefer overriding this option in dedicated backup profiles instead of +# globally as it can greatly increase the volume if nested FS mounts are +# contained in backup source paths. +one-file-system = true + +# Toggle whether a failure in reading a backup source is considered an error +no-error-on-warning = false + +# Wait on acquiring locks when running the profile on a schedule +schedule-lock-wait = "45m" + +# Specify the user that runs profile tasks on a schedule +# "system" - root runs the profile tasks +# "user" - user that created the schedule runs the profile tasks +schedule-permission = "system" + +# Toggle verbose output for troubleshooting +#verbose = false + +# Toggles immediate repository check before and after backup. +# Checks can be heavy on resources. Consider scheduling "maintenance" +# (see "conf.d/check.conf") instead of enabling checks here. +check-before = false +check-after = false + + +## +# Snapshot retention defaults +[profiles.default.retention] +# Remove obsolete snapshots prior to starting a backup +before-backup = false +# Remove obsolete snapshots after a successful backup +after-backup = true + +# +# Note: Retention operates on host, path and tag filters to identify snapshots +# to retain or remove. In most cases these filters should be in sync with +# the backup configuration of a profile so that snapshots will be removed +# that truely belong to a profile's backup. +# + +# Host filter +# Copying "host" block to identify snapshots by hostname +{{ template "conf:default-host" . }} + +# Tag filter +# Retention allows to build tag filter from backup with 'true'. It is strongly +# advised not to change this as tags are the primary filter besides hostname. +tag = true + +# Path filter +# Set to "true" to copy source paths from backup, "false" or a list of paths +# to disable or customize the path filter. +# +# Note: Path filters match literally on the absolute source paths recorded +# when a snapshot was created. If sources are changed, specified relative or +# with wildcards, snapshots may no longer be matched. Tag and host filters are +# better suited to identify all snapshots of certain profile. +path = false + +# Specify the snapshots to keep when checking for obsolete snapshots +# Snapshots that do not match any condition are removed +keep-tag = [ "forever" ] +keep-last = 3 +#keep-hourly = 1 +#keep-daily = 1 +#keep-weekly = 1 +#keep-monthly = 1 +#keep-yearly = 1 +#keep-within = "30d" + +# Use compact format for listing snapshots +#compact = false + +# Toggles immediate prune of the repository as snapshots are removed. +# +# While removing snapshots is a light operation, prune (reclaim space) can +# be heavy on resources as it rewrites parts of the repository. +# Consider scheduling "maintenance" (see "conf.d/prune.conf") instead of +# enabling prune here. +# +# Also an attempt to recover a removed snapshot with "resticprofile recover" +# only works as long as the repository was not yet pruned. +prune = false + + +## +# Defaults for operations on repository snapshots of this host +# Usage: +# - "resticprofile snapshots" - view snapshots +# - "resticprofile mount /mnt/restore" - mount snapshots +# - "resticprofile ls latest /" - list files in a snapshot +# - "resticprofile dump latest /file" - dump a file to stdout +# - "resticprofile find PATTERN..." - find files in snapshots +# - "resticprofile copy --repo2=..." - copy snapshots to repo2 +# - "resticprofile restore --target=/to/dir --include=PATTERN... latest" +[profiles.default.copy] +{{ template "conf:default-host" . }} +schedule-lock-wait = "1h30m" +[profiles.default.dump] +{{ template "conf:default-host" . }} +[profiles.default.find] +{{ template "conf:default-host" . }} +[profiles.default.forget] +{{ template "conf:default-host" . }} +[profiles.default.ls] +{{ template "conf:default-host" . }} +[profiles.default.mount] +{{ template "conf:default-host" . }} +[profiles.default.restore] +{{ template "conf:default-host" . }} +[profiles.default.snapshots] +{{ template "conf:default-host" . }} +[profiles.default.stats] +{{ template "conf:default-host" . }} +[profiles.default.tag] +{{ template "conf:default-host" . }} + + +## +# Setup operations on repository snapshots for profiles deriving from "base" +# Usage: +# - "resticprofile profileName.snapshots" - view snapshots +# - "resticprofile profileName.mount /mnt/restore" - mount snapshots +# - "resticprofile profileName.ls latest /" - list files in a snapshot +# - "resticprofile profileName.dump latest /file" - dump a file to stdout +# - "resticprofile profileName.find PATTERN..." - find files in snapshots +# - "resticprofile profileName.copy --repo2=..." - copy snapshots to repo2 +# - "resticprofile profileName.restore --target=/to/dir latest" +[profiles.base.copy] +tag = true +[profiles.base.dump] +tag = true +[profiles.base.find] +tag = true +[profiles.base.forget] +tag = true +[profiles.base.ls] +tag = true +[profiles.base.mount] +tag = true +[profiles.base.restore] +tag = true +[profiles.base.snapshots] +tag = true +[profiles.base.stats] +tag = true +[profiles.base.tag] +tag = true + + +# ----------------------------------------------------------------------------- diff --git a/contrib/posix/conf.d/check.conf b/contrib/posix/conf.d/check.conf index 806058ce5..6dcd1e789 100644 --- a/contrib/posix/conf.d/check.conf +++ b/contrib/posix/conf.d/check.conf @@ -1,71 +1,52 @@ - -## -# Groups for profiles that check & verify repositories -# Usage -# - `resticprofile -n check-all` -# - `resticprofile -n check-all schedule` -# - `resticprofile -n check-all unschedule` -# - `resticprofile -n verify-all` -# - `resticprofile -n verify-all schedule` -# - `resticprofile -n verify-all unschedule` -[groups] -check-all = [ "check" ] -verify-all = [ "verify" ] - - +# ----------------------------------------------------------------------------- ## -# Profile "check" may be used to schedule repository checks +# Repository check defaults # # Usage -# - `resticprofile -n check` -# - `resticprofile -n check schedule` -# - `resticprofile -n check unschedule` # -[check] -# Operate on the pepository defined in the "base" profile -inherit = "base" -initialize = false -default-command = "check" - -# Configuring the "check" command in profile "check" -[check.check] -schedule = "daily" -schedule-lock-wait = "4h" +# Run check on the default profile: +# - "resticprofile check" +# Run check on all maintenance profiles: +# - "resticprofile maintenance-all.check" # -# Check cache -# `check` uses a separate local repo cache to detect problems. The default -# is to create this cache below "/tmp/". While the daily check (without reading -# actual data) uses less space cache-dir may require changing when on tmpfs -cache-dir = "{{.TempDir}}/.restic-temp-check-cache" -with-cache = true +# Schedule the default maintenance profile: +# - "resticprofile maintenance.schedule" +# - "resticprofile maintenance.unschedule" +# Schedule all maintenance profiles: +# - "resticprofile maintenance-all.schedule" +# - "resticprofile maintenance-all.unschedule" +# + ## -# Profile "verify" may be used to schedule deep repository checks +# Defaults for repository checks +[profiles.default.check] +# Read actual repository data for verification +read-data = false +# Read a subset of the repository for verification (when read-data = true) +read-data-subset = "33%" # -# Usage -# - `resticprofile -n verify` -# - `resticprofile -n verify schedule` -# - `resticprofile -n verify unschedule` -# -[verify] -# Operate on the pepository defined in the "base" profile -inherit = "base" -initialize = false -default-command = "check" +# Check cache setup +# "check" uses a separate local one-time repo cache to detect problems. +# The default is to create this cache below "/tmp/" and it may grow to several +# GB during check execution, adjust path if needed. +cache-dir = "{{ .TempDir }}/restic-check-cache.resticprofile" +with-cache = true +# Allow longer lock waits for check schedules to ensure checks run as other +# tasks take longer to complete +schedule-lock-wait = "8h" +# Only root may schedule check +schedule-permission = "system" + -# Configuring the "check" command in profile "verify" -[verify.check] -schedule = "monthly" -schedule-lock-wait = "48h" -# Read the entire repository for verification +## +# Configuring "check" schedule in maintenance profile +[profiles.maintenance.check] +# Schedule at 5th, 10th, 15th, 20th, 25th and 30th day in month at 04:15 +schedule = "*-*-5,10,15,20,25,30 04:15:00" +# Read repository data for verification every 15th day in month +{{ if .Now.Day | eq 15 -}} read-data = true -# Read a subset of the repository for verification -#read-data-subset = "15%" -# -# Check cache -# `check` uses a separate local repo cache to detect problems. The built-in -# default location of this cache is "/tmp/" which can cause issues on tmpfs -# as the cache may need several GB of storage. Overridding built-in default -# for the full repo verification check: -cache-dir = "~/.restic-temp-check-cache" -with-cache = true +{{ end -}} + +# ----------------------------------------------------------------------------- diff --git a/contrib/posix/conf.d/hooks.conf b/contrib/posix/conf.d/hooks.conf index 0c5725e3a..0b6434f90 100644 --- a/contrib/posix/conf.d/hooks.conf +++ b/contrib/posix/conf.d/hooks.conf @@ -1,19 +1,53 @@ +# ----------------------------------------------------------------------------- +## +# Base configuration for action hooks that run before, after, after failure or +# always after a set of profile tasks (profile task = restic command or +# action hook) +# +# The following additional environment variables are defined in an action: +# +# - PROFILE_NAME - Name of the active profile +# - PROFILE_COMMAND - Name of the requested command +# - ERROR - Error message if the requested command failed +# - ERROR_EXIT_CODE - Exit code of the failing command or action hook +# - ERROR_COMMANDLINE - Commandline of the failing command or action hook +# - ERROR_STDERR - Stderr of the failing command or action hook +# ## # Action hooks for profiles that derive from "base" -[base] -# Actions to run before any profile task -#run-before = [ -# 'echo ">>> ${PROFILE_NAME} - BEGIN ${PROFILE_COMMAND}"', -#] +[profiles.base] +## +# Actions to run before restic +run-before = [ + #'echo ">>> ${PROFILE_NAME} - BEGIN ${PROFILE_COMMAND}"', +] -# Actions to run after a profile task -#run-after = [ -# 'echo "<<< ${PROFILE_NAME} - END ${PROFILE_COMMAND}"', -#] +## +# Actions to run after restic (only if "run-before" and "restic" succeeded) +run-after = [ + #'echo "<<< ${PROFILE_NAME} - END ${PROFILE_COMMAND}"', +] -# Actions to run when a profile task has failed +## +# Actions to run when a profile task has failed (triggers as "run-before", +# "restic" or "run-after" failed) +# +# ERROR variables are set and contain details on the failed task run-after-fail = [ - 'echo "!!! ${PROFILE_NAME} - FAILED ${PROFILE_COMMAND}" - ERROR: ${ERROR}"', - # 'resticprofile-send-error admin@localhost', + # Example: Print error + 'echo "!!! ${PROFILE_NAME} - FAILED ${PROFILE_COMMAND} - ERROR: ${ERROR}"', + + # Send mail (see /etc/resticprofile/resticprofile-send-error.rc) + 'resticprofile-send-error -s -o "check,copy,forget,backup,prune"', ] + +## +# Actions to run in any case after all other actions. +# +# On failure ERROR variables are set and contain details on the failed task +run-finally = [ + '["${PROFILE_COMMAND}" == "check"] && rm -rf "{{ .TempDir }}/restic-check-cache.resticprofile"', +] + +# ----------------------------------------------------------------------------- diff --git a/contrib/posix/conf.d/metrics.conf b/contrib/posix/conf.d/metrics.conf index 8d99b48d0..73f2034db 100644 --- a/contrib/posix/conf.d/metrics.conf +++ b/contrib/posix/conf.d/metrics.conf @@ -1,15 +1,32 @@ +# ----------------------------------------------------------------------------- +## +# Metrics collection configuration +# +# Resticprofile can create metrics for prometheus or as a custom JSON format. +# In order to create metrics, the option "extended-status = true" must be set +# for "backup" along with metrics output configuration in the profile. +# ## -# Metric collection for profiles that derive from "base" -[base.backup] +# Toggle metrics collection in "backup" for profiles that derive from "base" +[profiles.base.backup] # Toggles full "restic" output capture to allow collecting backup metrics # for "status-file" and "prometheus-(save-to-file|push)" #extended-status = true + +## +# Metric configuration for profiles that derive from "base" +[profiles.base] +## # Write backup metrics as JSON (requires extended-status = true) -#status-file = "{{.TempDir}}/resticprofile-{{.Profile.Name}}-status.json" +#status-file = "{{ .TempDir }}/resticprofile-{{ .Profile.Name }}-status.json" +## # Export backup metrics to Prometheus (requires extended-status = true) -#prometheus-save-to-file = "/var/lib/prometheus/node-exporter/{{.Profile.Name}}.prom" +#prometheus-save-to-file = "/var/lib/prometheus/node-exporter/{{ .Profile.Name }}.prom" #prometheus-push = "http://host:9091/" + + +# ----------------------------------------------------------------------------- diff --git a/contrib/posix/conf.d/prune.conf b/contrib/posix/conf.d/prune.conf index 977fe7abd..adcd1071d 100644 --- a/contrib/posix/conf.d/prune.conf +++ b/contrib/posix/conf.d/prune.conf @@ -1,44 +1,56 @@ - -## -# Group for profiles that prune repositories -# Usage -# - `resticprofile -n prune-all` -# - `resticprofile -n prune-all schedule` -# - `resticprofile -n prune-all unschedule` -[groups] -prune-all = [ "prune" ] - - +# ----------------------------------------------------------------------------- ## -# Profile "prune" may be used to schedule pruning of the default repository -# which reclaims space that is no longer occupied by removed snapshots from -# profiles that use this repository. +# Repository prune defaults # # Usage -# - `resticprofile -n prune` -# - `resticprofile -n prune schedule` -# - `resticprofile -n prune unschedule` # -[prune] -# Operate on the pepository defined in the "base" profile -inherit = "base" -initialize = false -default-command = "prune" +# Run prune on the default profile: +# - "resticprofile prune" +# Run prune on all maintenance profiles: +# - "resticprofile maintenance-all.prune" +# +# Schedule the default maintenance profile: +# - "resticprofile maintenance.schedule" +# - "resticprofile maintenance.unschedule" +# Schedule all maintenance profiles: +# - "resticprofile maintenance-all.schedule" +# - "resticprofile maintenance-all.unschedule" +# -# Configuring the "prune" command in profile "prune" -[prune.prune] -schedule = "daily" -schedule-lock-wait = "4h" -# Auto remove old cache directories in the local cache + +## +# Defaults for repository prune +[profiles.default.prune] +## +# Auto remove old cache entries from the local cache that are no longer +# needed as data was pruned from the repository. cleanup-cache = true -# Repacking (repository compaction) + +## +# Repacking (repository compaction) configuration. # Limited by: -# - `max-unused` (higher precentage, less repacking) -# - `max-repack-size` (lower size value, less repacking) +# - "max-unused" (higher precentage, less repacking) +# - "max-repack-size" (lower size value, less repacking) # -# Repacking requires up to `max-repack-size` temporary space in the repository, +# Repacking requires up to "max-repack-size" temporary space in the repository, # ensure it has the configured size available at least before running prune. -max-repack-size = "5g" -# Tolerate given limit of unused data (default 5%) +# Using a smaller size means more iterations are needed until all space is +# reclaimed. +max-repack-size = "3g" +# Tolerate given limit of unused data that is not reclaimed (default 5%) # A higher percentage performs less repacking which will speedup prune. -max-unused = "5%" +max-unused = "5%" + +## +# Wait on acquiring locks when running "prune" on a schedule +schedule-lock-wait = "1h30m" +# Only root may schedule prune +schedule-permission = "system" + + +## +# Configuring "prune" schedule in maintenance profile +[profiles.maintenance.prune] +schedule = "daily" + +# ----------------------------------------------------------------------------- diff --git a/contrib/posix/conf.d/z_overrides.conf b/contrib/posix/conf.d/z_overrides.conf new file mode 100644 index 000000000..a307da0e8 --- /dev/null +++ b/contrib/posix/conf.d/z_overrides.conf @@ -0,0 +1,64 @@ +# ----------------------------------------------------------------------------- +## +# Is included as last conf file and may override any settings made earlier. +# +# Prefer this file to customize resticprofile's default configuration as +# this file is not updated during installation. +# + +## +# Example: Override default retention +# +# Note: Retention settings are inherited like all other parameters one-by-one. +# Anything that is set here must be explicitly configured in profiles +# if the default should not apply. +# +# [profiles.default.retention] +# keep-tag = [ "forever" ] +# keep-last = 10 +# keep-hourly = 0 +# keep-daily = 14 +# keep-weekly = 2 +# keep-monthly = 0 +# keep-yearly = 0 +# keep-within = "30d" + + +## +# Example: Global repository access +# +# Enable to access the default repository without host or tag filter +# +# [profiles.global.copy] +# host = false +# tag = false +# [profiles.global.dump] +# host = false +# tag = false +# [profiles.global.find] +# host = false +# tag = false +# [profiles.global.forget] +# host = false +# tag = false +# [profiles.global.ls] +# host = false +# tag = false +# [profiles.global.mount] +# host = false +# tag = false +# [profiles.global.restore] +# host = false +# tag = false +# [profiles.global.snapshots] +# host = false +# tag = false +# [profiles.global.stats] +# host = false +# tag = false +# [profiles.global.tag] +# host = false +# tag = false + + +# ----------------------------------------------------------------------------- diff --git a/contrib/posix/post-install.sh b/contrib/posix/post-install.sh index bb2626238..0a66707bf 100755 --- a/contrib/posix/post-install.sh +++ b/contrib/posix/post-install.sh @@ -1,39 +1,170 @@ #!/usr/bin/env sh -set -e +ROOT_PATH="${ROOT_PATH:-}" -CONFIG_PATH="/etc/resticprofile" -SECRET_FILE="${CONFIG_PATH}/conf.d/default-repository.secret" +FILES_OWNER="root:root" +if [ -n "${ROOT_PATH}" ] && [ "${ROOT_PATH}" != "/usr/local" ] ; then + FILES_OWNER="$(id -u):$(id -g)" +fi + +# Temp dir (using fixed path to ensure "rm -rf" will not have side effects) +TEMP_PATH="${ROOT_PATH}/tmp/.resticprofile-setup" +if [ -d "$TEMP_PATH" ] ; then + rm -rf "${TEMP_PATH}" +fi +mkdir -p "$TEMP_PATH" +trap "rm -rf \"${TEMP_PATH}\"" EXIT INT TERM + +# Paths +CACHE_PATH="${TEMP_PATH}/cache" +CONFIG_PATH="${ROOT_PATH}/etc/resticprofile" +CONFIG_CACHE_FILE="${CONFIG_PATH}/.dist.cache" +REPOSITORY_SECRET="repositories.d/default.secret" +SECRET_FILE="${CONFIG_PATH}/${REPOSITORY_SECRET}" + +# Config files that are merged with .dist files when already existing +MERGEABLES="conf.d/backup.conf conf.d/check.conf conf.d/hooks.conf" +MERGEABLES="${MERGEABLES} conf.d/metrics.conf conf.d/prune.conf" +MERGEABLES="${MERGEABLES} profiles.conf repositories.d/default.conf" + +# Search path of dirs to install shell completions (only first match will be used) +COMPLETION_DIRS="${ROOT_PATH}/usr/share/bash-completion/completions" +COMPLETION_DIRS="${COMPLETION_DIRS} ${ROOT_PATH}/usr/share/bash-completion/bash_completion" +COMPLETION_DIRS="${COMPLETION_DIRS} ${ROOT_PATH}/usr/local/etc/bash_completion.d" +COMPLETION_DIRS="${COMPLETION_DIRS} ${ROOT_PATH}/etc/bash_completion.d" + +# Fix permissions (only root may edit and read since password & tokens can be in any of the files) +set_permission() { + _path="${CONFIG_PATH}/$1" + + if [ -e "${_path}" ] ; then + echo "Setting perms on ${_path}" + chown "${FILES_OWNER}" "${_path}" || return 1 + fi -# Fix permissions (only root may edit and read since password -# & tokens can be in any of the files) -if [ -d "${CONFIG_PATH}" ] ; then - chown -R root:root "${CONFIG_PATH}" - chmod -R 0640 "${CONFIG_PATH}" + if [ -d "${_path}" ] || echo "${_path}" | grep -q -E '.rc$' ; then + chmod 0755 "${_path}" + elif [ -f "${_path}" ] ; then + echo "$1" | grep -q .secret \ + && chmod 0400 "${_path}" \ + || chmod 0640 "${_path}" + fi +} + +if cd "${CONFIG_PATH}" ; then + for file in *.dist \ + conf.d conf.d/*.dist \ + profiles.d profiles.d/*.dist \ + repositories.d repositories.d/*.dist ${REPOSITORY_SECRET} \ + templates templates/*.dist \ + ${MERGEABLES} ; do + set_permission "${file}" + done else echo "config path (${CONFIG_PATH}) not found" exit 1 fi # Check installation -if [ ! -e "$(which resticprofile)" ] || ! resticprofile version ; then +if [ ! -e "$(which resticprofile)" ] || ! resticprofile version >/dev/null ; then echo "resticprofile not found or not executable" exit 1 fi -# Generate default-repo secret (if missing) +# Generate default-repo secret if missing if [ ! -f "${SECRET_FILE}" ] ; then echo "Generating ${SECRET_FILE}" - resticprofile random-key > "${SECRET_FILE}" + if resticprofile random-key > "${SECRET_FILE}" ; then + set_permission "${REPOSITORY_SECRET}" + else + exit 1 + fi fi -# Unwrap dist files (if target is missing) -cd "${CONFIG_PATH}" -for file in conf.d/*.dist profiles.d/*.dist templates/*.dist ; do +# Change scheduler to crond when systemd is missing (Alpine) +if [ ! -d "${ROOT_PATH}/etc/systemd/" ] ; then + sed -iE 's/#scheduler = "systemd"/scheduler = "crond"/g' "${CONFIG_PATH}/profiles.conf.dist" \ + || exit 1 +fi + +# Generate bash completions +for completion_path in ${COMPLETION_DIRS} ; do + completion_file="${completion_path}/resticprofile" + + if [ -d "${completion_path}" ] ; then + echo "Generating ${completion_file}" + if [ -f "${completion_file}" ] ; then + chmod u+w "${completion_file}" + fi + resticprofile completion-script --bash > "${completion_file}" \ + && chown root:root "${completion_file}" \ + && chmod 0555 "${completion_file}" + + break # install only in the first path + fi +done + +# Merge configuration updates with existing files +if [ -e "$(which diff3)" ] ; then + echo "Merging updates to config files" + + # Extract previous .dist files from .dist.cache + if [ -s "${CONFIG_CACHE_FILE}" ] ; then + cd "${CACHE_PATH}" \ + && tar -xzf "${CONFIG_CACHE_FILE}" + fi + + new_dist_cache_list="${TEMP_PATH}/dist_cache.list" + + cd "${CONFIG_PATH}" || exit 1 + + # Merge existing config with updates from .dist files + for file in $MERGEABLES ; do + target_file="${file}" + new_file="${file}.dist" + cached_file="${CACHE_PATH}/${new_file}" + output="${TEMP_PATH}/merged.conf" + + if [ -e "${new_file}" ] ; then + echo "${new_file}" >> "${new_dist_cache_list}" + fi + + if [ -e "${target_file}" ] && [ -e "${new_file}" ] && [ -e "${cached_file}" ] ; then + + diff3 --easy-only --merge "${new_file}" "${cached_file}" "${target_file}" > "${output}" + + if [ "$?" = "0" ] || [ "$?" = "1" ] ; then + if [ "$?" = "1" ] ; then + backup="${target_file}.prev" + cp -f "${target_file}" "${backup}" \ + && set_permission "${backup}" + echo "Conflicts found in \"${target_file}\", please verify. Created ${backup}" + fi + if [ -s "${output}" ] ; then + mv -f "${output}" "${target_file}" \ + && set_permission "${target_file}" + fi + else + echo "Failed merging \"${new_file}\" \"${cached_file}\" \"${target_file}\"" + fi + fi + done + + # Create new .dist.cache from current .dist files + if [ -s "${new_dist_cache_list}" ] ; then + tar -c --files-from "${new_dist_cache_list}" -zf "${CONFIG_CACHE_FILE}" + fi +fi + +# Unwrap remaining dist files where target file does not exist already +cd "${CONFIG_PATH}" || exit 1 + +for file in *.dist conf.d/*.dist profiles.d/*.dist repositories.d/*.dist templates/*.dist ; do target_file="$(dirname "${file}")/$(basename -s ".dist" "${file}")" + if [ -e "${target_file}" ] ; then - echo "Skipping ${target_file}. File already exists" rm "${file}" else - mv -f "${file}" "${target_file}" + mv -f "${file}" "${target_file}" \ + && set_permission "${target_file}" fi done diff --git a/contrib/posix/post-install_test.sh b/contrib/posix/post-install_test.sh new file mode 100755 index 000000000..26424e1a5 --- /dev/null +++ b/contrib/posix/post-install_test.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +SCRIPT_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd -P )" + +TEMP_PATH="$(mktemp -d)" +if [[ -z ${TEMP_PATH} || ! -d "${TEMP_PATH}" ]] ; then + exit 1 +fi +export ROOT_PATH="${TEMP_PATH}" + +trap 'cleanup remove ; cd "${SCRIPT_PATH}"' EXIT INT TERM + +function cleanup() { + echo "Cleaning '${TEMP_PATH}' $1" + rm -rf "${TEMP_PATH}" + [[ "$1" == "remove" ]] || mkdir -p "${TEMP_PATH}" +} + +function invoke() { + cd "${SCRIPT_PATH}" && ./post-install.sh +} + +function setup() { + cleanup "" + local config="${TEMP_PATH}/etc/resticprofile" + mkdir -p "${config}" \ + && cp -R *.conf *.rc conf.d profiles.d repositories.d templates "${config}" \ + && find "${config}" -name "*.conf" -exec mv {} {}.dist \; + find "${config}" +} + +# TODO tests +setup && invoke || exit 1 +find "${TEMP_PATH}" + diff --git a/contrib/posix/profiles.conf b/contrib/posix/profiles.conf index 802ce9e05..197afa35d 100644 --- a/contrib/posix/profiles.conf +++ b/contrib/posix/profiles.conf @@ -4,23 +4,29 @@ # See https://github.com/creativeprojects/resticprofile # # Note: This configuration file should not be changed directly. -# Add overrides to "conf.d" and profiles to "profiles.d". +# Configure resticprofile in "(conf|repositories).d" and add +# profiles to "profiles.d". # ----------------------------------------------------------------------------- +## Configuration file format version +version = 2 + ## -# Loading config overrides from "conf.d" and profiles from "profiles.d": +# Loading config from "(conf|repositories).d" and profiles from "profiles.d": includes = [ + 'templates/*.conf', 'conf.d/*.conf', + 'repositories.d/*.conf', + 'profiles.d/*.conf', 'profiles.d/*.toml', 'profiles.d/*.yaml', ] - ## # The global section [global] -# initialize a repository if none exists at the specified location -# (can be overriden in individual profiles) +# Initialize a repository if none exists at the specified location? +# Is configured in profiles, global setting should remain 'false'. initialize = false # restic IO priority @@ -39,7 +45,7 @@ systemd-unit-template = "templates/systemd.unit.in" systemd-timer-template = "templates/systemd.timer.in" # what to run when no command is specified -default-command = "snapshots" +default-command = "profiles" # Optional: Specify path to restic #restic-binary = "/usr/local/bin/restic" @@ -57,7 +63,8 @@ default-command = "snapshots" # # The default profile should not be used directly for running backups # nor should it contain schedules. -[default] +[profiles.default] +description = "Profile defaults for this host" ## Locks # Prevent concurrent invocation of a profile lock = "{{.TempDir}}/resticprofile-{{.Profile.Name}}.lock" @@ -66,122 +73,46 @@ force-inactive-lock = true ## -# Backup defaults -[default.backup] - -# Hostname and tags to identify backup snapshots in the repository -#### {{block "conf:default-backup-tags" .}} -# Multiple tags can be used, defaulting to profile name. -# Important: Set the same tags for "backup" and "retention" -tag = [ "{{.Profile.Name}}" ] -#### {{end}} -#### {{block "conf:default-backup-host" .}} -# Specify a hostname or leave it at 'true' for the current hostname -host = true -#### {{end}} - -# Exclude known cache files & folders from backups -exclude-caches = true - -# Exclude nested filesystems -one-file-system = true - -# Toggle whether a failure in reading a backup source is considered an error -#no-error-on-warning = false - -# Wait on acquiring locks when running the profile on a schedule -schedule-lock-wait = "30m" - -# Specify the user that runs profile tasks on a schedule -# "system" - root runs the profile tasks -# "user" - user that created the schedule runs the profile tasks -schedule-permission = "system" - -# Toggle verbose output for troubleshooting -#verbose = false - - -## -# Snapshot retention defaults -[default.retention] -# Remove obsolete snapshots prior to starting a backup -before-backup = false -# Remove obsolete snapshots after a successful backup -after-backup = true - -# Copying "host" and "tags" blocks to identify snapshots to retain or remove -# Note: "host" and "tag" must be in sync between "backup" and "retention" -{{template "conf:default-backup-host" .}} -# In retention, tags can be copied from backup with `true`: -tags = true - -# Copying backup source paths to identify snapshots to retain or remove -# Set to "false" or a list of paths to disable or customize the path filter -path = true - -# Specify the snapshots to keep when checking for obsolete snapshots -# Snapshots that do not match any condition are removed -keep-tag = [ "forever" ] -keep-last = 3 -#keep-hourly = 1 -#keep-daily = 1 -#keep-weekly = 1 -#keep-monthly = 1 -#keep-yearly = 1 -#keep-within = "30d" - -# Use compact format for listing snapshots -#compact = false - -# Enable to prune the repository immediatelly as snapshots are removed -# Prune can be expensive. Consider scheduling prune (see "conf.d/prune.conf") -#prune = true - - -## -# Defaults for showing snapshots of this host -# Usage: -# - `resticprofile snapshots` to view snapshots of this host -[default.snapshots] -# Copying "host" block to identify snapshots to list -{{template "conf:default-backup-host" .}} - - -## -# Defaults for mounting snapshot of this host -# Usage: -# - `resticprofile mount /mnt/restore` to mount snapshots of this host -[default.mount] -# Copying "host" block to identify snapshots to mount -{{template "conf:default-backup-host" .}} - - - -## -# The "base" profile is the base for all other profiles to inherit from +# The "base" profile is the dedicated parent to all other profiles and it +# extends "default" with additional settings for backup, restore & maintenance # # Note: Profiles that do not inherit from "base" will run on built-in # defaults instead and have to take care of a full profile setup. -[base] +[profiles.base] +description = "Base config for derived profiles" # Inherit all settings from the "default" profile inherit = "default" +initialize = true ## -# Configures the "snapshots" command for profiles inheriting from "base" -# Usage: -# - `resticprofile -n profileName snapshots` to view snapshots of one profile -[base.snapshots] -# Copying "host" and "tags" blocks to identify snapshots to list -{{template "conf:default-backup-host" .}} -{{template "conf:default-backup-tags" .}} +# The "maintenance" profile is used to perform ondemand or scheduled +# repository maintenance tasks like prune and check. +# +# See also "conf.d/check.conf" & "conf.d/prune.conf" +[profiles.maintenance] +description = "Maintenance for the default repository" +inherit = "base" +# We never initialize a new repository for maintenance +initialize = false + +## +# Group for profiles that check & verify repositories +# Usage +# - "resticprofile maintenance-all.check" +# - "resticprofile maintenance-all.prune" +# - "resticprofile maintenance-all.cache" +# - "resticprofile maintenance-all.schedule" +# - "resticprofile maintenance-all.unschedule" +[groups.maintenance-all] +description = "Group of check and cleanup profiles" +profiles = [ "maintenance" ] ## -# Configures the "mount" command for profiles inheriting from "base" -# Usage: -# - `resticprofile -n profileName mount /mnt/restore` to mount snapshots -[base.mount] -# Copying "host" and "tags" block to identify snapshots to mount -{{template "conf:default-backup-host" .}} -{{template "conf:default-backup-tags" .}} +# Group for profiles that perform actual backups +# Usage +# - "resticprofile backup-all.backup" +[groups.backup-all] +description = "Group of backup profiles" +profiles = [ ] diff --git a/contrib/posix/profiles.d/fs-snapshot.yaml.sample b/contrib/posix/profiles.d/fs-snapshot.yaml.sample index d37c9d57a..1dd0040b3 100644 --- a/contrib/posix/profiles.d/fs-snapshot.yaml.sample +++ b/contrib/posix/profiles.d/fs-snapshot.yaml.sample @@ -1,54 +1,65 @@ +# ----------------------------------------------------------------------------- ## -# Example profile for applications whose files must be backed-up from a readonly snapshot. +# Example profile for applications whose files must be backed-up from a +# readonly snapshot. # -applications: - inherit: base +profiles: + applications: + description: Applications backup + inherit: base - backup: - source: - - /opt/apps/_backup - - /mnt/data_backup - - /opt/vms/my-vm1.xml - - /opt/vms/my-vm1.qcow2 + backup: + source: + - /opt/apps/_backup + - /mnt/data_backup + - /opt/vms/my-vm1.xml + - /opt/vms/my-vm1.qcow2 - # Create snapshots of supported sources prior to running a backup - run-before: - # Snapshot on Btrfs (mounted on /opt/apps/_backup) - - btrfs subvolume snapshot -r /opt/apps/ /opt/apps/_backup + # Create snapshots of supported sources prior to running a backup + run-before: + # Snapshot on Btrfs (mounted on /opt/apps/_backup) + - btrfs subvolume snapshot -r /opt/apps/ /opt/apps/_backup - # Snapshot on LVM (mounted on /opt/apps/_backup) - - lvcreate -l100%FREE -s -n data_backup /dev/vg00/data && mount /dev/vg00/data_backup /mnt/data_backup + # Snapshot on LVM (mounted on /opt/apps/_backup) + - lvcreate -l100%FREE -s -n data_backup /dev/vg00/data && mount /dev/vg00/data_backup /mnt/data_backup - # Snapshot & config dump for libvirt VMs (VM disk images are readonly during backup) - - virsh dumpxml "my-vm1" > /opt/vms/my-vm1.xml - - {{template "libvirt-create-snapshot" "my-vm1"}} + # Snapshot & config dump for libvirt VMs (VM disk images are readonly during backup) + - virsh dumpxml "my-vm1" > /opt/vms/my-vm1.xml + - {{ template "libvirt-create-snapshot" "my-vm1" }} - # Release snapshots after backup - run-finally: - # Cleanup on Btrfs - - btrfs subvolume delete /opt/apps/_backup + # Release snapshots after backup + run-finally: + # Cleanup on Btrfs + - btrfs subvolume delete /opt/apps/_backup - # Cleanup on LVM - - umount /mnt/data_backup && lvremove -f /dev/vg00/data_backup + # Cleanup on LVM + - umount /mnt/data_backup && lvremove -f /dev/vg00/data_backup - # Cleanup for libvirt VMs (VM disk image receives changes from livedata file) - - {{template "libvirt-delete-snapshot" "my-vm1"}} + # Cleanup for libvirt VMs (VM disk image receives changes from livedata file) + - {{ template "libvirt-delete-snapshot" "my-vm1" }} +groups: + backup-all: + profiles: [ "applications" ] + ##### -# Config Templates -# Use -# - `resticprofile -n applications show` to see rendered result +# Config templates +# Notes: +# - "resticprofile applications.show" shows rendered template output +# - Could also be saved to "templates/my-libvirt-utils.conf" and shared # -{{define "libvirt-create-snapshot" -}} +{{ define "libvirt-create-snapshot" -}} virsh snapshot-create-as --domain "{{.}}" --name "{{.}}-backup" {{- " " -}} --diskspec "vda,file=/var/db/{{.}}-livedata.qcow2" {{- " " -}} --disk-only --atomic --no-metadata --quiesce -{{- end}} +{{- end }} # -{{define "libvirt-delete-snapshot" -}} +{{ define "libvirt-delete-snapshot" -}} virsh blockcommit --domain "{{.}}" vda --wait --active {{- " " -}} && virsh blockjob --domain "{{.}}" "/var/db/{{.}}-livedata.qcow2" --pivot {{- " " -}} && rm -f /var/db/{{.}}-livedata.qcow2 -{{- end}} +{{- end }} ##### + +# ----------------------------------------------------------------------------- diff --git a/contrib/posix/profiles.d/minimal.conf.sample b/contrib/posix/profiles.d/minimal.conf.sample new file mode 100644 index 000000000..d1814242b --- /dev/null +++ b/contrib/posix/profiles.d/minimal.conf.sample @@ -0,0 +1,27 @@ +## +# Minimal backup configuration example +# +# Usage: +# - "resticprofile minimal.show" +# - "resticprofile --dry-run minimal.backup" +# - "resticprofile minimal.backup" +# - "resticprofile minimal.snapshots" +# - "resticprofile minimal.mount /restore" +# - "resticprofile minimal.status" +# - "resticprofile minimal.schedule" +# - "resticprofile minimal.unschedule" +# +[profiles.minimal] +description = "Minimal backup example" +inherit = "base" + +[profiles.minimal.backup] +schedule = "daily" +source = [ + "/path/to/backup", + "/other/path/to/backup", +] + + +[groups.backup-all] +profiles = [ "minimal" ] diff --git a/contrib/posix/profiles.d/minimal.yaml.sample b/contrib/posix/profiles.d/minimal.yaml.sample new file mode 100644 index 000000000..02b546796 --- /dev/null +++ b/contrib/posix/profiles.d/minimal.yaml.sample @@ -0,0 +1,28 @@ +## +# Minimal backup configuration example +# +# Usage: +# - "resticprofile minimal.show" +# - "resticprofile --dry-run minimal.backup" +# - "resticprofile minimal.backup" +# - "resticprofile minimal.snapshots" +# - "resticprofile minimal.mount /restore" +# - "resticprofile minimal.status" +# - "resticprofile minimal.schedule" +# - "resticprofile minimal.unschedule" +# +profiles: + minimal: + description: Minimal backup example + inherit: base + + backup: + schedule: daily + source: + - /path/to/backup + - /other/path/to/backup + + +groups: + backup-all: + profiles: [ "minimal" ] diff --git a/contrib/posix/profiles.d/system.conf b/contrib/posix/profiles.d/system.conf new file mode 100644 index 000000000..197663ede --- /dev/null +++ b/contrib/posix/profiles.d/system.conf @@ -0,0 +1,55 @@ +# ----------------------------------------------------------------------------- +## +# The "root" profile to backup the system +# +# Backup $HOME of root and 'etc' folders once a day +# +# Usage: +# - "resticprofile root.show" +# - "resticprofile --dry-run root.backup" +# - "resticprofile root.backup" +# - "resticprofile root.snapshots" +# - "resticprofile root.mount /restore" +# - "resticprofile root.status" +# - "resticprofile root.schedule" +# - "resticprofile root.unschedule" +# +[profiles.root] +description = "System backup" +inherit = "base" + +## +# Backup sources and schedule +[profiles.root.backup] +no-error-on-warning = false +schedule = "daily" +source = [ + "/etc", + "/usr/local/etc", + "/opt/local/etc", + "~root/", +] +exclude = [ + "*.swp", +] +#exclude-larger-than = "2g" +#include = [ +# "**", +#] + +## +# Keep last 14 days and the latest from each of last 8 weeks and 6 months +# Total snapshots to keep: 24 +# (14 days, weeks not covered by days and months not covered by weeks) +[profiles.root.retention] +keep-last = true +keep-daily = 14 +keep-weekly = 8 +keep-monthly = 6 + +## +# Add to "backup-all" +[groups.backup-all] +profiles = [ "root" ] + +# ----------------------------------------------------------------------------- \ No newline at end of file diff --git a/contrib/posix/profiles.d/system.toml b/contrib/posix/profiles.d/system.toml deleted file mode 100644 index 70eb089c5..000000000 --- a/contrib/posix/profiles.d/system.toml +++ /dev/null @@ -1,38 +0,0 @@ -## -# The "root" profile -# -# Backup $HOME of root and 'etc' folders once a day -# -# Usage: -# - `resticprofile -n root show` -# - `resticprofile -n root --dry-run backup` -# - `resticprofile -n root backup` -# - `resticprofile -n root snapshots` -# - `resticprofile -n root mount /restore` -# - `resticprofile -n root status` -# - `resticprofile -n root schedule` -# - `resticprofile -n root unschedule` -# -[root] -inherit = "base" - -## -# Backup sources and schedule -[root.backup] -source = [ - "/etc", - "/usr/local/etc", - "/opt/local/etc", - "~root/", -] -schedule = "daily" - -## -# Keep last 14 days and the latest from each of last 8 weeks and 6 months -# Total snapshots to keep: 24 -# (14 days, weeks not covered by days and months not covered by weeks) -[root.retention] -keep-last = false -keep-daily = 14 -keep-weekly = 8 -keep-monthly = 6 diff --git a/contrib/posix/conf.d/repository.conf b/contrib/posix/repositories.d/default.conf similarity index 57% rename from contrib/posix/conf.d/repository.conf rename to contrib/posix/repositories.d/default.conf index a43dfa4fd..2877e0bd5 100644 --- a/contrib/posix/conf.d/repository.conf +++ b/contrib/posix/repositories.d/default.conf @@ -1,4 +1,4 @@ - +# ----------------------------------------------------------------------------- ## # Repository configuration # See https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html @@ -6,62 +6,49 @@ ## # Default repository (used in all derived profiles unless redefined) -[default] -# Local: Repository mounted to local folder +[profiles.default] +# Repository password file +password-file = "repositories.d/default.secret" + +## +# Local: Repository created in local path repository = "local:/backup" #run-before = [ 'mountpoint -q /backup' ] +## # SFTP: (requires password-less public-key auth for the user running restic) #repository = "sftp:user@host:/restic-repo" #repository = "sftp://user@[::1]:2222//restic-repo" +## # REST server: (https://github.com/restic/rest-server) #repository = "rest:https://user:pass@host:8000/my_backup_repo/" -#cacert = "conf.d/default-repository-self-signed-pub.pem" -#tls-client-cert = "conf.d/default-repository-client.pem" +#cacert = "repositories.d/default-repository-self-signed-pub.pem" +#tls-client-cert = "repositories.d/default-repository-client.pem" +## # S3 storage (see [default.env]) #repository = "s3:s3.amazonaws.com/bucket_name" #repository = "s3:http://host:9000/bucket_name" #repository = "s3:https://host/bucket_name" -#cacert = "conf.d/default-repository-self-signed-pub.pem" +#cacert = "repositories.d/default-repository-self-signed-pub.pem" +## # Azure storage (see [default.env]) #repository = "azure:container_name:/" -# Repository password file -password-file = "conf.d/default-repository.secret" ## # Environment variables to pass to "restic" -[default.env] +[profiles.default.env] +## # S3 Storage (AWS, Minio, etc.) #AWS_ACCESS_KEY_ID = "id" #AWS_SECRET_ACCESS_KEY = "key" +## # Azure Blob Storage #AZURE_ACCOUNT_NAME = "storage_account" #AZURE_ACCOUNT_KEY = "key" - -## -# Initialize the repository (if empty) for profiles deriving from "base" -[base] -# Initialize a repository if none exists at the specified location -initialize = true - - -## -# Example: Secondary repository -# Other repository for profiles inheriting from "other-repository-base": -# -# [other-repository-base] -# inherit = "base" -# repository = "local:/backup-other" -# -# Usage: -# [my-profile-other] -# inherit = "other-repository-base" -# [my-profile-other.backup] -# source = "/path" -# +# ----------------------------------------------------------------------------- diff --git a/contrib/posix/repositories.d/other.conf.sample b/contrib/posix/repositories.d/other.conf.sample new file mode 100644 index 000000000..2221d6935 --- /dev/null +++ b/contrib/posix/repositories.d/other.conf.sample @@ -0,0 +1,69 @@ +## +# Example: Secondary repository +# +# Usage: +# +# [profiles.my-profile-other] +# inherit = "base-other" +# [profiles.my-profile-other.backup] +# source = "/path" +# + + +# ----------------------------------------------------------------------------- +## +# Secondary repository for profiles inheriting from "other-base" +[profiles.base-other] +# Inherit all settings from base +inherit = "base" + +# +# Repository password for the secondary repository +# +# Generate with: +# - "resticprofile random-key > repositories.d/other-repository.secret" +password-file = "repositories.d/other-repository.secret" + +# +# See "default.conf" for more examples +repository = "local:/backup-other" + +# ----------------------------------------------------------------------------- + + +# ----------------------------------------------------------------------------- +## +# Setup repository maintenance for "base-other" +[profiles.maintenance-other] +description = "Maintenance for other repository" +inherit = "base-other" +initialize = false + + +## +# Configuring "check" schedule in maintenance profile +[profiles.maintenance-other.check] + +# Schedule at 5th, 10th, 15th, 20th, 25th and 30th day in month at 04:15 +schedule = "*-*-5,10,15,20,25,30 04:15:00" + +# Read repository data for verification every 15th day in month +{{ if .Now.Day | eq 15 -}} +read-data = true +{{ end -}} +#read-data-subset = "100%" + + +## +# Configuring "prune" schedule in maintenance profile +[profiles.maintenance-other.prune] +schedule = "daily" +#schedule = "weekly" + + +## +# Add "maintenance-other" to "maintenance-all" +[groups.maintenance-all] +profiles = [ "maintenance-other" ] + +# ----------------------------------------------------------------------------- diff --git a/contrib/posix/resticprofile-send-error.rc b/contrib/posix/resticprofile-send-error.rc new file mode 100644 index 000000000..7acff1e6c --- /dev/null +++ b/contrib/posix/resticprofile-send-error.rc @@ -0,0 +1,39 @@ +# ----------------------------------------------------------------------------- +# +# Configuration for "resticprofile-send-error" +# +# Usage, see: +# resticprofile-send-error -h +# +# Examples: +# +# Test mail sending: +# resticprofile-send-error -f user@domain.org +# +# Send if PROFILE_NAME is set +# resticprofile-send-error user@domain.org +# +# Send if PROFILE_NAME & MAIL_TO are set +# resticprofile-send-error +# +# Send if PROFILE_COMMAND is "check" "backup" or "prune": +# resticprofile-send-error -o check,backup,prune user@domain.org +# +# Send only if RESTICPROFILE_ON_SCHEDULE is set to 1 +# resticprofile-send-error -s user@domain.org +# +# ----------------------------------------------------------------------------- + +## +# Sender address +#MAIL_FROM="\"resticprofile $(hostname -f)\" <$USER@$(hostname -f)>" + +## +# Subject line +#MAIL_SUBJECT="restic failed: \"${PROFILE_COMMAND}\" in \"${PROFILE_NAME}\"" + +## +# Recipients (are combined with recipients specified in CLI) +# Use space as delimiter when declaring more than one recipient +#MAIL_TO="admin1@mynet.org admin2@mynet.org ..." +#MAIL_TO="" diff --git a/contrib/posix/templates/default-host.conf b/contrib/posix/templates/default-host.conf new file mode 100644 index 000000000..decb73dae --- /dev/null +++ b/contrib/posix/templates/default-host.conf @@ -0,0 +1,7 @@ +{{ define "conf:default-host" }} + +# Specify a hostname to set in snapshots created by this system. +# Use 'true' for the current hostname of the system +host = true + +{{ end }}