2023-03-05 08:15:16 +01:00
|
|
|
#!/bin/bash
|
|
|
|
|
|
|
|
declare -a pkgs
|
|
|
|
while read pkg; do
|
|
|
|
pkgs+=("${pkg}")
|
|
|
|
done
|
|
|
|
|
2023-03-06 00:14:09 +01:00
|
|
|
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
|
|
|
|
|
2023-03-06 00:39:18 +01:00
|
|
|
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
|
2023-03-06 02:38:17 +01:00
|
|
|
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
|
2023-03-06 00:13:33 +01:00
|
|
|
if [[ ! "${pkgs_list_max_length}" ]]; then pkgs_list_max_length='24'; fi
|
2023-03-06 00:57:02 +01:00
|
|
|
if [[ ! "${snap_only_local_datasets}" ]]; then snap_only_local_datasets='true'; fi
|
2023-03-06 01:21:56 +01:00
|
|
|
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
|
2023-03-06 01:21:56 +01:00
|
|
|
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}"
|
|
|
|
;;
|
2023-03-06 02:14:47 +01:00
|
|
|
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
|
2023-03-07 00:01:45 +01:00
|
|
|
if grep -Piq -- '^('"${important_names}"')$' <<<"${pkg}"; then
|
2023-03-05 08:15:16 +01:00
|
|
|
important_pkgs_in_transaction+=("${pkg}")
|
|
|
|
else
|
2023-03-05 22:17:21 +01:00
|
|
|
trivial_pkgs_in_transaction+=("${pkg}")
|
2023-03-05 08:15:16 +01:00
|
|
|
fi
|
|
|
|
done
|
|
|
|
}
|
|
|
|
|
2023-03-06 00:14:47 +01:00
|
|
|
function set_severity () {
|
|
|
|
if [[ "${#important_pkgs_in_transaction[@]}" -ge '1' ]]; then
|
2023-03-06 02:38:39 +01:00
|
|
|
severity="${snaps_important_suffix}"
|
2023-03-06 00:14:47 +01:00
|
|
|
else
|
2023-03-06 02:38:39 +01:00
|
|
|
severity="${snaps_trivial_suffix}"
|
2023-03-06 00:14:47 +01:00
|
|
|
fi
|
|
|
|
}
|
|
|
|
|
2023-03-06 00:32:58 +01:00
|
|
|
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}"
|
|
|
|
}
|
|
|
|
|
2023-03-06 00:57:02 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-03-06 00:15:23 +01:00
|
|
|
function write_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
|
|
|
|
}
|
|
|
|
|
2023-03-06 01:24:13 +01:00
|
|
|
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' '_')"
|
|
|
|
|
2023-03-06 03:06:19 +01:00
|
|
|
local dataset_name_no_pkgs
|
2023-03-06 01:24:13 +01:00
|
|
|
max_dataset_name_length='0'
|
|
|
|
for dataset in "${snappable_datasets[@]}"; do
|
2023-03-06 02:15:28 +01:00
|
|
|
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:'
|
2023-03-06 01:24:13 +01:00
|
|
|
if [[ "${#dataset_name_no_pkgs}" -gt "${max_dataset_name_length}" ]]; then
|
|
|
|
max_dataset_name_length="${#dataset_name_no_pkgs}"
|
|
|
|
fi
|
|
|
|
done
|
2023-03-06 02:04:37 +01:00
|
|
|
|
|
|
|
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
|
2023-03-06 01:24:13 +01:00
|
|
|
}
|
|
|
|
|
2023-03-06 02:07:54 +01:00
|
|
|
function trim_pkg_list_oneline () {
|
|
|
|
local available_pkg_list_length
|
|
|
|
available_pkg_list_length="$((${max_zfs_snapshot_name_length} - ${max_dataset_name_length}))"
|
2023-03-06 03:10:01 +01:00
|
|
|
if [[ "${available_pkg_list_length}" -lt "${pkgs_list_max_length}" ]]; then
|
2023-03-06 02:07:54 +01:00
|
|
|
# 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
|
2023-03-06 03:15:48 +01:00
|
|
|
shorter_pkg_list="${unabridged_pkg_list_oneline}"
|
2023-03-06 02:07:54 +01:00
|
|
|
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
|
2023-03-06 02:07:54 +01:00
|
|
|
# If this is still too long we empty the package list
|
|
|
|
shorter_pkg_list=''
|
2023-03-06 02:55:21 +01:00
|
|
|
fi
|
2023-03-06 02:07:54 +01:00
|
|
|
trimmed_pkg_list_oneline="${shorter_pkg_list}"
|
|
|
|
}
|
|
|
|
|
2023-03-06 02:15:52 +01:00
|
|
|
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}"
|
2023-03-06 02:47:11 +01:00
|
|
|
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
|
2023-03-06 02:15:52 +01:00
|
|
|
done
|
|
|
|
}
|
|
|
|
|
2023-03-06 02:39:51 +01:00
|
|
|
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}"
|
|
|
|
}
|
|
|
|
|
2023-03-06 02:40:16 +01:00
|
|
|
function do_retention () {
|
|
|
|
local snap_list snaps_done_by_us snaps_in_cur_sev snaps_limit oldest_snap
|
2023-03-06 02:47:11 +01:00
|
|
|
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
|
2023-03-06 02:40:16 +01:00
|
|
|
done
|
2023-03-06 02:47:11 +01:00
|
|
|
fi
|
2023-03-06 02:40:16 +01:00
|
|
|
}
|
|
|
|
|
2023-03-05 08:15:16 +01:00
|
|
|
function main () {
|
|
|
|
local pkgs_in_transaction
|
|
|
|
pkgs_in_transaction=("${@}")
|
|
|
|
|
2023-03-06 00:16:00 +01:00
|
|
|
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[@]}"
|
|
|
|
|
2023-03-06 00:14:47 +01:00
|
|
|
local severity
|
|
|
|
set_severity
|
|
|
|
|
2023-03-06 00:32:58 +01:00
|
|
|
local -a globally_snappable_datasets
|
|
|
|
get_globally_snappable_datasets
|
|
|
|
|
2023-03-06 00:57:02 +01:00
|
|
|
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
|
|
|
|
|
2023-03-06 03:15:48 +01:00
|
|
|
local unabridged_pkg_list_oneline
|
2023-03-06 00:15:23 +01:00
|
|
|
write_pkg_list_oneline
|
|
|
|
|
2023-03-06 02:15:28 +01:00
|
|
|
local date_string max_zfs_snapshot_name_length max_dataset_name_length
|
|
|
|
date_string="$(date +"${snap_date_format}")"
|
2023-03-06 02:07:54 +01:00
|
|
|
max_zfs_snapshot_name_length='255'
|
2023-03-06 01:24:13 +01:00
|
|
|
find_max_dataset_name_length
|
|
|
|
|
2023-03-06 02:07:54 +01:00
|
|
|
local trimmed_pkg_list_oneline
|
|
|
|
trim_pkg_list_oneline
|
2023-03-06 02:15:52 +01:00
|
|
|
|
2023-03-06 02:40:16 +01:00
|
|
|
local -a successfully_snapped_datasets
|
2023-03-06 02:15:52 +01:00
|
|
|
do_snaps
|
2023-03-06 02:40:16 +01:00
|
|
|
do_retention
|
2023-03-05 08:15:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
main "${pkgs[@]}"
|