Compare commits
67 Commits
70145d5897
...
1-get-base
Author | SHA1 | Date | |
---|---|---|---|
9ce4de84d7 | |||
83e11af519 | |||
136b1aa99a | |||
01180844e7 | |||
d606ae9688 | |||
2246823d06 | |||
3516f56580 | |||
525600dde4 | |||
650a91f2cc | |||
917a71ced4 | |||
ff963aa844 | |||
c23567e2b1 | |||
562ffbdc0f | |||
204a2a49b1 | |||
d027966ed3 | |||
f16f2b8612 | |||
93380891ec | |||
8de0e25ab8 | |||
4069c33145 | |||
2fbbf4da19 | |||
47885efbab | |||
e3f6316c47 | |||
7681bbde84 | |||
961649d252 | |||
360c726326 | |||
7d573627f5 | |||
818082a0b3 | |||
04caca48a5 | |||
7efb2e5821 | |||
c686f7645f | |||
31f5655ff0 | |||
3560b52f4c | |||
c210adc485 | |||
f654c9d5f7 | |||
a32a4df6a6 | |||
45df9755f1 | |||
8cfedf6e11 | |||
b8b3ad550d | |||
d9b1ae5905 | |||
ba8b561e80 | |||
70284671ed | |||
bba8160d84 | |||
0b9c00c26b | |||
d3b3e72fe0 | |||
42cb32ea83 | |||
c6bd627b4a | |||
df8764c739 | |||
47bb8cbd30 | |||
9e8772fad5 | |||
2e0bb42372 | |||
12283e49c2 | |||
c899d26134 | |||
7fc828fb14 | |||
7a494cb65a | |||
2fadc427c5 | |||
ce93c8558e | |||
4553bee760 | |||
39490c3d7a | |||
776480a9a3 | |||
c9fcdb5b29 | |||
3277e7a31a | |||
9a92e99c6a | |||
ed15631ad2 | |||
85a7fb66be | |||
41c30d5971 | |||
9437dea225 | |||
35f572fe97 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.idea
|
170
README.md
170
README.md
@@ -15,12 +15,12 @@ Get started like so:
|
|||||||
1. Symlink to files, for example
|
1. Symlink to files, for example
|
||||||
```
|
```
|
||||||
sudo ln -s <repo>/pacman-zfs-snapshot.sh /usr/local/bin/pacman-zfs-snapshot
|
sudo ln -s <repo>/pacman-zfs-snapshot.sh /usr/local/bin/pacman-zfs-snapshot
|
||||||
sudo ln -s <repo>/pacman-zfs-snapshot-install.hook /usr/share/libalpm/hooks/pacman-zfs-snapshot-install.hook
|
sudo ln -s <repo>/pacman-zfs-snapshot-install.hook /usr/share/libalpm/hooks/00-pacman-zfs-snapshot-install.hook
|
||||||
sudo ln -s <repo>/pacman-zfs-snapshot-remove.hook /usr/share/libalpm/hooks/pacman-zfs-snapshot-remove.hook
|
sudo ln -s <repo>/pacman-zfs-snapshot-remove.hook /usr/share/libalpm/hooks/00-pacman-zfs-snapshot-remove.hook
|
||||||
sudo ln -s <repo>/pacman-zfs-snapshot-upgrade.hook /usr/share/libalpm/hooks/pacman-zfs-snapshot-upgrade.hook
|
sudo ln -s <repo>/pacman-zfs-snapshot-upgrade.hook /usr/share/libalpm/hooks/00-pacman-zfs-snapshot-upgrade.hook
|
||||||
sudo ln -s <repo>/pacman-zfs-snapshot.conf /etc/pacman-zfs-snapshot.conf
|
sudo ln -s <repo>/pacman-zfs-snapshot.conf /etc/pacman-zfs-snapshot.conf
|
||||||
```
|
```
|
||||||
Note that while you may choose arbitrary locations for symlinks the `pacman-zfs-snapshot.hook` file references `/usr/local/bin/pacman-zfs-snapshot`. Change that accordingly if you need to.
|
Note that while you may choose arbitrary locations for symlinks the `00-pacman-zfs-snapshot-*.hook` files reference `/usr/local/bin/pacman-zfs-snapshot`. Change that accordingly if you need to.
|
||||||
1. For datasets you want auto-snapshotted add property `space.quico:auto-snapshot=true`
|
1. For datasets you want auto-snapshotted add property `space.quico:auto-snapshot=true`
|
||||||
```
|
```
|
||||||
zfs set space.quico:auto-snapshot=true zpool/root/archlinux
|
zfs set space.quico:auto-snapshot=true zpool/root/archlinux
|
||||||
@@ -30,13 +30,167 @@ Get started like so:
|
|||||||
|
|
||||||
# What's it do?
|
# What's it do?
|
||||||
|
|
||||||
In `pacman` on every `PreTransaction`, meaning right before any actual operation on a package begins, we trigger a ZFS snapshot. By default we identify the active system dataset by doing `findmnt / --noheadings --output source`. If exactly one source returns that is the exact name of a ZFS dataset in an imported zpool we create a snapshot on it. If no source returns we silently exit. If more than one source returns we raise an error and halt the `pacman` transaction.
|
In `pacman` on every `PreTransaction`, meaning right before any actual operation on a package begins, we trigger a ZFS snapshot. This happens via a so-called hook which is a plain text config file. Hook files make use of the Arch Linux Package Management (ALPM) library, also known as `libalpm` for which `pacman` is a frontend. By default hooks are stored in `/usr/share/libalpm/hooks`. Additionally `/etc/pacman.conf` has a directory configured as:
|
||||||
|
```
|
||||||
|
#HookDir = /etc/pacman.d/hooks/
|
||||||
|
```
|
||||||
|
Hook files from both directories are collectively parsed and executed in lexicographical order. Hook names from _this_ repo begin with `00-*` so on a default Arch Linux they are the first to be executed during `pacman` transactions.
|
||||||
|
|
||||||
We retain two different snapshot chains, one for `pacman` transactions that only affect what we are calling _trivial_ packages and a separate chain for _important_ packages. By default only the exact regular expression package name match `^linux$` is considered important. Whenever an important package is affected by a transaction a snapshot goes into the corresponding chain. In all other cases - when an important package is not affected - snapshots go into the trivial chain.
|
For ZFS snapshots intended to save your bacon the `00-*` naming convention is particularly critical. In `/usr/share/libalpm/hooks` you can see for example that when a kernel upgrade happens `60-mkinitcpio-remove.hook` is executed (deleting your existing `vmlinuz-*` kernel image for example at `/boot/vmlinuz-linux`). After that if you're using the `zfs-dkms` package which itself requires `dkms` which in turn installs `71-dkms-remove.hook` this hook removes your ZFS kernel module files. Both the `60-*` and optionally the `71-*` hook (for `zfs-dkms` users) run early due to their naming. If we don't create a snapshot before these hooks run we end up creating a snapshot without kernel image and without ZFS kernel module files. Our `00-*` hook files are executed early enough ensuring that a snapshot can safely return you to a working system.
|
||||||
|
|
||||||
The _trivial_ snapshot chain by default keeps 15 snapshots, the _important_ chain keeps 5. The thought process here is that you will likely not futz around with a kernel every day whereas you may very well install arbitrary packages multiple times a day. Snapshots should keep you safe for a couple of days hence the defaults of 5 and 15 snapshots, respectively.
|
## Snapshot selection
|
||||||
|
|
||||||
You may optionally include more ZFS datasets in this snapshot mechanism. Have a look at `pacman-zfs-snapshot.conf`, its comments should be clear enough to get you going.
|
We snapshot datasets that have the `space.quico:auto-snapshot` property set to `true`. By default we further limit datasets to only those that are currently mounted in your active operating system. We identify these by asking `findmnt` for a list of mounted file systems of `fstype=="zfs"` which for example returns:
|
||||||
|
```
|
||||||
|
# findmnt --json --list --output 'fstype,source,target' | \
|
||||||
|
jq --raw-output '.[][] | select(.fstype=="zfs") | .source'
|
||||||
|
|
||||||
|
zpool/root/archlinux
|
||||||
|
```
|
||||||
|
If no dataset (or no _local_ dataset) has the property set correctly no snapshots are done. The script will print an info-level message about that on `pacman` transactions.
|
||||||
|
|
||||||
|
## Snapshot chains
|
||||||
|
|
||||||
|
We retain two different snapshot chains, one for `pacman` transactions that only affect what we are calling _trivial_ packages and a separate chain for _important_ packages. By default only the exact regular expression package name match `^(linux(-zen)?(-headers)?|systemd|zfs-(linux(-zen)?|dkms|utils))$` is considered important so in plain English any one of:
|
||||||
|
|
||||||
|
- `linux`
|
||||||
|
- `linux-headers`
|
||||||
|
- `linux-zen`
|
||||||
|
- `linux-zen-headers`
|
||||||
|
- `systemd`
|
||||||
|
- `zfs-linux`
|
||||||
|
- `zfs-linux-zen`
|
||||||
|
- `zfs-dkms`
|
||||||
|
- `zfs-utils`
|
||||||
|
|
||||||
|
Whenever an important package is affected by a transaction a snapshot goes into the corresponding chain. In all other cases - when an important package is not affected - snapshots go into the trivial chain.
|
||||||
|
|
||||||
|
The _trivial_ snapshot chain by default keeps 25 snapshots, the _important_ chain keeps 10. The thought process here is that you will likely not futz around with a kernel every day whereas you may very well install arbitrary packages multiple times a day. Snapshots should keep you safe for a low number of weeks up to maybe a full month on an average daily driver system hence the defaults of 10 and 25 snapshots, respectively.
|
||||||
|
|
||||||
|
## Dataset naming and uniqueness
|
||||||
|
|
||||||
|
Snapshots may look like so:
|
||||||
|
```
|
||||||
|
$ zfs list -o name -t all
|
||||||
|
NAME snap_date_format='%F-%H%M' ┌─── Important because systemd
|
||||||
|
zpool | | is on our list of
|
||||||
|
zpool/root ▼ ┌ Counter | important packages
|
||||||
|
zpool/root/archlinux ┌─────────────┐ ▼ ▼▼▼
|
||||||
|
zpool/root/archlinux@pacman_2023-03-07-0113_1_op:upgr_sev:imp_pkgs:systemd:bind:enchant:grep
|
||||||
|
zpool/root/archlinux@pacman_2023-03-07-0113_1_op:upgr_sev:trv_pkgs:jdk17-temurin
|
||||||
|
zpool/root/archlinux@pacman_2023-03-07-0114_1_op:inst_sev:trv_pkgs:docker-credential-secretser...
|
||||||
|
zpool/root/archlinux@pacman_2023-03-07-0115_1_op:upgr_sev:trv_pkgs:proton-ge-custom-bin
|
||||||
|
▲▲▲▲ ▲▲▲ └────────────────────────────┘
|
||||||
|
| | Max. 30 characters per our
|
||||||
|
Pacman operation that triggered this snapshot ───┘ | pacman-zfs-snapshot.conf
|
||||||
|
| setting 'pkgs_list_max_length'
|
||||||
|
Severity based on affected packages, here trivial ───────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice how in this case the _counter_ is `1` for all four snapshots. The counter is used as the distinguishing factor for snapshots that are otherwise identical. This avoids naming collisions by incrementing it as needed. In day-to-day operations you will typically see it at `1` as there rarely is a need to avoid collisions unless you purposely limit the timestamp length and/or package list length to the point that successive snapshots may appear identical. See [Avoiding naming collisions](#avoiding-naming-collisions) for more details.
|
||||||
|
|
||||||
|
Notice also how snapshot line 3 ends in `docker-credential-secretser...`. This snapshot was triggered on installation of the Arch User Repository package [docker-credential-secretservice-bin](https://aur.archlinux.org/packages/docker-credential-secretservice-bin) whose package name is 35 characters long. In this example our `pkgs_list_max_length` setting limits maximum name of the packages string to `30` characters. If we can't naturally fit package names into this limit by removing packages from the list we instead cut off part of the package name and add an ellipsis (three dots `...`). The default setting is `pkgs_list_max_length='30'`. In case the user wants three characters or fewer thus making an ellipsis impractical we simply trim the package name to that many characters:
|
||||||
|
```
|
||||||
|
pkgs_list_max_length='7': dock...
|
||||||
|
pkgs_list_max_length='6': doc...
|
||||||
|
pkgs_list_max_length='5': do...
|
||||||
|
pkgs_list_max_length='4': d...
|
||||||
|
pkgs_list_max_length='3': doc
|
||||||
|
pkgs_list_max_length='2': do
|
||||||
|
pkgs_list_max_length='1': d
|
||||||
|
```
|
||||||
|
With a package list allowance of 0 characters the entire `pkgs` field is removed. Above example will then look like so:
|
||||||
|
```
|
||||||
|
$ zfs list -o name -t all
|
||||||
|
NAME
|
||||||
|
zpool
|
||||||
|
zpool/root
|
||||||
|
zpool/root/archlinux
|
||||||
|
zpool/root/archlinux@pacman_2023-03-07-0113_1_op:upgr_sev:imp
|
||||||
|
zpool/root/archlinux@pacman_2023-03-07-0113_1_op:upgr_sev:trv
|
||||||
|
zpool/root/archlinux@pacman_2023-03-07-0114_1_op:inst_sev:trv
|
||||||
|
zpool/root/archlinux@pacman_2023-03-07-0115_1_op:upgr_sev:trv
|
||||||
|
```
|
||||||
|
|
||||||
|
Whatever you set as your `pkgs_list_max_length` is still just a best effort as it is subject to ZFS' internal maximum for dataset name length. This limit is currently 255 characters. For a snapshot the dataset name in front of the `@` character plus everything else starting with the `@` character til the end count against the limit. If you'd like e.g. 200 characters allocated to the package list chances are that you'll see fewer characters than that depending on how long your dataset names are on their own.
|
||||||
|
|
||||||
|
## Special characters in package names
|
||||||
|
|
||||||
|
Arch Linux has no qualms with at (`@`) characters and plus (`+`) characters in package names but ZFS very much does take issue with those. Just a heads-up, when constructing a ZFS snapshot name we replace all `@` characters in package names with one dot each (`.`) and we replace all `+` characters with one underscore each (`_`).
|
||||||
|
|
||||||
|
A snapshot name that would appear like so:
|
||||||
|
```
|
||||||
|
$ zfs list -o name -t all
|
||||||
|
NAME
|
||||||
|
zpool
|
||||||
|
zpool/root
|
||||||
|
zpool/root/archlinux
|
||||||
|
zpool/root/archlinux@pacman_2023-03-07-0113_1_op:upgr_sev:trv_pkgs:jdk17-temurin:libc++
|
||||||
|
~~~~~~
|
||||||
|
```
|
||||||
|
We'll create like so instead:
|
||||||
|
```
|
||||||
|
$ zfs list -o name -t all
|
||||||
|
NAME
|
||||||
|
zpool
|
||||||
|
zpool/root
|
||||||
|
zpool/root/archlinux
|
||||||
|
zpool/root/archlinux@pacman_2023-03-07-0113_1_op:upgr_sev:trv_pkgs:jdk17-temurin:libc__
|
||||||
|
~~~~~~
|
||||||
|
```
|
||||||
|
|
||||||
|
Have a look at `pacman-zfs-snapshot.conf` as well, its comments should be clear enough to get you going.
|
||||||
|
|
||||||
|
# Avoiding naming collisions
|
||||||
|
|
||||||
|
By default snapshot names contain a timestamp formatted like so: `2023-03-07-0114`. This makes snapshot names reasonably unique. You can change both the timestamp format and timezone in `pacman-zfs-snapshot.conf` where the format defaults to:
|
||||||
|
```
|
||||||
|
snap_date_format='%F-%H%M'
|
||||||
|
```
|
||||||
|
And the timezone defaults to:
|
||||||
|
```
|
||||||
|
snap_timezone='Etc/UTC'
|
||||||
|
```
|
||||||
|
|
||||||
|
With these settings it is possible to cause ZFS snapshot name collisions (meaning reuse of the exact same snapshot name) when all of the following conditions are true for any two `pacman` operations:
|
||||||
|
- They occur within the same minute
|
||||||
|
- They cover the same type of operation (_Install_, _Remove_ or _Upgrade_)
|
||||||
|
- They cover the same list of packages
|
||||||
|
|
||||||
|
The script safeguards against naming collisions by adding a monotonically incrementing counter after the timestamp string.
|
||||||
|
|
||||||
|
For example by running `pacman -S tmux` three times within the same minute (once for an _Install_ operation and two more times for two identical _Upgrade_ operations) your system may generate the following example snapshots:
|
||||||
|
```
|
||||||
|
zpool/root/archlinux@pacman_2023-03-07-0116_1_op:inst_sev:trv_pkgs:tmux
|
||||||
|
zpool/root/archlinux@pacman_2023-03-07-0116_1_op:upgr_sev:trv_pkgs:tmux
|
||||||
|
zpool/root/archlinux@pacman_2023-03-07-0116_2_op:upgr_sev:trv_pkgs:tmux
|
||||||
|
~~~
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that lines 2 and 3 would collide since their dataset names are virtually identical other than the counter suffix which was incremented by 1 to avoid a collision.
|
||||||
|
|
||||||
|
This facilitates a hands-off approach to using this script on a daily driver system without risking missing snapshots or employing other more involved approaches to avoid naming collisions.
|
||||||
|
|
||||||
|
# Rollback
|
||||||
|
|
||||||
|
After a rollback for example via the excellent [ZFSBootMenu](https://docs.zfsbootmenu.org/) `pacman` and all AUR helpers you may be using will consider the `pacman` database to be locked. No `pacman` transactions can start, you will for example see:
|
||||||
|
|
||||||
|
- In `pacman`
|
||||||
|
```
|
||||||
|
# pacman -Syu
|
||||||
|
:: Synchronizing package databases...
|
||||||
|
error: failed to synchronize all databases (unable to lock database)
|
||||||
|
```
|
||||||
|
- In `paru`
|
||||||
|
```
|
||||||
|
$ paru
|
||||||
|
:: Pacman is currently in use, please wait...
|
||||||
|
```
|
||||||
|
|
||||||
|
The moment a snapshot was created `pacman` was already in a transaction so it had already written its lock file to `/var/lib/pacman/db.lck`. After a clean finish `pacman` would have deleted that lock itself but since you rolled back to a point mid-transaction it's still there. Just delete the file and you're good to go:
|
||||||
|
```
|
||||||
|
sudo rm /var/lib/pacman/db.lck
|
||||||
|
```
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ Type = Package
|
|||||||
Target = *
|
Target = *
|
||||||
|
|
||||||
[Action]
|
[Action]
|
||||||
Description = Create ZFS snapshot on active system dataset
|
Description = Create ZFS snapshot(s)
|
||||||
When = PreTransaction
|
When = PreTransaction
|
||||||
Exec = /bin/sh -c 'while read -r f; do echo "$f"; done | /usr/local/bin/pacman-zfs-snapshot install'
|
Exec = /bin/sh -c 'while read -r f; do echo "$f"; done | /usr/local/bin/pacman-zfs-snapshot install'
|
||||||
Depends = jq
|
Depends = jq
|
||||||
|
@@ -4,7 +4,7 @@ Type = Package
|
|||||||
Target = *
|
Target = *
|
||||||
|
|
||||||
[Action]
|
[Action]
|
||||||
Description = Create ZFS snapshot on active system dataset
|
Description = Create ZFS snapshot(s)
|
||||||
When = PreTransaction
|
When = PreTransaction
|
||||||
Exec = /bin/sh -c 'while read -r f; do echo "$f"; done | /usr/local/bin/pacman-zfs-snapshot remove'
|
Exec = /bin/sh -c 'while read -r f; do echo "$f"; done | /usr/local/bin/pacman-zfs-snapshot remove'
|
||||||
Depends = jq
|
Depends = jq
|
||||||
|
@@ -4,7 +4,7 @@ Type = Package
|
|||||||
Target = *
|
Target = *
|
||||||
|
|
||||||
[Action]
|
[Action]
|
||||||
Description = Create ZFS snapshot on active system dataset
|
Description = Create ZFS snapshot(s)
|
||||||
When = PreTransaction
|
When = PreTransaction
|
||||||
Exec = /bin/sh -c 'while read -r f; do echo "$f"; done | /usr/local/bin/pacman-zfs-snapshot upgrade'
|
Exec = /bin/sh -c 'while read -r f; do echo "$f"; done | /usr/local/bin/pacman-zfs-snapshot upgrade'
|
||||||
Depends = jq
|
Depends = jq
|
||||||
|
@@ -1,20 +1,28 @@
|
|||||||
do_dry_run='true'
|
# Set to 'true' to do nothing and just print messages during pacman
|
||||||
|
# operations. Helpful to get a feel for what these hooks do. This defaults
|
||||||
|
# to 'false' so if you set this to an empty string or remove or uncomment it
|
||||||
|
# in this conf file it'll equal 'false'.
|
||||||
|
do_dry_run='false'
|
||||||
|
|
||||||
# Pipe-separated list of kernel names. Will be matched against regular
|
# Pipe-separated list of package names we consider important. Will be
|
||||||
# expression ^this_var_here$. Snapshots taken before a pacman transaction on
|
# matched against regular expression ^(this_var_here)$. Snapshots taken
|
||||||
# an important package have a separate retention from snapshots for trivial
|
# before a pacman transaction on an important package have a separate
|
||||||
# packages. Lends itself to keeping kernel updates separate from everything
|
# retention from snapshots for trivial packages. Lends itself to keeping
|
||||||
# else.
|
# high-risk updates separate from everything else.
|
||||||
important_names='tmux'
|
important_names='linux(-zen)?(-headers)?|systemd|zfs-(linux(-zen)?|dkms|utils)'
|
||||||
# zfs pkgs, systemd?
|
|
||||||
|
|
||||||
snaps_trivial_keep='15'
|
# Number snapshots to keep
|
||||||
snaps_important_keep='5'
|
snaps_trivial_keep='25'
|
||||||
|
snaps_important_keep='10'
|
||||||
|
|
||||||
|
# Which suffix to use in snapshot names to identify snapshots before a
|
||||||
|
# trivial pacman operation and before important pacman operations.
|
||||||
snaps_trivial_suffix='trv'
|
snaps_trivial_suffix='trv'
|
||||||
snaps_important_suffix='imp'
|
snaps_important_suffix='imp'
|
||||||
|
|
||||||
pkgs_list_max_length='24'
|
# Snapshot name will contain list of affected packages trimmed to this many
|
||||||
|
# max characters.
|
||||||
|
pkgs_list_max_length='30'
|
||||||
|
|
||||||
# Hook will by default snapshot all datasets that have the property
|
# Hook will by default snapshot all datasets that have the property
|
||||||
# 'space.quico:auto-snapshot=true' set, even the ones that are not currently
|
# 'space.quico:auto-snapshot=true' set, even the ones that are not currently
|
||||||
@@ -26,10 +34,24 @@ pkgs_list_max_length='24'
|
|||||||
# jq --raw-output '.[][] | select(.fstype=="zfs") | .source'
|
# jq --raw-output '.[][] | select(.fstype=="zfs") | .source'
|
||||||
snap_only_local_datasets='true'
|
snap_only_local_datasets='true'
|
||||||
|
|
||||||
|
# Which characters do we want to use to separate snapshot name fields
|
||||||
snap_field_separator='_'
|
snap_field_separator='_'
|
||||||
snap_name_prefix='pac'
|
# Prefix all our snapshots with this string to keep them separate from
|
||||||
snap_date_format='+F-%H%M'
|
# snapshots done by any other means
|
||||||
|
snap_name_prefix='pacman'
|
||||||
|
# We do "$(date +<whatever>)" to put a timestamp into snapshot names.
|
||||||
|
# Defaults to "$(date +'%F-%H%M')" which returns '2023-03-07-0050'.
|
||||||
|
snap_date_format='%F-%H%M'
|
||||||
|
# The tzdata-formatted timezone name used to add timestamps to snapshot
|
||||||
|
# names. Check for example 'timedatectl list-timezones' to get a list of
|
||||||
|
# valid names on your system. Format looks like 'America/Fortaleza',
|
||||||
|
# 'Asia/Magadan' or 'Australia/Sydney'. Defaults to 'Etc/UTC'. Can also be
|
||||||
|
# the empty string (as in snap_timezone='') in which case we'll use your
|
||||||
|
# system's timezone setting.
|
||||||
|
snap_timezone='Etc/UTC'
|
||||||
|
|
||||||
|
# Which strings do we want to diffferentiate pacman operations Install,
|
||||||
|
# Remove, Upgrade
|
||||||
snap_op_installation_suffix='inst'
|
snap_op_installation_suffix='inst'
|
||||||
snap_op_remove_suffix='rmvl'
|
snap_op_remove_suffix='rmvl'
|
||||||
snap_op_upgrade_suffix='upgr'
|
snap_op_upgrade_suffix='upgr'
|
||||||
|
@@ -5,9 +5,6 @@ while read pkg; do
|
|||||||
pkgs+=("${pkg}")
|
pkgs+=("${pkg}")
|
||||||
done
|
done
|
||||||
|
|
||||||
declare operation
|
|
||||||
operation="${1}"
|
|
||||||
|
|
||||||
declare conf_file
|
declare conf_file
|
||||||
conf_file='/etc/pacman-zfs-snapshot.conf'
|
conf_file='/etc/pacman-zfs-snapshot.conf'
|
||||||
|
|
||||||
@@ -16,20 +13,45 @@ if [[ -r "${conf_file}" ]]; then
|
|||||||
source "${conf_file}"
|
source "${conf_file}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! "${do_dry_run}" ]]; then do_dry_run='true'; fi
|
# User-defined
|
||||||
if [[ ! "${important_names}" ]]; then important_names='linux'; fi
|
do_dry_run="${do_dry_run:-false}"
|
||||||
if [[ ! "${snaps_trivial_keep}" ]]; then snaps_trivial_keep='15'; fi
|
important_names="${important_names:-linux|systemd|zfs-(dkms|utils)}"
|
||||||
if [[ ! "${snaps_important_keep}" ]]; then snaps_important_keep='5'; fi
|
snaps_trivial_keep="${snaps_trivial_keep:-15}"
|
||||||
if [[ ! "${snaps_trivial_suffix}" ]]; then snaps_trivial_suffix='trv'; fi
|
snaps_important_keep="${snaps_important_keep:-5}"
|
||||||
if [[ ! "${snaps_important_suffix}" ]]; then snaps_important_suffix='imp'; fi
|
snaps_trivial_suffix="${snaps_trivial_suffix:-trv}"
|
||||||
if [[ ! "${pkgs_list_max_length}" ]]; then pkgs_list_max_length='24'; fi
|
snaps_important_suffix="${snaps_important_suffix:-imp}"
|
||||||
if [[ ! "${snap_only_local_datasets}" ]]; then snap_only_local_datasets='true'; fi
|
pkgs_list_max_length="${pkgs_list_max_length:-30}"
|
||||||
if [[ ! "${snap_field_separator}" ]]; then snap_field_separator='_'; fi
|
snap_only_local_datasets="${snap_only_local_datasets:-true}"
|
||||||
if [[ ! "${snap_name_prefix}" ]]; then snap_name_prefix='pac'; fi
|
snap_field_separator="${snap_field_separator:-_}"
|
||||||
if [[ ! "${snap_date_format}" ]]; then snap_date_format='+F-%H%M'; fi
|
snap_name_prefix="${snap_name_prefix:-pacman}"
|
||||||
if [[ ! "${snap_op_installation_suffix}" ]]; then snap_op_installation_suffix='inst'; fi
|
snap_date_format="${snap_date_format:-%F-%H%M}"
|
||||||
if [[ ! "${snap_op_remove_suffix}" ]]; then snap_op_remove_suffix='rmvl'; fi
|
snap_timezone="${snap_timezone:-Etc/UTC}"
|
||||||
if [[ ! "${snap_op_upgrade_suffix}" ]]; then snap_op_upgrade_suffix='upgr'; fi
|
snap_op_installation_suffix="${snap_op_installation_suffix:-inst}"
|
||||||
|
snap_op_remove_suffix="${snap_op_remove_suffix:-rmvl}"
|
||||||
|
snap_op_upgrade_suffix="${snap_op_upgrade_suffix:-upgr}"
|
||||||
|
|
||||||
|
# Internal
|
||||||
|
declare zfs_prop pkg_separator max_zfs_snapshot_name_length color_reset color_lyellow color_red
|
||||||
|
zfs_prop='space.quico:auto-snapshot'
|
||||||
|
pkg_separator=':'
|
||||||
|
max_zfs_snapshot_name_length='255'
|
||||||
|
color_reset='\e[0m'
|
||||||
|
color_lyellow='\e[93m'
|
||||||
|
color_red='\e[31m'
|
||||||
|
|
||||||
|
declare operation conf_op_suffix
|
||||||
|
operation="${1}"
|
||||||
|
case "${operation}" in
|
||||||
|
install)
|
||||||
|
conf_op_suffix="${snap_op_installation_suffix}"
|
||||||
|
;;
|
||||||
|
remove)
|
||||||
|
conf_op_suffix="${snap_op_remove_suffix}"
|
||||||
|
;;
|
||||||
|
upgrade)
|
||||||
|
conf_op_suffix="${snap_op_upgrade_suffix}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
function pprint () {
|
function pprint () {
|
||||||
local style msg exit_code
|
local style msg exit_code
|
||||||
@@ -37,17 +59,12 @@ function pprint () {
|
|||||||
msg="${2:?}"
|
msg="${2:?}"
|
||||||
exit_code="${3}"
|
exit_code="${3}"
|
||||||
|
|
||||||
local color_reset color_lyellow
|
|
||||||
color_reset='\e[0m'
|
|
||||||
color_lyellow='\e[93m'
|
|
||||||
color_red='\e[31m'
|
|
||||||
|
|
||||||
case "${style}" in
|
case "${style}" in
|
||||||
warn)
|
warn)
|
||||||
printf -- "${color_lyellow}"'[WARN]'"${color_reset}"' %s\n' "${msg}"
|
printf -- "${color_lyellow}"'[WARN]'"${color_reset}"' %s\n' "${msg}"
|
||||||
;;
|
;;
|
||||||
err)
|
err)
|
||||||
printf -- "${color_red}"'[ERR]'"${color_reset}"' %s\n' "${msg}"
|
printf -- "${color_red}"'[ERR] '"${color_reset}"' %s\n' "${msg}"
|
||||||
;;
|
;;
|
||||||
info)
|
info)
|
||||||
printf -- '[INFO] %s\n' "${msg}"
|
printf -- '[INFO] %s\n' "${msg}"
|
||||||
@@ -61,7 +78,7 @@ function split_pkgs_by_importance () {
|
|||||||
local pkgs_in_transaction
|
local pkgs_in_transaction
|
||||||
pkgs_in_transaction=("${@}")
|
pkgs_in_transaction=("${@}")
|
||||||
for pkg in "${pkgs_in_transaction[@]}"; do
|
for pkg in "${pkgs_in_transaction[@]}"; do
|
||||||
if grep -Piq -- '^'"${important_names}"'$' <<<"${pkg}"; then
|
if grep -Piq -- '^('"${important_names}"')$' <<<"${pkg}"; then
|
||||||
important_pkgs_in_transaction+=("${pkg}")
|
important_pkgs_in_transaction+=("${pkg}")
|
||||||
else
|
else
|
||||||
trivial_pkgs_in_transaction+=("${pkg}")
|
trivial_pkgs_in_transaction+=("${pkg}")
|
||||||
@@ -79,25 +96,25 @@ function set_severity () {
|
|||||||
|
|
||||||
function get_globally_snappable_datasets () {
|
function get_globally_snappable_datasets () {
|
||||||
local datasets_list
|
local datasets_list
|
||||||
# For all datasets show their 'space.quico:auto-snapshot' property; only
|
# For all datasets show their "${zfs_prop}" property; only print dataset
|
||||||
# print dataset name in column 1 and property value in column 2. In awk
|
# name in column 1 and property value in column 2. In awk limit this
|
||||||
# limit this list to datasets where tab-delimited column 2 has exact
|
# list to datasets where tab-delimited column 2 has exact string
|
||||||
# string '^true$' then further limit output by eliminating snapshots
|
# '^true$' then further limit output by eliminating snapshots from list,
|
||||||
# from list, i.e. dataset names that contain an '@' character.
|
# i.e. dataset names that contain an '@' character.
|
||||||
datasets_list="$(zfs get -H -o 'name,value' 'space.quico:auto-snapshot' | \
|
datasets_list="$(zfs get -H -o 'name,value' "${zfs_prop}" | \
|
||||||
awk -F'\t' '{if($2 ~ /^true$/ && $1 !~ /@/) print $1}')"
|
awk -F'\t' '{if($2 ~ /^true$/ && $1 !~ /@/) print $1}')"
|
||||||
while IFS= read -r dataset; do
|
while IFS= read -u10 -r dataset; do
|
||||||
globally_snappable_datasets+=("${dataset}")
|
globally_snappable_datasets+=("${dataset}")
|
||||||
done <<<"${datasets_list}"
|
done 10<<<"${datasets_list}"
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_local_snappable_datasets () {
|
function get_local_snappable_datasets () {
|
||||||
local datasets_list
|
local datasets_list
|
||||||
datasets_list="$(findmnt --json --list --output 'fstype,source,target' | \
|
datasets_list="$(findmnt --json --list --output 'fstype,source,target' | \
|
||||||
jq --raw-output '.[][] | select(.fstype=="zfs") | .source')"
|
jq --raw-output '.[][] | select(.fstype=="zfs") | .source')"
|
||||||
while IFS= read -r dataset; do
|
while IFS= read -u10 -r dataset; do
|
||||||
local_snappable_datasets+=("${dataset}")
|
local_snappable_datasets+=("${dataset}")
|
||||||
done <<<"${datasets_list}"
|
done 10<<<"${datasets_list}"
|
||||||
}
|
}
|
||||||
|
|
||||||
function trim_globally_snappable_datasets () {
|
function trim_globally_snappable_datasets () {
|
||||||
@@ -111,11 +128,10 @@ function trim_globally_snappable_datasets () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function write_pkg_list_oneline () {
|
function write_pkg_list_oneline () {
|
||||||
local unabridged_pkg_list_oneline
|
if [[ "${severity}" == "${snaps_important_suffix}" ]]; then
|
||||||
if [[ "${severity}" == 'imp' ]]; then
|
|
||||||
for pkg in "${important_pkgs_in_transaction[@]}"; do
|
for pkg in "${important_pkgs_in_transaction[@]}"; do
|
||||||
if [[ "${unabridged_pkg_list_oneline}" ]]; then
|
if [[ "${unabridged_pkg_list_oneline}" ]]; then
|
||||||
unabridged_pkg_list_oneline="${unabridged_pkg_list_oneline}"','"${pkg}"
|
unabridged_pkg_list_oneline="${unabridged_pkg_list_oneline}${pkg_separator}${pkg}"
|
||||||
else
|
else
|
||||||
unabridged_pkg_list_oneline="${pkg}"
|
unabridged_pkg_list_oneline="${pkg}"
|
||||||
fi
|
fi
|
||||||
@@ -124,7 +140,7 @@ function write_pkg_list_oneline () {
|
|||||||
if [[ "${#trivial_pkgs_in_transaction[@]}" -ge '1' ]]; then
|
if [[ "${#trivial_pkgs_in_transaction[@]}" -ge '1' ]]; then
|
||||||
for pkg in "${trivial_pkgs_in_transaction[@]}"; do
|
for pkg in "${trivial_pkgs_in_transaction[@]}"; do
|
||||||
if [[ "${unabridged_pkg_list_oneline}" ]]; then
|
if [[ "${unabridged_pkg_list_oneline}" ]]; then
|
||||||
unabridged_pkg_list_oneline="${unabridged_pkg_list_oneline}"','"${pkg}"
|
unabridged_pkg_list_oneline="${unabridged_pkg_list_oneline}${pkg_separator}${pkg}"
|
||||||
else
|
else
|
||||||
unabridged_pkg_list_oneline="${pkg}"
|
unabridged_pkg_list_oneline="${pkg}"
|
||||||
fi
|
fi
|
||||||
@@ -132,79 +148,166 @@ function write_pkg_list_oneline () {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function find_max_dataset_name_length () {
|
function trim_single_remaining_package_name () {
|
||||||
local longest_op_suffix op_suffix_string
|
local pkg_name
|
||||||
longest_op_suffix='0'
|
pkg_name="${shorter_pkg_list}"
|
||||||
for op_suffix in "${snap_op_installation_suffix}" "${snap_op_remove_suffix}" "${snap_op_upgrade_suffix}"; do
|
case 1 in
|
||||||
if [[ "${#op_suffix}" -gt "${longest_op_suffix}" ]]; then
|
# Trim to 1 to 3 characters, no trailing ellipsis (...)
|
||||||
longest_op_suffix="${#op_suffix}"
|
$(( 1<=pkgs_list_max_length && pkgs_list_max_length<=3 )))
|
||||||
fi
|
pkg_name="${pkg_name::${pkgs_list_max_length}}"
|
||||||
done
|
;;
|
||||||
op_suffix_string="$(head -c "${longest_op_suffix}" '/dev/zero' | tr '\0' '_')"
|
# Show as many pkg name characters as we can while also
|
||||||
|
# fitting an ellipsis into the name (...) to indicate
|
||||||
local longest_sev_suffix sev_suffix_string
|
# that we've cut the pkg name off at the end.
|
||||||
longest_sev_suffix='0'
|
$(( pkgs_list_max_length>=4 )))
|
||||||
for sev_suffix in "${snaps_trivial_suffix}" "${snaps_important_suffix}"; do
|
pkg_name="${pkg_name::$(( pkgs_list_max_length - 3 ))}"'...'
|
||||||
if [[ "${#sev_suffix}" -gt "${longest_sev_suffix}" ]]; then
|
;;
|
||||||
longest_sev_suffix="${#sev_suffix}"
|
esac
|
||||||
fi
|
shorter_pkg_list="${pkg_name}"
|
||||||
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 () {
|
function trim_pkg_list_oneline () {
|
||||||
local available_pkg_list_length
|
local available_pkg_list_length
|
||||||
available_pkg_list_length="$((${max_zfs_snapshot_name_length} - ${max_dataset_name_length}))"
|
available_pkg_list_length="${1}"
|
||||||
if [[ "${available_pkg_list_length}" -lt "{pkgs_list_max_length}" ]]; then
|
if [[ "${available_pkg_list_length}" -lt "${pkgs_list_max_length}" ]]; then
|
||||||
# If we have fewer characters available than the user wants limit
|
# If we have fewer characters available before hitting the
|
||||||
# package list length
|
# ZFS internal maximum snapshot name length than the user
|
||||||
|
# wants limit package list length.
|
||||||
pkgs_list_max_length="${available_pkg_list_length}"
|
pkgs_list_max_length="${available_pkg_list_length}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local shorter_pkg_list
|
local shorter_pkg_list
|
||||||
shorter_pkg_list="${pkg_list_oneline}"
|
if [[ "${pkgs_list_max_length}" -le '0' ]]; then
|
||||||
|
# User wants zero characters of pkg names in snapshot name,
|
||||||
|
# no need to even find an appropriate pkg name string. Just
|
||||||
|
# set to empty string and we're done here.
|
||||||
|
shorter_pkg_list=''
|
||||||
|
else
|
||||||
|
shorter_pkg_list="${unabridged_pkg_list_oneline}"
|
||||||
while [[ "${#shorter_pkg_list}" -gt "${pkgs_list_max_length}" ]]; do
|
while [[ "${#shorter_pkg_list}" -gt "${pkgs_list_max_length}" ]]; do
|
||||||
shorter_pkg_list="${shorter_pkg_list%,*}"
|
shorter_pkg_list="${shorter_pkg_list%${pkg_separator}*}"
|
||||||
if ! grep -Piq ',' <<<"${shorter_pkg_list}"; then
|
if ! grep -Piq "${pkg_separator}" <<<"${shorter_pkg_list}"; then
|
||||||
# Only one package remains in package list, no need to continue
|
# Only one package remains in package list, no need to continue
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [[ "${#shorter_pkg_list}" -gt "${pkgs_list_max_length}" ]]; do
|
# If pkg name is still too long trim it. If there's enough
|
||||||
# If this is still too long we empty the package list
|
# space for an ellipsis (...) we add that to indicate we've
|
||||||
shorter_pkg_list=''
|
# trimmed the name, otherwise we just take however many
|
||||||
done
|
# characters of the pkg name we can get.
|
||||||
|
if [[ "${#shorter_pkg_list}" -gt "${pkgs_list_max_length}" ]]; then
|
||||||
|
trim_single_remaining_package_name
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
trimmed_pkg_list_oneline="${shorter_pkg_list}"
|
trimmed_pkg_list_oneline="${shorter_pkg_list}"
|
||||||
}
|
}
|
||||||
|
|
||||||
function do_snaps () {
|
function test_snap_names_for_validity () {
|
||||||
local snap_name
|
local snap_counter max_dataset_name_length trimmed_pkg_list_oneline dataset_name_no_pkgs dataset_name_with_pkgs
|
||||||
for snappable_dataset in "${snappable_datasets[@]}"; do
|
snap_counter="${1}"
|
||||||
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}"
|
max_dataset_name_length='0'
|
||||||
if [[ "${do_dry_run}" == 'true' ]]; then
|
for dataset in "${snappable_datasets[@]}"; do
|
||||||
pprint 'info' 'Dry-run, pretending to run zfs snapshot '"${snap_name}"
|
# Begin building snapshot name
|
||||||
else
|
dataset_name_no_pkgs="${dataset}"'@'"${snap_name_prefix}${snap_field_separator}${date_string}"
|
||||||
zfs snapshot "${snap_name}" && {
|
|
||||||
successfully_snapped_datasets+=("${snappable_dataset}")
|
# Append counter
|
||||||
pprint 'info' 'Snapshot done: '"${snap_name}"
|
dataset_name_no_pkgs="${dataset_name_no_pkgs}${snap_field_separator}${snap_counter}"
|
||||||
} || {
|
|
||||||
pprint 'warn' 'Snapshot failed: '"${snap_name}"
|
# Append operation, severity and packages fields
|
||||||
}
|
dataset_name_no_pkgs="${dataset_name_no_pkgs}${snap_field_separator}"'op:'"${conf_op_suffix}${snap_field_separator}"'sev:'"${severity}"
|
||||||
|
|
||||||
|
# Update the longest snapshot name seen so far. We add an automatic
|
||||||
|
# +6 to string length (or more exactly ${#snap_field_separator}+5)
|
||||||
|
# to account for the fact that by default the dataset will end in
|
||||||
|
# the separator string "${snap_field_separator}" plus 'pkgs:' for a
|
||||||
|
# total of 6 additional characters. If these additional characters
|
||||||
|
# cause us to reach or go over the ZFS dataset name length limit
|
||||||
|
# there's no point in attempting to add package names to snapshots.
|
||||||
|
# We calculate as if these additional characters existed and we add
|
||||||
|
# dataset names to our planned_snaps array as if they don't.
|
||||||
|
if [[ "$(( ${#dataset_name_no_pkgs}+${#snap_field_separator}+5 ))" -gt "${max_dataset_name_length}" ]]; then
|
||||||
|
max_dataset_name_length="$(( ${#dataset_name_no_pkgs}+${#snap_field_separator}+5 ))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
planned_snaps+=("${dataset_name_no_pkgs}")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Abort if this is longer than what ZFS allows
|
||||||
|
if [[ "${max_dataset_name_length}" -gt "${max_zfs_snapshot_name_length}" ]]; then
|
||||||
|
pprint 'err' 'Snapshot name would exceed ZFS '"${max_zfs_snapshot_name_length}"' chars limit. Aborting ...' '1'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${max_dataset_name_length}" -eq "${max_zfs_snapshot_name_length}" ]]; then
|
||||||
|
for planned_snap in "${planned_snaps[@]}"; do
|
||||||
|
if grep -Piq -- '^'"${planned_snap}"'$' <<<"${existing_snaps}"; then
|
||||||
|
# This snapshot name already exists. Unset array and break.
|
||||||
|
# Try again with next higher counter suffix.
|
||||||
|
unset planned_snaps[@]
|
||||||
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
# If planned_snaps array still has members we take the snapshot
|
||||||
|
# names already generated. If not we return without array in which
|
||||||
|
# case this function will run again with the snapshot counter
|
||||||
|
# incremented by one. Maximum length seen across all snapshot names
|
||||||
|
# is exactly the ZFS snapshot character limit. We won't be able to
|
||||||
|
# add packages to snapshot names but they will all fit perfectly.
|
||||||
|
# This is good enough.
|
||||||
|
return
|
||||||
|
else
|
||||||
|
# We have enough room to add package names.
|
||||||
|
local available_pkg_list_length
|
||||||
|
available_pkg_list_length="${pkgs_list_max_length}"
|
||||||
|
if [[ "${max_dataset_name_length}" -gt $(( max_zfs_snapshot_name_length - pkgs_list_max_length )) ]]; then
|
||||||
|
available_pkg_list_length="$(( max_zfs_snapshot_name_length - max_dataset_name_length ))"
|
||||||
|
fi
|
||||||
|
trim_pkg_list_oneline "${available_pkg_list_length}"
|
||||||
|
for planned_snap_id in "${!planned_snaps[@]}"; do
|
||||||
|
planned_snaps["${planned_snap_id}"]="${planned_snaps[${planned_snap_id}]}${snap_field_separator}"'pkgs:'"${trimmed_pkg_list_oneline}"
|
||||||
|
if grep -Piq -- '^'"${planned_snaps[${planned_snap_id}]}"'$' <<<"${existing_snaps}"; then
|
||||||
|
# This snapshot name already exists. Unset array and break.
|
||||||
|
# Try again with next higher counter suffix.
|
||||||
|
unset planned_snaps[@]
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_snap_names () {
|
||||||
|
local snap_counter existing_snaps
|
||||||
|
snap_counter='0'
|
||||||
|
existing_snaps="$(zfs list -t all -oname -H)"
|
||||||
|
until [[ "${#planned_snaps[@]}" -gt '0' ]]; do
|
||||||
|
snap_counter="$(( snap_counter+1 ))"
|
||||||
|
test_snap_names_for_validity "${snap_counter}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
function do_snaps () {
|
||||||
|
local snap_return_code
|
||||||
|
if [[ "${do_dry_run}" == 'true' ]]; then
|
||||||
|
pprint 'info' 'Dry-run, pretending to atomically do ZFS snapshot:'
|
||||||
|
for planned_snap in "${planned_snaps[@]}"; do
|
||||||
|
pprint 'info' ' '"${planned_snap}"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
zfs snapshot "${planned_snaps[@]}"
|
||||||
|
snap_return_code="${?}"
|
||||||
|
if [[ "${snap_return_code}" -eq '0' ]]; then
|
||||||
|
successfully_snapped_datasets=("${snappable_datasets[@]}")
|
||||||
|
pprint 'info' 'ZFS snapshot atomically done:'
|
||||||
|
for planned_snap in "${planned_snaps[@]}"; do
|
||||||
|
pprint 'info' ' '"${planned_snap}"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
pprint 'warn' 'ZFS snapshot failed:'
|
||||||
|
for planned_snap in "${planned_snaps[@]}"; do
|
||||||
|
pprint 'warn' ' '"${planned_snap}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_snaps_in_cur_sev () {
|
function get_snaps_in_cur_sev () {
|
||||||
@@ -213,13 +316,14 @@ function get_snaps_in_cur_sev () {
|
|||||||
snap_list="$(zfs list -H -o 'name' -t snapshot "${dataset_to_query}")"
|
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_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}")"
|
snaps_in_cur_sev="$(grep -Pi -- "${snap_field_separator}"'sev:'"${severity}${snap_field_separator}" <<<"${snaps_done_by_us}")"
|
||||||
printf -- '%s' "${snaps_in_cur_sev}"
|
printf -- '%s\n' "${snaps_in_cur_sev}"
|
||||||
}
|
}
|
||||||
|
|
||||||
function do_retention () {
|
function do_retention () {
|
||||||
local snap_list snaps_done_by_us snaps_in_cur_sev snaps_limit oldest_snap
|
local snap_list snaps_done_by_us snaps_in_cur_sev snaps_limit oldest_snap snap_return_code
|
||||||
|
local -a destroyed_snaps failed_to_destroy_snaps
|
||||||
if [[ "${do_dry_run}" == 'true' ]]; then
|
if [[ "${do_dry_run}" == 'true' ]]; then
|
||||||
pprint 'info' 'Dry-run, skipping potential zfs destroy operations ...'
|
pprint 'info' 'Dry-run, skipping potential ZFS destroy operations ...'
|
||||||
else
|
else
|
||||||
for successfully_snapped_dataset in "${successfully_snapped_datasets[@]}"; do
|
for successfully_snapped_dataset in "${successfully_snapped_datasets[@]}"; do
|
||||||
snaps_in_cur_sev="$(get_snaps_in_cur_sev "${successfully_snapped_dataset}")"
|
snaps_in_cur_sev="$(get_snaps_in_cur_sev "${successfully_snapped_dataset}")"
|
||||||
@@ -231,14 +335,38 @@ function do_retention () {
|
|||||||
while [[ "$(get_snaps_in_cur_sev "${successfully_snapped_dataset}" | wc -l)" -gt "${snaps_limit}" ]]; do
|
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)"
|
oldest_snap="$(get_snaps_in_cur_sev "${successfully_snapped_dataset}" | head -n1)"
|
||||||
zfs destroy "${oldest_snap}"
|
zfs destroy "${oldest_snap}"
|
||||||
|
snap_return_code="${?}"
|
||||||
|
if [[ "${snap_return_code}" -eq '0' ]]; then
|
||||||
|
destroyed_snaps+=("${oldest_snap}")
|
||||||
|
else
|
||||||
|
failed_to_destroy_snaps+=("${oldest_snap}")
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
if [[ "${#destroyed_snaps[@]}" -gt '0' ]]; then
|
||||||
|
pprint 'info' 'Oldest ZFS snapshot'"$([[ "${#failed_to_destroy_snaps[@]}" -gt '1' ]] && printf -- '%s' 's')"' in chain '"'"'sev:'"${severity}"''"'"' destroyed:'
|
||||||
|
for destroyed_snap in "${destroyed_snaps[@]}"; do
|
||||||
|
pprint 'info' ' '"${destroyed_snap}"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
if [[ "${#failed_to_destroy_snaps[@]}" -gt '0' ]]; then
|
||||||
|
pprint 'warn' 'Failed to prune ZFS snapshot'"$([[ "${#failed_to_destroy_snaps[@]}" -gt '1' ]] && printf -- '%s' 's')"' in chain '"'"'sev:'"${severity}"''"'"':'
|
||||||
|
for failed_to_destroy_snap in "${failed_to_destroy_snaps[@]}"; do
|
||||||
|
pprint 'warn' ' '"${failed_to_destroy_snap}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function main () {
|
function main () {
|
||||||
local pkgs_in_transaction
|
local pkgs_in_transaction
|
||||||
pkgs_in_transaction=("${@}")
|
pkgs_in_transaction=("${@}")
|
||||||
|
# Replace characters that are valid as Arch Linux package names but invalid as ZFS dataset names with something ZFS
|
||||||
|
# doesn't mind. Replace at characters ('@') indiscriminately with one dot each ('.'), replace plus characters ('+')
|
||||||
|
# with one underscore each ('_').
|
||||||
|
pkgs_in_transaction=("${pkgs_in_transaction[@]//+/_}")
|
||||||
|
pkgs_in_transaction=("${pkgs_in_transaction[@]//@/.}")
|
||||||
|
|
||||||
local -a important_pkgs_in_transaction trivial_pkgs_in_transaction
|
local -a important_pkgs_in_transaction trivial_pkgs_in_transaction
|
||||||
split_pkgs_by_importance "${pkgs_in_transaction[@]}"
|
split_pkgs_by_importance "${pkgs_in_transaction[@]}"
|
||||||
@@ -254,20 +382,27 @@ function main () {
|
|||||||
local local_snappable_datasets
|
local local_snappable_datasets
|
||||||
get_local_snappable_datasets
|
get_local_snappable_datasets
|
||||||
trim_globally_snappable_datasets
|
trim_globally_snappable_datasets
|
||||||
|
if [[ "${#snappable_datasets[@]}" -eq '0' ]]; then
|
||||||
|
pprint 'info' 'ZFS snapshot skipped, no local (= currently mounted) dataset has'
|
||||||
|
pprint 'info' 'property '"'"''"${zfs_prop}"''"'"' set to '"'"'true'"'"'. At the same'
|
||||||
|
pprint 'info' 'time option '"'"'snap_only_local_datasets'"'"' equals '"'"'true'"'"' so'
|
||||||
|
pprint 'info' 'we must only snapshot local datasets. Nothing to do here while'
|
||||||
|
pprint 'info' 'none of them have '"'"''"${zfs_prop}"''"'"' set to '"'"'true'"'"'.' '0'
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
snappable_datasets=("${globally_snappable_datasets}")
|
snappable_datasets=("${globally_snappable_datasets}")
|
||||||
|
if [[ "${#snappable_datasets[@]}" -eq '0' ]]; then
|
||||||
|
pprint 'info' 'ZFS snapshot skipped, no dataset has property '"'"''"${zfs_prop}"''"'"' set to '"'"'true'"'"'.' '0'
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local pkg_list_oneline
|
local unabridged_pkg_list_oneline
|
||||||
write_pkg_list_oneline
|
write_pkg_list_oneline
|
||||||
|
|
||||||
local date_string max_zfs_snapshot_name_length max_dataset_name_length
|
local date_string
|
||||||
date_string="$(date +"${snap_date_format}")"
|
local -a planned_snaps
|
||||||
max_zfs_snapshot_name_length='255'
|
date_string="$($([[ "${snap_timezone}" ]] && printf -- 'export TZ='"${snap_timezone}"); date +"${snap_date_format}")"
|
||||||
find_max_dataset_name_length
|
generate_snap_names
|
||||||
|
|
||||||
local trimmed_pkg_list_oneline
|
|
||||||
trim_pkg_list_oneline
|
|
||||||
|
|
||||||
local -a successfully_snapped_datasets
|
local -a successfully_snapped_datasets
|
||||||
do_snaps
|
do_snaps
|
||||||
|
Reference in New Issue
Block a user