initial commit
This commit is contained in:
commit
eea18118a0
4
README.md
Normal file
4
README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# nix-auto-update
|
||||||
|
|
||||||
|
A nix module for automatically updating a flake based system.
|
||||||
|
It is inspired by `system.autoUpgrade` but tries to implement protection against soft-bricked systems due to failed updates as well as accidential system downgrades.
|
||||||
7
flake.nix
Normal file
7
flake.nix
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
|
||||||
|
outputs = { self }: {
|
||||||
|
nixosModules.default = import ./module;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
218
module/default.nix
Normal file
218
module/default.nix
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.system.autoUpdate;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
|
||||||
|
options = {
|
||||||
|
|
||||||
|
system.autoUpdate = {
|
||||||
|
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Whether to periodically update the NixOS
|
||||||
|
configuration to the latest version.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
flake = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "github:kloenk/nix";
|
||||||
|
description = ''
|
||||||
|
The Flake URI of the NixOS configuration to build.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
flags = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
example = [
|
||||||
|
"-I"
|
||||||
|
"stuff=/home/alice/nixos-stuff"
|
||||||
|
"--option"
|
||||||
|
"extra-binary-caches"
|
||||||
|
"http://my-cache.example.org/"
|
||||||
|
];
|
||||||
|
description = ''
|
||||||
|
Any additional flags passed to {command}`nixos-rebuild`.
|
||||||
|
|
||||||
|
If you are using flakes and use a local repo you can add
|
||||||
|
{command}`[ "--update-input" "nixpkgs" "--commit-lock-file" ]`
|
||||||
|
to update nixpkgs.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
dates = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "04:40";
|
||||||
|
example = "daily";
|
||||||
|
description = ''
|
||||||
|
How often or when upgrade occurs. For most desktop and server systems
|
||||||
|
a sufficient upgrade frequency is once a day.
|
||||||
|
|
||||||
|
The format is described in
|
||||||
|
{manpage}`systemd.time(7)`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
randomizedDelaySec = lib.mkOption {
|
||||||
|
default = "0";
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "45min";
|
||||||
|
description = ''
|
||||||
|
Add a randomized delay before each automatic upgrade.
|
||||||
|
The delay will be chosen between zero and this value.
|
||||||
|
This value must be a time span in the format specified by
|
||||||
|
{manpage}`systemd.time(7)`
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
fixedRandomDelay = lib.mkOption {
|
||||||
|
default = false;
|
||||||
|
type = lib.types.bool;
|
||||||
|
example = true;
|
||||||
|
description = ''
|
||||||
|
Make the randomized delay consistent between runs.
|
||||||
|
This reduces the jitter between automatic upgrades.
|
||||||
|
See {option}`randomizedDelaySec` for configuring the randomized delay.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
rebootWindow = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
Define a lower and upper time value (in HH:MM format) which
|
||||||
|
constitute a time window during which reboots are allowed after an upgrade.
|
||||||
|
This option only has an effect when {option}`allowReboot` is enabled.
|
||||||
|
The default value of `null` means that reboots are allowed at any time.
|
||||||
|
'';
|
||||||
|
default = null;
|
||||||
|
example = {
|
||||||
|
lower = "01:00";
|
||||||
|
upper = "05:00";
|
||||||
|
};
|
||||||
|
type =
|
||||||
|
with lib.types;
|
||||||
|
nullOr (submodule {
|
||||||
|
options = {
|
||||||
|
lower = lib.mkOption {
|
||||||
|
description = "Lower limit of the reboot window";
|
||||||
|
type = lib.types.strMatching "[[:digit:]]{2}:[[:digit:]]{2}";
|
||||||
|
example = "01:00";
|
||||||
|
};
|
||||||
|
|
||||||
|
upper = lib.mkOption {
|
||||||
|
description = "Upper limit of the reboot window";
|
||||||
|
type = lib.types.strMatching "[[:digit:]]{2}:[[:digit:]]{2}";
|
||||||
|
example = "05:00";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
environment = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf lib.types.str;
|
||||||
|
description = "Extra Environment variables to pass to the update script.";
|
||||||
|
default = {};
|
||||||
|
example = {
|
||||||
|
GIT_SSH = "ssh -i /root/.ssh/custom-deploy-key";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
persistent = lib.mkOption {
|
||||||
|
default = true;
|
||||||
|
type = lib.types.bool;
|
||||||
|
example = false;
|
||||||
|
description = ''
|
||||||
|
Takes a boolean argument. If true, the time when the service
|
||||||
|
unit was last triggered is stored on disk. When the timer is
|
||||||
|
activated, the service unit is triggered immediately if it
|
||||||
|
would have been triggered at least once during the time when
|
||||||
|
the timer was inactive. Such triggering is nonetheless
|
||||||
|
subject to the delay imposed by RandomizedDelaySec=. This is
|
||||||
|
useful to catch up on missed runs of the service when the
|
||||||
|
system was powered down.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
||||||
|
system.autoUpgrade.flags = (
|
||||||
|
[
|
||||||
|
"--refresh"
|
||||||
|
"--flake ${cfg.flake}"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
systemd.services.nixos-update = {
|
||||||
|
description = "NixOS Upgrade";
|
||||||
|
|
||||||
|
restartIfChanged = false;
|
||||||
|
unitConfig.X-StopOnRemoval = false;
|
||||||
|
|
||||||
|
serviceConfig.Type = "oneshot";
|
||||||
|
|
||||||
|
environment =
|
||||||
|
config.nix.envVars
|
||||||
|
// {
|
||||||
|
inherit (config.environment.sessionVariables) NIX_PATH;
|
||||||
|
HOME = "/root";
|
||||||
|
}
|
||||||
|
// config.networking.proxy.envVars // cfg.environment;
|
||||||
|
|
||||||
|
path = with pkgs; [
|
||||||
|
coreutils
|
||||||
|
gnutar
|
||||||
|
xz.bin
|
||||||
|
gzip
|
||||||
|
gitMinimal
|
||||||
|
config.nix.package.out
|
||||||
|
config.programs.ssh.package
|
||||||
|
];
|
||||||
|
|
||||||
|
script = import ./script.nix {
|
||||||
|
inherit cfg;
|
||||||
|
nixos-rebuild = "${config.system.build.nixos-rebuild}/bin/nixos-rebuild";
|
||||||
|
date = "${pkgs.coreutils}/bin/date";
|
||||||
|
realpath = "${pkgs.coreutils}/bin/realpath";
|
||||||
|
stat = "${pkgs.coreutils}/bin/stat";
|
||||||
|
cut = "${pkgs.coreutils}/bin/cut";
|
||||||
|
head = "${pkgs.coreutils}/bin/head";
|
||||||
|
grep = "${pkgs.gnugrep}/bin/grep";
|
||||||
|
grub-reboot = "${pkgs.grub2}/bin/grub-reboot";
|
||||||
|
shutdown = "${config.systemd.package}/bin/shutdown";
|
||||||
|
bootctl = "${config.systemd.package}/bin/bootctl";
|
||||||
|
systemd-analyze = "${config.systemd.package}/bin/systemd-analyze";
|
||||||
|
upgradeFlag = "--upgrade";
|
||||||
|
units = "${pkgs.units}/bin/units";
|
||||||
|
jq = "${pkgs.jq}/bin/jq";
|
||||||
|
hostname = "${pkgs.nettools}/bin/hostname";
|
||||||
|
};
|
||||||
|
|
||||||
|
startAt = cfg.dates;
|
||||||
|
|
||||||
|
after = [ "network-online.target" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.timers.nixos-update = {
|
||||||
|
timerConfig = {
|
||||||
|
RandomizedDelaySec = cfg.randomizedDelaySec;
|
||||||
|
FixedRandomDelay = cfg.fixedRandomDelay;
|
||||||
|
Persistent = cfg.persistent;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
185
module/script.nix
Normal file
185
module/script.nix
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
{
|
||||||
|
cfg,
|
||||||
|
nixos-rebuild,
|
||||||
|
date,
|
||||||
|
realpath,
|
||||||
|
stat,
|
||||||
|
cut,
|
||||||
|
head,
|
||||||
|
grep,
|
||||||
|
grub-reboot,
|
||||||
|
shutdown,
|
||||||
|
bootctl,
|
||||||
|
systemd-analyze,
|
||||||
|
upgradeFlag,
|
||||||
|
units,
|
||||||
|
jq,
|
||||||
|
hostname,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
''
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
hostname=$(${hostname})
|
||||||
|
new_system=$(nix build --no-link --print-out-paths "${cfg.flake}#nixosConfigurations.''${hostname}.config.system.build.toplevel")
|
||||||
|
|
||||||
|
last_flake_update_time() {
|
||||||
|
nix flake metadata --refresh --json "$1" | ${jq} .lastModified
|
||||||
|
}
|
||||||
|
|
||||||
|
updated_since_reboot() {
|
||||||
|
[[ $(${stat} -c %Y /run/current-system) -gt $(($(${date} +%s) - $(${cut} -d. -f1 /proc/uptime) + $(${units} --compact --one-line "ceil($(${units} --compact --one-line "$(${systemd-analyze} time | head -n 1 | ${grep} -Eo "\S+\s+$")" seconds))"))) ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
boot_system_path() {
|
||||||
|
bpath=""
|
||||||
|
btime=0
|
||||||
|
for f in /nix/var/nix/profiles/*
|
||||||
|
do
|
||||||
|
if [[ -z $bpath ]] || [[ $btime -lt $(${stat} -c %Y $f) ]]
|
||||||
|
then
|
||||||
|
bpath=$f
|
||||||
|
btime=$(${stat} -c %Y $f)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -d "/nix/var/nix/profiles/system-profiles/" ]] && [[ -n "$(ls -A /nix/var/nix/profiles/system-profiles/)" ]]
|
||||||
|
then
|
||||||
|
for f in /nix/var/nix/profiles/system-profiles/*
|
||||||
|
do
|
||||||
|
if [[ -z $bpath ]] || [[ $btime -lt $(${stat} -c %Y $f) ]]
|
||||||
|
then
|
||||||
|
bpath=$f
|
||||||
|
btime=$(${stat} -c %Y $f)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
echo "$bpath"
|
||||||
|
}
|
||||||
|
|
||||||
|
last_update_time() {
|
||||||
|
rpath=$(${realpath} /run/current-system)
|
||||||
|
rtime=$(${stat} -c %Y /run/current-system)
|
||||||
|
boot_system=$(boot_system_path)
|
||||||
|
bpath=$(${realpath} "$boot_system")
|
||||||
|
btime=$(${stat} -c %Y "$boot_system")
|
||||||
|
if [[ "''${rpath}" == "''${bpath}" ]]
|
||||||
|
then
|
||||||
|
if ! updated_since_reboot
|
||||||
|
then
|
||||||
|
echo $btime
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ $rtime -gt $btime ]]
|
||||||
|
then
|
||||||
|
echo $rtime
|
||||||
|
else
|
||||||
|
echo $btime
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
lastest_system=$(boot_system_path)
|
||||||
|
|
||||||
|
|
||||||
|
if [[ "$(${realpath} /run/booted-system)" == "$(${realpath} /run/current-system)" ]] && [[ "$(${realpath} /run/booted-system)" == "$(${realpath} "$latest_system")" ]]
|
||||||
|
then
|
||||||
|
if [[ -f /boot/grub/grub.cfg ]]
|
||||||
|
then
|
||||||
|
if [[ $(($(${date} +%s) - $(${cut} -d. -f1 /proc/uptime))) -gt $(${stat} -c %Y /boot/grub/grub.cfg) ]]
|
||||||
|
then
|
||||||
|
echo "persisting boot system"
|
||||||
|
"$(${realpath} /run/booted-system)/bin/switch-to-configuration" "boot"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$new_system" == "$(${realpath} /run/current-system)" ]] && [[ "$new_system" == "$(${realpath} "$latest_system")" ]]
|
||||||
|
then
|
||||||
|
echo "system is up-to-date"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
system_update_time=$(last_update_time)
|
||||||
|
flake_update_time=$(last_flake_update_time "${cfg.flake}")
|
||||||
|
|
||||||
|
echo "last system update: $(${date} -d @"$system_update_time")"
|
||||||
|
echo "last flake update: $(${date} -d @"$flake_update_time")"
|
||||||
|
|
||||||
|
if [[ "$system_update_time" -gt "$flake_update_time" ]]
|
||||||
|
then
|
||||||
|
echo ""
|
||||||
|
echo "System is newer than remote!"
|
||||||
|
echo "This happens if the system was updated manually or a system update failed."
|
||||||
|
echo "Skipping system update to prevent potential downgrade!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Updating system ..."
|
||||||
|
|
||||||
|
system_info_booted="$(${realpath} /run/booted-system/{initrd,kernel,kernel-modules})"
|
||||||
|
system_info_built="$(${realpath} "''${new_system}"/{initrd,kernel,kernel-modules})"
|
||||||
|
|
||||||
|
echo $system_info_booted
|
||||||
|
echo $system_info_built
|
||||||
|
|
||||||
|
current_time="$(${date} +%H:%M)"
|
||||||
|
|
||||||
|
lower=${cfg.rebootWindow.lower}
|
||||||
|
upper=${cfg.rebootWindow.upper}
|
||||||
|
|
||||||
|
if [[ "''${lower}" < "''${upper}" ]]; then
|
||||||
|
if [[ "''${current_time}" > "''${lower}" ]] && \
|
||||||
|
[[ "''${current_time}" < "''${upper}" ]]; then
|
||||||
|
do_reboot="true"
|
||||||
|
else
|
||||||
|
do_reboot="false"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# lower > upper, so we are crossing midnight (e.g. lower=23h, upper=6h)
|
||||||
|
# we want to reboot if cur > 23h or cur < 6h
|
||||||
|
if [[ "''${current_time}" < "''${upper}" ]] || \
|
||||||
|
[[ "''${current_time}" > "''${lower}" ]]; then
|
||||||
|
do_reboot="true"
|
||||||
|
else
|
||||||
|
do_reboot="false"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$system_info_booted" != "$system_info_built" ]]
|
||||||
|
then
|
||||||
|
echo "Updating system requires reboot!"
|
||||||
|
if [ "''${do_reboot}" != true ]
|
||||||
|
then
|
||||||
|
echo "Outside of configured reboot window, skipping update."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo nix-env --profile /nix/var/nix/profiles/system --set "$new_system"
|
||||||
|
if [[ -f /boot/grub/grub.cfg ]]
|
||||||
|
then
|
||||||
|
echo "bootloader 'GRUB' detected, configuring test-boot into new system"
|
||||||
|
"''${latest_system}/bin/switch-to-configuration" "boot"
|
||||||
|
${grub-reboot} "NixOS - All configurations>0"
|
||||||
|
#${grub-reboot} "NixOS - Profile '$PROFILE_NAME'>0"
|
||||||
|
elif ${bootctl} list 2>&1 | ${grep} -v "No boot loader entries found." > /dev/null
|
||||||
|
then
|
||||||
|
echo "bootloader 'systemd-boot' detected, configuring oneshot-boot into new system"
|
||||||
|
# TODO
|
||||||
|
exit -1
|
||||||
|
else
|
||||||
|
echo "test-boot not supported. persisting new boot system"
|
||||||
|
"''${new_system}/bin/switch-to-configuration" "boot"
|
||||||
|
fi
|
||||||
|
${shutdown} -r +1
|
||||||
|
else
|
||||||
|
nix-env --profile /nix/var/nix/profiles/system --set "$new_system"
|
||||||
|
if ! "''${new_system}/bin/switch-to-configuration" "test"
|
||||||
|
then
|
||||||
|
echo "testing new configuration failed!"
|
||||||
|
echo "system was automatically rolled back to old configuration."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"''${new_system}/bin/switch-to-configuration" "boot"
|
||||||
|
fi
|
||||||
|
''
|
||||||
Loading…
x
Reference in New Issue
Block a user