zfs-pacman-hook/pacman-zfs-snapshot.sh

278 lines
10 KiB
Bash
Raw Normal View History

2023-03-05 08:15:16 +01:00
#!/bin/bash
declare -a pkgs
while read pkg; do
pkgs+=("${pkg}")
done
declare operation
operation="${1}"
2023-03-05 08:15:16 +01:00
declare conf_file
conf_file='/etc/pacman-zfs-snapshot.conf'
declare important_names snaps_trivial_keep snaps_important_keep snaps_trivial_suffix snaps_important_suffix
if [[ -r "${conf_file}" ]]; then
source "${conf_file}"
fi
if [[ ! "${do_dry_run}" ]]; then do_dry_run='true'; fi
2023-03-05 08:15:16 +01:00
if [[ ! "${important_names}" ]]; then important_names='linux'; fi
if [[ ! "${snaps_trivial_keep}" ]]; then snaps_trivial_keep='15'; fi
2023-03-05 08:15:16 +01:00
if [[ ! "${snaps_important_keep}" ]]; then snaps_important_keep='5'; fi
if [[ ! "${snaps_trivial_suffix}" ]]; then snaps_trivial_suffix='trv'; fi
if [[ ! "${snaps_important_suffix}" ]]; then snaps_important_suffix='imp'; fi
if [[ ! "${pkgs_list_max_length}" ]]; then pkgs_list_max_length='24'; fi
if [[ ! "${snap_only_local_datasets}" ]]; then snap_only_local_datasets='true'; fi
if [[ ! "${snap_field_separator}" ]]; then snap_field_separator='_'; fi
if [[ ! "${snap_name_prefix}" ]]; then snap_name_prefix='pac'; fi
2023-03-06 03:02:02 +01:00
if [[ ! "${snap_date_format}" ]]; then snap_date_format='%F-%H%M'; fi
if [[ ! "${snap_op_installation_suffix}" ]]; then snap_op_installation_suffix='inst'; fi
if [[ ! "${snap_op_remove_suffix}" ]]; then snap_op_remove_suffix='rmvl'; fi
if [[ ! "${snap_op_upgrade_suffix}" ]]; then snap_op_upgrade_suffix='upgr'; fi
2023-03-05 08:15:16 +01:00
2023-03-06 02:03:54 +01:00
function pprint () {
local style msg exit_code
style="${1:?}"
msg="${2:?}"
exit_code="${3}"
local color_reset color_lyellow
color_reset='\e[0m'
color_lyellow='\e[93m'
color_red='\e[31m'
case "${style}" in
warn)
printf -- "${color_lyellow}"'[WARN]'"${color_reset}"' %s\n' "${msg}"
;;
err)
printf -- "${color_red}"'[ERR]'"${color_reset}"' %s\n' "${msg}"
;;
info)
2023-03-06 02:03:54 +01:00
printf -- '[INFO] %s\n' "${msg}"
;;
esac
[[ "${exit_code}" ]] && exit "${exit_code}"
}
2023-03-05 08:15:16 +01:00
function split_pkgs_by_importance () {
local pkgs_in_transaction
pkgs_in_transaction=("${@}")
for pkg in "${pkgs_in_transaction[@]}"; do
if grep -Piq -- '^'"${important_names}"'$' <<<"${pkg}"; then
important_pkgs_in_transaction+=("${pkg}")
else
trivial_pkgs_in_transaction+=("${pkg}")
2023-03-05 08:15:16 +01:00
fi
done
}
function set_severity () {
if [[ "${#important_pkgs_in_transaction[@]}" -ge '1' ]]; then
severity="${snaps_important_suffix}"
else
severity="${snaps_trivial_suffix}"
fi
}
function get_globally_snappable_datasets () {
local datasets_list
# For all datasets show their 'space.quico:auto-snapshot' property; only
# print dataset name in column 1 and property value in column 2. In awk
# limit this list to datasets where tab-delimited column 2 has exact
# string '^true$' then further limit output by eliminating snapshots
# from list, i.e. dataset names that contain an '@' character.
datasets_list="$(zfs get -H -o 'name,value' 'space.quico:auto-snapshot' | \
awk -F'\t' '{if($2 ~ /^true$/ && $1 !~ /@/) print $1}')"
while IFS= read -r dataset; do
globally_snappable_datasets+=("${dataset}")
done <<<"${datasets_list}"
}
function get_local_snappable_datasets () {
local datasets_list
datasets_list="$(findmnt --json --list --output 'fstype,source,target' | \
jq --raw-output '.[][] | select(.fstype=="zfs") | .source')"
while IFS= read -r dataset; do
local_snappable_datasets+=("${dataset}")
done <<<"${datasets_list}"
}
function trim_globally_snappable_datasets () {
for global_dataset in "${globally_snappable_datasets[@]}"; do
for local_dataset in "${local_snappable_datasets[@]}"; do
if grep -Piq -- '^'"${local_dataset}"'$' <<<"${global_dataset}"; then
snappable_datasets+=("${global_dataset}")
fi
done
done
}
function write_pkg_list_oneline () {
local unabridged_pkg_list_oneline
if [[ "${severity}" == 'imp' ]]; then
for pkg in "${important_pkgs_in_transaction[@]}"; do
if [[ "${unabridged_pkg_list_oneline}" ]]; then
unabridged_pkg_list_oneline="${unabridged_pkg_list_oneline}"','"${pkg}"
else
unabridged_pkg_list_oneline="${pkg}"
fi
done
fi
if [[ "${#trivial_pkgs_in_transaction[@]}" -ge '1' ]]; then
for pkg in "${trivial_pkgs_in_transaction[@]}"; do
if [[ "${unabridged_pkg_list_oneline}" ]]; then
unabridged_pkg_list_oneline="${unabridged_pkg_list_oneline}"','"${pkg}"
else
unabridged_pkg_list_oneline="${pkg}"
fi
done
fi
}
function find_max_dataset_name_length () {
local longest_op_suffix op_suffix_string
longest_op_suffix='0'
for op_suffix in "${snap_op_installation_suffix}" "${snap_op_remove_suffix}" "${snap_op_upgrade_suffix}"; do
if [[ "${#op_suffix}" -gt "${longest_op_suffix}" ]]; then
longest_op_suffix="${#op_suffix}"
fi
done
op_suffix_string="$(head -c "${longest_op_suffix}" '/dev/zero' | tr '\0' '_')"
local longest_sev_suffix sev_suffix_string
longest_sev_suffix='0'
for sev_suffix in "${snaps_trivial_suffix}" "${snaps_important_suffix}"; do
if [[ "${#sev_suffix}" -gt "${longest_sev_suffix}" ]]; then
longest_sev_suffix="${#sev_suffix}"
fi
done
sev_suffix_string="$(head -c "${longest_sev_suffix}" '/dev/zero' | tr '\0' '_')"
local max_dataset_name_length dataset_name_no_pkgs
max_dataset_name_length='0'
for dataset in "${snappable_datasets[@]}"; do
dataset_name_no_pkgs="${dataset}"'@'"${snap_name_prefix}${snap_field_separator}${date_string}${snap_field_separator}"'op:'"${op_suffix_string}${snap_field_separator}"'sev:'"${sev_suffix_string}${snap_field_separator}"'pkgs:'
if [[ "${#dataset_name_no_pkgs}" -gt "${max_dataset_name_length}" ]]; then
max_dataset_name_length="${#dataset_name_no_pkgs}"
fi
done
if [[ "${max_dataset_name_length}" -gt "${max_zfs_snapshot_name_length}" ]]; then
pprint 'warn' 'Snapshot name would exceed ZFS '"${max_zfs_snapshot_name_length}"' chars limit. Skipping snapshots ...' '0'
fi
}
function trim_pkg_list_oneline () {
local available_pkg_list_length
available_pkg_list_length="$((${max_zfs_snapshot_name_length} - ${max_dataset_name_length}))"
if [[ "${available_pkg_list_length}" -lt "{pkgs_list_max_length}" ]]; then
# If we have fewer characters available than the user wants limit
# package list length
pkgs_list_max_length="${available_pkg_list_length}"
fi
local shorter_pkg_list
shorter_pkg_list="${pkg_list_oneline}"
while [[ "${#shorter_pkg_list}" -gt "${pkgs_list_max_length}" ]]; do
shorter_pkg_list="${shorter_pkg_list%,*}"
if ! grep -Piq ',' <<<"${shorter_pkg_list}"; then
# Only one package remains in package list, no need to continue
break
fi
done
2023-03-06 02:55:21 +01:00
if [[ "${#shorter_pkg_list}" -gt "${pkgs_list_max_length}" ]]; then
# If this is still too long we empty the package list
shorter_pkg_list=''
2023-03-06 02:55:21 +01:00
fi
trimmed_pkg_list_oneline="${shorter_pkg_list}"
}
function do_snaps () {
local snap_name
for snappable_dataset in "${snappable_datasets[@]}"; do
snap_name="${snappable_dataset}"'@'"${snap_name_prefix}${snap_field_separator}${date_string}${snap_field_separator}"'op:'"${operation}${snap_field_separator}"'sev:'"${severity}${snap_field_separator}"'pkgs:'"${trimmed_pkg_list_oneline}"
if [[ "${do_dry_run}" == 'true' ]]; then
pprint 'info' 'Dry-run, pretending to run zfs snapshot '"${snap_name}"
else
zfs snapshot "${snap_name}" && {
successfully_snapped_datasets+=("${snappable_dataset}")
pprint 'info' 'Snapshot done: '"${snap_name}"
} || {
pprint 'warn' 'Snapshot failed: '"${snap_name}"
}
fi
done
}
function get_snaps_in_cur_sev () {
local dataset_to_query
dataset_to_query="${1:?}"
snap_list="$(zfs list -H -o 'name' -t snapshot "${dataset_to_query}")"
snaps_done_by_us="$(grep -Pi -- '@'"${snap_name_prefix}${snap_field_separator}" <<<"${snap_list}")"
snaps_in_cur_sev="$(grep -Pi -- "${snap_field_separator}"'sev:'"${severity}${snap_field_separator}" <<<"${snaps_done_by_us}")"
printf -- '%s' "${snaps_in_cur_sev}"
}
function do_retention () {
local snap_list snaps_done_by_us snaps_in_cur_sev snaps_limit oldest_snap
if [[ "${do_dry_run}" == 'true' ]]; then
pprint 'info' 'Dry-run, skipping potential zfs destroy operations ...'
else
for successfully_snapped_dataset in "${successfully_snapped_datasets[@]}"; do
snaps_in_cur_sev="$(get_snaps_in_cur_sev "${successfully_snapped_dataset}")"
if [[ "${severity}" == "${snaps_important_suffix}" ]]; then
snaps_limit="${snaps_important_keep}"
else
snaps_limit="${snaps_trivial_keep}"
fi
while [[ "$(get_snaps_in_cur_sev "${successfully_snapped_dataset}" | wc -l)" -gt "${snaps_limit}" ]]; do
oldest_snap="$(get_snaps_in_cur_sev "${successfully_snapped_dataset}" | head -n1)"
zfs destroy "${oldest_snap}"
done
done
fi
}
2023-03-05 08:15:16 +01:00
function main () {
local pkgs_in_transaction
pkgs_in_transaction=("${@}")
local -a important_pkgs_in_transaction trivial_pkgs_in_transaction
2023-03-05 08:15:16 +01:00
split_pkgs_by_importance "${pkgs_in_transaction[@]}"
local severity
set_severity
local -a globally_snappable_datasets
get_globally_snappable_datasets
local -a snappable_datasets
if [[ "${snap_only_local_datasets}" == 'true' ]]; then
local local_snappable_datasets
get_local_snappable_datasets
trim_globally_snappable_datasets
else
snappable_datasets=("${globally_snappable_datasets}")
fi
local pkg_list_oneline
write_pkg_list_oneline
local date_string max_zfs_snapshot_name_length max_dataset_name_length
date_string="$(date +"${snap_date_format}")"
max_zfs_snapshot_name_length='255'
find_max_dataset_name_length
local trimmed_pkg_list_oneline
trim_pkg_list_oneline
local -a successfully_snapped_datasets
do_snaps
do_retention
2023-03-05 08:15:16 +01:00
}
main "${pkgs[@]}"