diff --git a/backup.sh b/backup.sh index 4f5bc6a..763e8b7 100755 --- a/backup.sh +++ b/backup.sh @@ -1,4 +1,5 @@ #!/usr/local/bin/bash + # # Hjemmesnekret script som tar backup av PVV-servere. # @@ -7,263 +8,363 @@ # Oppdatert av pederbs og yorinad 2017.02.04 # -#for å legge til en backup jobb: -# zfs create principal/backupz/%name% +# For å legge til en backup jobb, lag et nyt datasett under principal/backupz: # -#sørg for at principal kan logge inn som brukerene på boksen uten passord: -# -#ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsCm6f5JOlKDbZMiQ6rXvU35QU4Gs/WKKlZymXtFxPhK5jBoZskQjNhfsr+peuhnZ1y+04L9qEBkN3jN0ThROaRaKSFVWfnMnij7pEB0bmJRxDmkQsHL7YsAm6tPn8116tt9m9ASPYE5RpnaDGtxTP7uXa42URvwXdvWY618tdX4z39JG9f85KYexhwRbhaBMLnSPH6JKCKu1tTBwr7oEbuhLFrFXQwqWRip/oN/6/eSxnIrwaey1GM+CdNfeGh/0OeQJ4XIUas2WbgiQRw6Dkxo32FeFS5LVaOYWRys6W3Znw9a9yOYImjJ8WUmiwSeN2bksUizNBNA1HBhoKGM5b root@alphys +# zfs create principal/backupz/%name% # +# Sørg for at principal kan logge inn som brukerene på boksen uten passord. +# Det ligger et skript for å lage nye nøkkelpar ved './ssh_extra_keys/generate_keypair.sh', +# men du kan alternativt bruke nøkkelen som ligger i '~/.ssh/id_ed25519.pub'. +# I førstnevnte tilfelle må du også registrere boksen i './ssh_config_backup_targets'. -echo 'PVV; hjemmelaget backupscript ($Id: backup.sh,v 1.16 2025/05/24 17:22:00 root Exp root $)' +if ! command -v git >/dev/null 2>&1; then + echo "PVV; hjemmelaget backupscript (\$ git commit sha: ??? \$)" +else + echo "PVV; hjemmelaget backupscript (\$ git commit sha: $(cd /backupz && git rev-parse HEAD) \$)" +fi echo -echo $(date) +date +# === Sjekk at nødvendige verktøy er installert. === -# Blir brukt som navn på ZFS-snapshot og logger. -snapshot=`date +%Y%m%d` -if [ "x$1" = "xfull" ]; then # Fullbackupen starter før midnat, legg på en dag - snapshot=`date -v +1d +%Y%m%d` +required_tools=( + rsync + ssh + zfs + zpool + awk +) +for tool in "${required_tools[@]}"; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "Mangler påkrevd verktøy: $tool" + exit 1 + fi +done + +if [ "$(id -u)" -ne 0 ]; then + echo "Må kjøres som root!" + exit 1 fi -lockfile="/backupz/backup.sh.lock" +# 5.1 er minimum pga. wait '-p' flagget. +minimum_bash_major_version="5" +minimum_bash_minor_version="1" +if [ \ + "${BASH_VERSINFO[0]}" -lt "$minimum_bash_major_version" ] || \ + { [ "${BASH_VERSINFO[0]}" -eq "$minimum_bash_major_version" ] && \ + [ "${BASH_VERSINFO[1]}" -lt "$minimum_bash_minor_version" ]; }; then + echo "Må kjøres med bash versjon $minimum_bash_major_version eller nyere!" + exit 1 +fi + +# === Sjekk for pågående backup eller avbrutt backup, sett låsfil og signalhåndtering. === + +# Blir brukt som navn på ZFS-snapshot og logger. +snapshot_date="$(date +%Y%m%d)" +if [ "$1" = "full" ]; then # Fullbackupen starter før midnatt, legg på en dag + snapshot_date=$(date -v +1d +%Y%m%d) +fi + +# Sjekk om en annen backup kjører eller om forrige backup ble avbrutt. +lockfile="/backupz/backup.sh.lock" if [ -e $lockfile ]; then - pid=$(cat $lockfile) + existing_pid="$(cat $lockfile)" # TODO: Bruk bedre metode for å finne prosess. - if ps -p $pid ; then - echo "Backup kjører allerede:" `cat $lockfile` - exit -1 + if ps -p "$existing_pid"; then + echo "Backup kjører allerede: $existing_pid" + exit 1 fi echo "Forrige backup ble avbrutt, rydder opp..." - rm $lockfile + rm "$lockfile" - zfs snapshot -r principal/backupz@avbrutt_${snapshot} && \ + zfs snapshot -r "principal/backupz@avbrutt_${snapshot_date}" && \ echo "ZFS-snapshot OK." || echo "ZFS-snapshot FEILET!" - # TODO: zfs rollback til forrige komplette backup. + # TODO: zfs rollback til forrige komplette backup. echo "Ferdig med oppryddingen." fi # Fang SIGINT, vi vil rydde opp om vi blir avbrutt. -trap "rm $lockfile; echo 'Avbrutt, sletter låsfil...'; exit -2" SIGINT -echo $$ > $lockfile +on_sigint() { + rm "$lockfile" + echo 'Avbrutt, sletter låsfil...' + exit 2 +} +trap on_sigint SIGINT + +echo "$$" > "$lockfile" + + +# === Rydd opp gamle backups hvis det trengs plass. === echo "Sjekker at det er nok ledig plass på disken..." zfs get available principal/backupz -#while [ $(/bin/df /backupz/ | /usr/bin/awk '/^backupz/{print $4}' ) -lt 100000000 ] ; do -while [ $( zfs get -Hp available principal/backupz | cut -f3 ) -lt 50000000000 ] ; do - echo; echo "Disken er nesten full, rydder"; echo + +# Dette tilsvarer 50 GB, juster etter behov. +min_free_space=50000000000 +while [ "$(zfs get -Hp available principal/backupz | cut -f3)" -lt "$min_free_space" ] ; do + echo; + echo "Disken er nesten full, rydder"; + echo + min_backups=5 - if [ $(zfs list -t snapshot | grep backupz@ | wc -l) -lt $min_backups ]; then - echo; echo "Mindre enn $min_backups backups lagret, feiger ut fra sletting"; echo + if [ "$(zfs list -t snapshot | grep -c "backupz@")" -lt "$min_backups" ]; then + echo; + echo "Mindre enn $min_backups backups lagret, feiger ut fra sletting"; + echo break; fi - oldest=$(zfs list -t snapshot | grep backupz@ | head -n1 | tr @ ' ' | awk '{print $2}') - echo "Kjører zfs destroy på alle disker @$oldest" + + oldest_snapshot=$(zfs list -t snapshot | grep backupz@ | head -n1 | tr @ ' ' | awk '{print $2}') + echo "Kjører zfs destroy på alle disker @$oldest_snapshot" for d in $(zfs list | grep ^principal/backupz | awk '{print $1}'); do - zfs destroy "$d@$oldest" + zfs destroy "$d@$oldest_snapshot" done + zfs get available principal/backupz echo done +# === Liste over vertsmaskiner og hva som skal tas backup av. === -echo "Starter backup..." +# TODO: kanskje noe av dette enklere kunne blitt uttrykt +# med en egen json-fil og litt jq-magi? -#export RSYNC_RSH="ssh -c arcfour -v" -export RSYNC_RSH="ssh -v" -rsync="/usr/local/bin/rsync" -rsync_flags="--archive --hard-links --one-file-system --compress --delete --numeric-ids --stats --inplace --relative --exclude=/.zfs/" -logdir="/backupz/log" +# Liste over vertsmaskiner som skal tas backup av. +declare -a hosts=() -# Gjør selve overføringen. -# Start på en liste over PID vi skal vente på. -venteproc="" - -# Ikke kjoer med --checkum. Den er nyttig senere for aa detektere bitraate. -# # Gjør "full"-backup på søndager. -# if [ "x$1" = "xfull" ]; then # Det er søndag -# echo "Det er søndag; tar «full»-backup." -# rsync_flags="${rsync_flags} --checksum" -# fi - -echo "Snapshot ID: $snapshot" -echo -echo +# SSH-vertsnavn hvis det er forskjellig fra vertsnavnet i listen. +declare -A host_ssh_hostname=() +# Backup-katalog hvis den er forskjellig fra vertsnavnet i listen. +declare -A hosts_output_dir=() # ameno -command="${rsync} ${rsync_flags} \ - --log-file=${logdir}/ameno.log.$snapshot \ - --exclude=/var/cache/ \ - --exclude=/var/lib/snapd/ \ - --exclude=/var/log/journal/ \ - ameno:/ \ - ameno:/boot/firmware \ - /backupz/ameno/current/" -$command >${logdir}/ameno.out.$snapshot 2>&1 & -venteproc="$venteproc $!" -echo "Startet $!: $command" +hosts+=("ameno") +ameno_includes=( + "/" + "/boot/firmware" +) +ameno_excludes=( + "/var/cache/" + "/var/lib/snapd/" + "/var/log/journal/" +) -# # knakelibrak (wiki) -# command="${rsync} ${rsync_flags}\ -# wiki:/var/lib/mediawiki /backupz/wiki/" -# $command >${logdir}/wiki.out.$snapshot 2>&1 & -# venteproc="$venteproc $!" -# echo "Startet $!: $command" - -# # knakelibrak (databases) -# command="${rsync} ${rsync_flags}\ -# knakelibrak:/var/backups/databases /backupz/databases/" -# $command >${logdir}/knakelibrak.out.$snapshot 2>&1 & -# venteproc="$venteproc $!" -# echo "Startet $!: $command" - -# # jokum (synapse) -# command="${rsync} ${rsync_flags}\ -# jokum:/data/synapse/ /backupz/jokum/synapse/" -# $command >${logdir}/jokum.out.$snapshot 2>&1 & -# venteproc="$venteproc $!" -# echo "Startet $!: $command" - -# # skrotnisse -# command="${rsync} ${rsync_flags}\ -# skrotnisse:/var/www/ skrotnisse:/srv/ /backupz/skrotnisse/" -# $command >${logdir}/skrotnisse.out.$snapshot 2>&1 & -# venteproc="$venteproc $!" -# echo "Startet $!: $command" - -# # dash8 -# command="${rsync} ${rsync_flags}\ -# dash8:/var/lib/bitlbee/ /backupz/dash8/" -# $command >${logdir}/dash8.out.$snapshot 2>&1 & -# venteproc="$venteproc $!" -# echo "Startet $!: $command" - -# tim døde, tom tok over men har ikke squirrelmail (4.feb 2019) -# tim (web) -#command="${rsync} ${rsync_flags}\ -# tim:/var/lib/squirrelmail /backupz/web/" -#$command >${logdir}/tim.out.$snapshot 2>&1 & -#venteproc="$venteproc $!" -#echo "Startet $!: $command" - -# homepvv -# Maa ha --one-file-system, ellers henger rsync pga. gvfs. -command="${rsync} ${rsync_flags} \ - --log-file=${logdir}/homepvv.log.$snapshot \ - --exclude-from=/backupz/homepvv.exclude \ - homepvv:/ \ - homepvv:/boot \ - homepvv:/export/home/pvv \ - homepvv:/var \ - /backupz/homepvv/" -$command >${logdir}/homepvv.out.$snapshot 2>&1 & -venteproc="$venteproc $!" -echo "Startet $!: $command" +# microbel (hjemmeområder) +hosts+=("homepvv") +homepvv_includes=( + "/" + "/boot" + "/export/home/pvv" + "/var" +) +homepvv_excludes=( + # Se ./homepvv.exclude - den skal bli plukket opp automatisk +) # innovation # Minecraft-verden kopieres fra /var/backups/minecraft/current/ istf. # /srv/minecraft-pvv/. -command="${rsync} ${rsync_flags} \ - --log-file=${logdir}/innovation.log.$snapshot \ - --exclude=/srv/minecraft-pvv/ \ - --exclude=/var/cache/ \ - --exclude=/var/db/freebsd-update/files/ \ - innovation:/ \ - innovation:/boot/efi \ - /backupz/innovation/current/" -$command >${logdir}/innovation.out.$snapshot 2>&1 & -venteproc="$venteproc $!" -echo "Startet $!: $command" - -# # lommel -# command="${rsync} ${rsync_flags} --exclude-from=/backupz/lommel.exclude \ -# lommel:/ /backupz/lommel/" -# $command >${logdir}/lommel.out.$snapshot 2>&1 & -# venteproc="$venteproc $!" -# echo "Startet $!: $command" +hosts+=("innovation") +innovation_includes=( + "/" + "/boot/efi" +) +innovation_excludes=( + "/srv/minecraft-pvv/" + "/var/cache/" + "/var/db/freebsd-update/files/" +) # sleipner -command="${rsync} ${rsync_flags} \ - --log-file=${logdir}/sleipner.log.$snapshot \ - --exclude=/scratch/ \ - --exclude=/var/cache/ \ - sleipner:/ \ - /backupz/sleipner/current/" -$command >${logdir}/sleipner.out.$snapshot 2>&1 & -venteproc="$venteproc $!" -echo "Startet $!: $command" - -# # Spikkjeposche -# command="${rsync} ${rsync_flags} \ -# spikkjeposche:/usr/local/www \ -# spikkjeposche:/usr/local/etc/lighttpd \ -# spikkjeposche:/etc/ssl/private \ -# /backupz/spikkjeposche/" -# $command >${logdir}/spikkjeposche.out.$snapshot 2>&1 & -# venteproc="$venteproc $!" -# echo "Startet $!: $command" +hosts+=("sleipner") +sleipner_includes=( + "/" +) +sleipner_excludes=( + "/scratch/" + "/var/cache/" +) # tom -command="${rsync} ${rsync_flags} \ - --log-file=${logdir}/tom.log.$snapshot \ - --exclude=/var/cache/ \ - tom:/ \ - tom:/boot/efi \ - /backupz/tom/current/" -$command >${logdir}/tom.out.$snapshot 2>&1 & -venteproc="$venteproc $!" -echo "Startet $!: $command" +hosts+=("tom") +tom_includes=( + "/" + "/boot/efi" +) +tom_excludes=( + "/var/cache/" +) +# === Sjekk at alle backup-kataloger har tilhørende datasett, og at datasettene er montert. === +for host in "${hosts[@]}"; do + katalog="/backupz/${hosts_output_dir[$host]:-$host}" + dataset="principal/backupz/${hosts_output_dir[$host]:-$host}" -## Andre ting som skal kopieres her... + # Sjekk at ZFS-datasettet finnes + if ! zfs list "$dataset" >/dev/null 2>&1; then + echo "Feil: ZFS-datasettet '$dataset' finnes ikke. Opprett det med: zfs create $dataset" >&2 + rm "$lockfile" + exit 1 + fi + if [ ! -d "$katalog" ]; then + echo "Feil: katalogen '$katalog' finnes ikke. Opprett den med: mkdir '$katalog'" >&2 + rm "$lockfile" + exit 1 + fi -echo -echo "Rsync er i gang." - -# Vent til rsync er ferdig. -echo "Venter til rsync er ferdig: $venteproc" -for i in $venteproc ; do - wait $i - feilkode=$? - if [ $feilkode -eq 0 ]; then - echo "$i: OK" - else - echo "$i: Rsync returnerte feil (${feilkode})." - fi + # Sjekk at datasettet er montert, og forsøk å mounte om nødvendig + is_mounted="$(zfs get -H -o value mounted "$dataset" 2>/dev/null || echo "no")" + if [ "${is_mounted:-no}" != "yes" ]; then + echo "Datasettet '$dataset' er ikke montert på '$katalog'. Forsøker å mounte..." + if ! zfs mount "$dataset" >/dev/null 2>&1; then + echo "Kunne ikke mounte '$dataset'. Avbryter." >&2 + rm "$lockfile" + exit 1 + fi + fi done -echo + +# === Start rsync-jobber === + +echo "Starter backup..." +echo "Snapshot ID: $snapshot_date" +echo + +start_time="$(date +%s)" +rsync="/usr/local/bin/rsync" +rsync_flags=( + --archive + --hard-links + --compress + --delete + --numeric-ids + --one-file-system + --relative + --rsh="ssh -v" + --stats + --inplace + --exclude=/.zfs/ +) +logdir="/backupz/log" +declare -A venteproc=() + +echo "Starter rsync for følgende verter: ${hosts[*]}" +echo +for host in "${hosts[@]}"; do + command=( + "$rsync" + "${rsync_flags[@]}" + "--log-file=${logdir}/${host}.log.$snapshot_date" + ) + + extra_array_name="${host}_extra_rsync_flags" + if declare -p "$extra_array_name" >/dev/null 2>&1; then + extra_rsync_flags_var="${host}_extra_rsync_flags[@]" + extra_rsync_flags=( "${!extra_rsync_flags_var}" ) + for ef in "${extra_rsync_flags[@]}"; do + command+=( "$ef" ) + done + fi + + exclude_array_name="${host}_excludes" + if declare -p "$exclude_array_name" >/dev/null 2>&1; then + exclude_paths_var="${host}_excludes[@]" + exclude_paths=( "${!exclude_paths_var}" ) + for exclude in "${exclude_paths[@]}"; do + command+=( "--exclude=${exclude}" ) + done + fi + + if [ -f "/backupz/${host}.exclude" ]; then + command+=("--exclude-from=/backupz/${host}.exclude") + fi + + include_array_name="${host}_includes" + if declare -p "$include_array_name" >/dev/null 2>&1; then + include_paths_var="${host}_includes[@]" + include_paths=( "${!include_paths_var}" ) + for include in "${include_paths[@]}"; do + command+=("${host_ssh_hostname[$host]:-$host}:$include") + done + else + echo "Ingen inkluderingsstier for vert $host, hopper over." + continue + fi + + command+=("/backupz/${hosts_output_dir[$host]:-$host}/") + + echo "Starter backup for vert: $host" + "${command[@]}" >"${logdir}/${host}.out.$snapshot_date" 2>&1 & + venteproc[$!]="$host" + echo "Startet $!:" + echo "${command[0]} \\" + # Ikke print de første rsync-flagga i kommandoen + echo " # ...standard rsync-flagg... \\" + start_index="$((1 + ${#rsync_flags[@]}))" + for ((i="$start_index"; i<${#command[@]}-1; i++)); do + echo " ${command[i]} \\" + done + echo " ${command[-1]}" + echo +done +echo "Rsync er i gang." + + +# === Vent på at alle rsync-jobbene skal bli ferdige === + +echo "Venter til rsync er ferdig: ${!venteproc[*]}" +echo +while true; do + wait -n -p pid; code=$? + [[ -z "${pid}" ]] && break + + if [ $code -eq 0 ]; then + echo "${pid} (${venteproc["$pid"]:-???}): OK" + else + echo "${pid} (${venteproc["$pid"]:-???}): Rsync returnerte feil (${code})" + echo "Se loggfilene her for mer informasjon:" + echo "- ${logdir}/${venteproc["$pid"]}.log.$snapshot_date" + echo "- ${logdir}/${venteproc["$pid"]}.out.$snapshot_date" + fi + + unset 'venteproc["$pid"]' + + echo "${#venteproc[@]} jobber gjenstår..." + echo +done echo "Rsync er ferdig." -# Behoeves ikke da principal ikke har almen brukerinnlogging. -# # Gjør hjemmemappene lesbare bare for brukeren. -# find /backupz/homepvv/export/home/pvv -maxdepth 2 -mindepth 2 -exec chmod 700 {} \; + +# === Oppsummering, ta ZFS-snapshot og avslutt === + +end_time="$(date +%s)" +elapsed_time="$((end_time - start_time))" +printf 'Tid brukt på rsync: %03d:%02d:%02d\n' $((elapsed_time/3600)) $(((elapsed_time/60)%60)) $((elapsed_time%60)) # Touch home slik at timestamp på snapshot blir når backup var ferdig. touch /backupz/homepvv/export/home -# Ta et zfs snapshot echo "Tar ZFS-snapshot..." - -zfs snapshot -r principal/backupz@${snapshot} && \ +zfs snapshot -r "principal/backupz@${snapshot_date}" && \ echo "ZFS-snapshot ferdig." || echo "ZFS-snapshot FEILET!" echo -echo Ledig plass: $(zfs list -H -o avail principal/backupz) +echo Ledig plass: "$(zfs list -H -o avail principal/backupz)" echo # TODO: Slett enkelte gamle snapshots? echo "Backup ferdig: $(date)" -rm $lockfile +rm "$lockfile" zpool status -xv principal