diff --git a/README.md b/README.md index 6544efa..1192482 100644 --- a/README.md +++ b/README.md @@ -129,15 +129,30 @@ Conservative update policy with n-0.1 minor version pinning. Keeps you one minor **Commands:** ```bash -sudo ./proxmox-update-policy.sh enable # Enable with UI patches -sudo ./proxmox-update-policy.sh enable --no-ui # Enable without UI patches -sudo ./proxmox-update-policy.sh disable # Disable and restore UI +sudo ./proxmox-update-policy.sh enable # Pin + suppress warnings +sudo ./proxmox-update-policy.sh enable --no-ui # Pin only, keep warnings +sudo ./proxmox-update-policy.sh ui-only # Suppress warnings, pin nothing +sudo ./proxmox-update-policy.sh ui-disable # Restore warnings, keep pinning +sudo ./proxmox-update-policy.sh disable # Remove both sudo ./proxmox-update-policy.sh status # Show policy and versions sudo ./proxmox-update-policy.sh update # Refresh pinning sudo ./proxmox-update-policy.sh cron-enable # Install daily cron sudo ./proxmox-update-policy.sh cron-disable # Remove cron ``` +**Pinning and the UI customisation are independent.** A host can hold packages +back, suppress the subscription warnings, both, or neither: + +| Want | Command | +|---|---| +| Both | `enable` | +| Pin only, keep the warnings | `enable --no-ui` | +| Warnings gone, track latest | `ui-only` | +| Neither | `disable` | + +`ui-only` is the right choice for a host that should stay current -- a lab or +a personal box -- but without Proxmox telling you off about it on every login. + **UI customisations:** When enabled, the web interface reports the update policy that is actually in diff --git a/postinstall/proxmox-cleanup.sh b/postinstall/proxmox-cleanup.sh index 9b1a2d7..badfac1 100644 --- a/postinstall/proxmox-cleanup.sh +++ b/postinstall/proxmox-cleanup.sh @@ -82,6 +82,7 @@ OLD_SYSCTL=( ) FOUND=0 +KEEP=0 ACTIONS=() #--------------------------------------------------------------- @@ -107,6 +108,29 @@ has_readonly_keys() { echo "$content" | grep -qE '^[[:space:]]*(kernel\.random\.entropy_avail|kernel\.random\.(boot_id|uuid)|fs\.dentry-state|fs\.file-nr)[[:space:]]*=' } +sysctl_keys() { + # Settable keys in sysctl.d content ($1), one per line, sorted. + # Commented lines are inert and must not count. + local content="$1" + echo "$content" \ + | grep -oE '^[[:space:]]*[a-z][a-z0-9._-]*[[:space:]]*=' \ + | tr -d ' =' \ + | sort -u +} + +orphaned_keys() { + # Keys set by the superseded file ($1) that the replacement ($2) does NOT + # set, one per line. + # + # Removing a file purely because a newer generation exists loses whatever + # the newer one never picked up. On a real host the superseded tweaks file + # carried five keys the replacement had no equivalent for -- dirty + # writeback intervals, keepalive probe tuning, an inotify limit -- and all + # five would have gone silently. A backup makes that recoverable, not safe. + local old_content="$1" new_content="$2" + comm -23 <(sysctl_keys "$old_content") <(sysctl_keys "$new_content") +} + network_tier_from_path() { # 99-proxmox-network-10gbe.conf -> 10gbe local path="$1" base @@ -158,8 +182,28 @@ scan() { note high "$f" \ "sets read-only kernel statistics -- errors on every boot" else + # Only call it superseded if the replacement actually covers it. + local current_content orphans + current_content=$(cat /etc/sysctl.d/98-proxmox-optimize.conf 2>/dev/null || echo "") + orphans=$(orphaned_keys "$content" "$current_content") + + if [ -n "$orphans" ]; then + KEEP=$((KEEP + 1)) + # Counts as "something to report", or the no-findings fallback + # prints "none" directly underneath this entry. + found_sysctl=1 + printf ' %b[KEEP]%b %s\n' "$CYAN" "$NC" "$f" + printf ' overrides 98-proxmox-optimize.conf, but sets %s key(s) it does not:\n' \ + "$(echo "$orphans" | wc -l)" + while IFS= read -r orphan_key; do + printf ' %s\n' "$orphan_key" + done <<< "$orphans" + printf ' NOT removed -- fold these into 98-proxmox-optimize.conf first.\n' + continue + fi + note med "$f" \ - "superseded by 98-proxmox-optimize.conf, which it currently overrides by sort order" + "superseded by 98-proxmox-optimize.conf, which covers every key it sets" fi found_sysctl=1 done @@ -209,6 +253,9 @@ scan() { else echo -e "${YELLOW}${FOUND} superseded artefact(s) found.${NC}" fi + if [ "$KEEP" -gt 0 ]; then + echo -e "${CYAN}${KEEP} file(s) kept: they still set keys nothing else does.${NC}" + fi } #--------------------------------------------------------------- diff --git a/postinstall/proxmox-disk-errors.sh b/postinstall/proxmox-disk-errors.sh index b8771fd..1be491a 100644 --- a/postinstall/proxmox-disk-errors.sh +++ b/postinstall/proxmox-disk-errors.sh @@ -50,6 +50,13 @@ # completely unrelated, healthy pool lost their I/O. SMART reported PASSED # throughout. # +# Generalised from the hardening Derek wrote by hand during that incident. +# Two details are his and were reverted to after the generated versions +# proved worse: addressing drives by /dev/disk/by-id rather than sdX, and +# matching the udev rule on ID_MODEL rather than per-drive serial. Both +# matter for the same reason -- the protection has to survive a reboot or a +# disk swap without anyone remembering to re-run this. +# # SAFETY -- READ THIS: # SCT ERC is only correct BECAUSE there is redundancy. Telling a drive in a # NON-redundant pool to give up early tells it to abandon data it might @@ -179,6 +186,39 @@ base_device() { return 0 } +stable_device_path() { + # A /dev/disk/by-id path for $1, or /dev/$1 if none exists. + # + # Reverted to Derek's original approach from the incident scripts, which + # addressed drives by /dev/disk/by-id. The generated version used bare sdX + # names and was worse: kernel names are assigned in discovery order and are + # NOT stable across reboots, so a boot-time unit that reasserts drive + # settings by kernel name can target a different disk than the one it was + # written for. On a host where some drives take the setting and others + # cannot, that is exactly how the wrong drive ends up unprotected. + # + # ata-/nvme- links carry model and serial and are preferred; wwn- links are + # stable too but opaque, so they are only a fallback. + local dev="$1" link base fallback="" + + for link in /dev/disk/by-id/*; do + [ -e "$link" ] || continue + base=$(basename "$link") + case "$base" in *-part[0-9]*) continue ;; esac + [ "$(readlink -f "$link")" = "/dev/$dev" ] || continue + case "$base" in + wwn-*) [ -z "$fallback" ] && fallback="$link" ;; + *) echo "$link"; return 0 ;; + esac + done + + if [ -n "$fallback" ]; then + echo "$fallback" + return 0 + fi + echo "/dev/$dev" +} + pool_members() { # Base device names backing pool $1, one per line, deduplicated. local pool="$1" member dev @@ -285,9 +325,17 @@ write_erc_service() { return 0 fi + # Resolve to stable by-id paths at generation time. The helper must not + # depend on kernel names surviving a reboot -- see stable_device_path. + local dev paths=() + for dev in "${devices[@]}"; do + paths+=("$(stable_device_path "$dev")") + done + # SCT ERC is volatile: it is lost on a power cycle (though usually not on a # warm reboot), so it must be reasserted at every boot rather than set once. - cat > "$ERC_HELPER" << EOF + { + cat << EOF #!/bin/bash # Reassert SCT ERC on redundant pool members. # Installed by: proxmox-disk-errors.sh -- do not edit, changes are overwritten. @@ -295,16 +343,30 @@ write_erc_service() { # SCT ERC does not persist across a power cycle. Without this, the setting # silently reverts to the factory default (usually Disabled) and the drive goes # back to retrying without bound. +# +# Drives are addressed by /dev/disk/by-id, not sdX: kernel names are assigned +# in discovery order and a drive that moves between boots would otherwise have +# the setting applied to whatever took its old name. + +EOF + for dev in "${paths[@]}"; do + printf 'for_each_drive+=("%s")\n' "$dev" + done + cat << EOF -for dev in ${devices[*]}; do - [ -b "/dev/\$dev" ] || continue - smartctl -l scterc,${ERC_DECISECONDS},${ERC_DECISECONDS} "/dev/\$dev" >/dev/null 2>&1 \\ - && logger -t zfs-disk-erc "SCT ERC ${ERC_DECISECONDS}ds set on \$dev" \\ - || logger -t zfs-disk-erc "failed to set SCT ERC on \$dev" +for drive in "\${for_each_drive[@]}"; do + # A drive that has been pulled or replaced must not fail the unit. + [ -e "\$drive" ] || { logger -t zfs-disk-erc "absent, skipped: \$drive"; continue; } + if smartctl -l scterc,${ERC_DECISECONDS},${ERC_DECISECONDS} "\$drive" >/dev/null 2>&1; then + logger -t zfs-disk-erc "SCT ERC ${ERC_DECISECONDS}ds set on \$drive" + else + logger -t zfs-disk-erc "failed to set SCT ERC on \$drive" + fi done exit 0 EOF + } > "$ERC_HELPER" chmod +x "$ERC_HELPER" cat > "$ERC_SERVICE" << EOF @@ -349,24 +411,49 @@ write_udev_rule() { echo "# up on their behalf. The 30s default is far longer than any healthy SATA" echo "# I/O; ${NO_SCT_TIMEOUT}s converts a stall into an error ZFS can actually repair from." echo "#" - echo "# Matched on serial so the rule survives device renaming (sdc is not stable)." + echo "# Matched on MODEL, not serial -- reverted to Derek's original incident" + echo "# rule, which matched ENV{ID_MODEL} and worked better than the generated" + echo "# per-serial version that briefly replaced it." + echo "#" + echo "# Whether a drive supports SCT ERC is a property of the MODEL, so a" + echo "# replacement of the same model needs the same backstop -- and would not" + echo "# get it from a serial-specific rule until somebody remembered to re-run" + echo "# this script. Fitting a replacement disk is exactly the moment nobody is" + echo "# thinking about kernel timeouts." echo "#" echo "# DEVTYPE==\"disk\" restricts this to whole disks. A partition carries the" - echo "# same serial but has no device/timeout attribute of its own -- without the" - echo "# restriction udev retries the assignment against every partition and logs a" - echo "# failure for each." + echo "# same model but has no device/timeout attribute of its own -- without the" + echo "# restriction udev retries the assignment against every partition and logs" + echo "# a failure for each." echo "" + + # One rule per distinct model, rather than one per drive. + local models=() model seen for dev in "${devices[@]}"; do - serial=$(udevadm info --query=property --name="/dev/$dev" 2>/dev/null | \ - sed -n 's/^ID_SERIAL_SHORT=//p' | head -1) - if [ -n "$serial" ]; then - echo "ACTION==\"add|change\", SUBSYSTEM==\"block\", ENV{DEVTYPE}==\"disk\", ENV{ID_SERIAL_SHORT}==\"${serial}\", ATTR{device/timeout}=\"${NO_SCT_TIMEOUT}\"" - else - # No stable serial exposed; fall back to the kernel name and say - # so, rather than silently writing a rule that matches nothing. - echo "# WARNING: no ID_SERIAL_SHORT for ${dev}, matching on kernel name (not stable across reboots)" - echo "ACTION==\"add|change\", SUBSYSTEM==\"block\", ENV{DEVTYPE}==\"disk\", KERNEL==\"${dev}\", ATTR{device/timeout}=\"${NO_SCT_TIMEOUT}\"" + model=$(udevadm info --query=property --name="/dev/$dev" 2>/dev/null | \ + sed -n 's/^ID_MODEL=//p' | head -1) + if [ -z "$model" ]; then + serial=$(udevadm info --query=property --name="/dev/$dev" 2>/dev/null | \ + sed -n 's/^ID_SERIAL_SHORT=//p' | head -1) + if [ -n "$serial" ]; then + echo "# ${dev}: no ID_MODEL exposed, falling back to serial" + echo "ACTION==\"add|change\", SUBSYSTEM==\"block\", ENV{DEVTYPE}==\"disk\", ENV{ID_SERIAL_SHORT}==\"${serial}\", ATTR{device/timeout}=\"${NO_SCT_TIMEOUT}\"" + else + echo "# WARNING: no ID_MODEL or ID_SERIAL_SHORT for ${dev}; kernel name is not stable across reboots" + echo "ACTION==\"add|change\", SUBSYSTEM==\"block\", ENV{DEVTYPE}==\"disk\", KERNEL==\"${dev}\", ATTR{device/timeout}=\"${NO_SCT_TIMEOUT}\"" + fi + continue fi + + seen=0 + for m in "${models[@]:-}"; do + [ "$m" = "$model" ] && { seen=1; break; } + done + [ "$seen" -eq 1 ] && continue + models+=("$model") + + echo "# ${model} -- no SCT ERC support" + echo "ACTION==\"add|change\", SUBSYSTEM==\"block\", ENV{DEVTYPE}==\"disk\", ENV{ID_MODEL}==\"${model}\", ATTR{device/timeout}=\"${NO_SCT_TIMEOUT}\"" done } > "$UDEV_RULE" diff --git a/postinstall/proxmox-optimize.sh b/postinstall/proxmox-optimize.sh index b4bbd93..8a4a77f 100755 --- a/postinstall/proxmox-optimize.sh +++ b/postinstall/proxmox-optimize.sh @@ -219,11 +219,21 @@ net.core.netdev_max_backlog=8192 net.core.somaxconn=8192 net.ipv4.tcp_fin_timeout=30 net.ipv4.tcp_keepalive_time=300 +net.ipv4.tcp_keepalive_probes=3 +net.ipv4.tcp_keepalive_intvl=30 net.ipv4.tcp_tw_reuse=1 # File System fs.file-max=2097152 fs.inotify.max_user_watches=524288 +fs.inotify.max_user_instances=512 + +# Deliberately NOT set here: vm.dirty_expire_centisecs and +# vm.dirty_writeback_centisecs. Raising them keeps dirty pages in memory +# longer, which trades a measurable amount of unflushed data against a small +# throughput gain. That is a defensible choice on a host with a UPS and a +# reckless one without, so it is left to the operator rather than shipped as a +# default -- this file otherwise holds nothing that risks data on power loss. EOF # Bridge netfilter: NOT a Proxmox requirement, despite being widely repeated as diff --git a/postinstall/proxmox-power-management.sh b/postinstall/proxmox-power-management.sh index 9374df7..c451cf4 100755 --- a/postinstall/proxmox-power-management.sh +++ b/postinstall/proxmox-power-management.sh @@ -330,7 +330,18 @@ for host in /sys/class/scsi_host/host*/; do CURRENT=$(tr -d ' ' < "${host}link_power_management_policy" 2>/dev/null) if [[ " $ZFS_HOSTS " == *" $HOSTNAME_ID "* ]]; then - echo " ${HOSTNAME_ID}: backs a ZFS pool, leaving at '$CURRENT'" + # Assert max_performance rather than merely skipping. Skipping + # prevents this script making things worse, but leaves a controller + # an EARLIER run already put into a power-saving state -- which is + # the situation on any host that ran a previous version. The disks + # holding the VMs are the last place to save a watt. + if [[ "$CURRENT" == "max_performance" ]] || [[ "$CURRENT" == "keep_firmware_settings" ]]; then + echo " ${HOSTNAME_ID}: backs a ZFS pool, already '$CURRENT'" + elif echo "max_performance" > "${host}link_power_management_policy" 2>/dev/null; then + echo " ${HOSTNAME_ID}: backs a ZFS pool, '$CURRENT' -> max_performance" + else + echo -e " ${HOSTNAME_ID}: ${YELLOW}backs a ZFS pool but could not clear '$CURRENT'${NC}" + fi SATA_SKIPPED=$((SATA_SKIPPED + 1)) continue fi @@ -360,7 +371,7 @@ done set -e [ $SATA_COUNT -gt 0 ] && echo "Configured $SATA_COUNT SATA controllers" || echo "No SATA controllers needed changes" -[ $SATA_SKIPPED -gt 0 ] && echo "Skipped $SATA_SKIPPED controller(s) backing ZFS pools" +[ "$SATA_SKIPPED" -gt 0 ] && echo "$SATA_SKIPPED controller(s) backing ZFS pools held at max_performance" ############################################# # Network Power Management @@ -581,6 +592,43 @@ for usb in /sys/bus/usb/devices/*/power/control; do fi fi done + +# Hold SATA controllers that back a ZFS pool at max_performance. +# +# The sysfs link power policy does not persist across a reboot, so setting it +# once at install time is not enough -- it reverts to whatever the kernel +# defaults to for that controller. Reasserting it here is what makes it stick. +# +# The disks holding the VMs are the last place to save a watt: link power +# management parks the SATA link between commands and adds wake latency to +# every I/O, which works directly against the bounded retry times configured by +# proxmox-disk-errors.sh. +ZFS_HOSTS="" +if command -v zpool >/dev/null 2>&1; then + for pool in $(zpool list -H -o name 2>/dev/null); do + while read -r member; do + [ -z "$member" ] && continue + memberdev=$(readlink -f "$member" 2>/dev/null) + memberdev=${memberdev#/dev/} + memberdev=$(echo "$memberdev" | sed -E 's/(nvme[0-9]+n[0-9]+)p[0-9]+$/\1/; s/^([a-z]+)[0-9]+$/\1/') + hostpath=$(readlink -f "/sys/block/${memberdev}/device" 2>/dev/null) || continue + hostid=$(echo "$hostpath" | grep -oE 'host[0-9]+' | head -1) + [ -n "$hostid" ] && ZFS_HOSTS="$ZFS_HOSTS $hostid" + done < <(zpool list -vHP "$pool" 2>/dev/null | awk '$1 ~ /^\/dev\// {print $1}') + done +fi + +for host in /sys/class/scsi_host/host*/; do + [ -f "${host}link_power_management_policy" ] || continue + hostid=$(basename "$host") + case " $ZFS_HOSTS " in + *" $hostid "*) + CURRENT=$(tr -d ' ' < "${host}link_power_management_policy" 2>/dev/null) + [ "$CURRENT" != "max_performance" ] && \ + echo "max_performance" > "${host}link_power_management_policy" 2>/dev/null || true + ;; + esac +done EOF chmod +x /usr/local/bin/proxmox-power-apply.sh diff --git a/postinstall/proxmox-update-policy.sh b/postinstall/proxmox-update-policy.sh index 7132ae0..d4745e1 100755 --- a/postinstall/proxmox-update-policy.sh +++ b/postinstall/proxmox-update-policy.sh @@ -49,11 +49,19 @@ # Commands: # enable - Enable conservative update policy (default) # disable - Disable policy and allow all updates +# ui-only - Suppress subscription warnings WITHOUT pinning anything +# ui-disable - Remove the UI customisation, leave pinning alone # status - Show current policy and pinned versions # update - Refresh pinning based on current repo state # cron-enable - Install daily cron job to auto-update pinning # cron-disable- Remove the cron job # +# Pinning and the UI customisation are INDEPENDENT. A host can hold its +# packages back, suppress the subscription warnings, both, or neither. They +# were previously welded together -- the APT hook gated on the pinning file -- +# so a host that wanted latest packages silently got the warnings back with no +# way to say otherwise. +# # Requirements: # - Proxmox VE # - Root privileges @@ -86,6 +94,13 @@ MANAGER_FILE="/usr/share/pve-manager/js/pvemanagerlib.js" INDEX_TPL="/usr/share/pve-manager/index.html.tpl" POLICY_JS="/usr/share/pve-manager/js/conservative-policy.js" +# The served copy above lives in a pve-manager-owned directory, so a package +# update can delete it. This is the master copy, outside anything dpkg manages: +# it is both the marker that the customisation is installed and the source the +# APT hook restores from. +UI_STATE_DIR="/usr/local/share/proxmox-conservative" +UI_STATE_JS="${UI_STATE_DIR}/conservative-policy.js" + # Packages to pin, and how each of them is actually versioned. # # These packages do NOT share a version series. Pinning them all to @@ -407,6 +422,11 @@ replace_once() { write_policy_js() { # The whole customisation, as an ExtJS override. + # + # Written to the master copy; install_policy_js puts it where pveproxy + # serves it from. Tests point POLICY_JS at a temporary path, so the two + # stay separate rather than one being derived from the other. + mkdir -p "$(dirname "$POLICY_JS")" cat > "$POLICY_JS" << 'POLICY_JS_EOF' // Conservative Update Policy - UI customisation // Installed by: proxmox-update-policy.sh @@ -584,7 +604,11 @@ patch_ui() { [ -z "$quiet" ] && echo -e "${GREEN}OK Saved $INDEX_BACKUP${NC}" fi - write_policy_js + # Master copy first, then the served copy. A pve-manager update can delete + # the served one; the master survives and the APT hook restores from it. + mkdir -p "$UI_STATE_DIR" + POLICY_JS="$UI_STATE_JS" write_policy_js + install -m 0644 "$UI_STATE_JS" "$POLICY_JS" [ -z "$quiet" ] && echo -e "${GREEN}OK Wrote $POLICY_JS${NC}" if ensure_script_tag; then @@ -611,6 +635,12 @@ unpatch_ui() { local quiet="${1:-}" local restored=false + if [ -f "$UI_STATE_JS" ]; then + rm -f "$UI_STATE_JS" + rmdir "$UI_STATE_DIR" 2>/dev/null || true + restored=true + fi + if [ -f "$POLICY_JS" ]; then rm -f "$POLICY_JS" [ -z "$quiet" ] && echo -e "${GREEN}OK Removed $POLICY_JS${NC}" @@ -671,10 +701,13 @@ install_ui_hook() { INDEX_TPL="/usr/share/pve-manager/index.html.tpl" POLICY_JS="/usr/share/pve-manager/js/conservative-policy.js" +UI_STATE_JS="/usr/local/share/proxmox-conservative/conservative-policy.js" ANCHOR=' ' -# Only act when the policy is actually enabled. -[ -f "/etc/apt/preferences.d/proxmox-conservative" ] || exit 0 +# Gate on the UI customisation's OWN marker, not on the pinning file. The two +# are independent: a host can suppress the subscription warnings while tracking +# latest packages, and gating on the pins meant that host silently got neither. +[ -f "$UI_STATE_JS" ] || exit 0 [ -f "$INDEX_TPL" ] || exit 0 CHANGED=0 @@ -690,11 +723,17 @@ if ! grep -qF "conservative-policy.js" "$INDEX_TPL" 2>/dev/null; then fi fi -# The JS file itself lives in a pve-manager-owned directory and can be removed -# by a package update. Only its absence is repaired here; its CONTENT is owned -# by proxmox-update-policy.sh, so a stale hook can never overwrite a newer file. +# The served copy lives in a pve-manager-owned directory and is removed by a +# package update. Restore it from the master copy rather than just complaining: +# the content comes from that file, so a stale hook cannot write stale content. if [ ! -f "$POLICY_JS" ]; then - logger -t proxmox-policy-hook "conservative-policy.js missing; run 'proxmox-update-policy.sh update' to reinstate" + mkdir -p "$(dirname "$POLICY_JS")" + if install -m 0644 "$UI_STATE_JS" "$POLICY_JS" 2>/dev/null; then + CHANGED=1 + logger -t proxmox-policy-hook "restored conservative-policy.js" + else + logger -t proxmox-policy-hook "could not restore conservative-policy.js from $UI_STATE_JS" + fi fi # Restart pveproxy ONLY if something actually changed. Restarting it on every @@ -1177,11 +1216,13 @@ disable_cron() { show_help() { echo "Proxmox VE Conservative Update Policy" echo "" - echo "Usage: $0 {enable|disable|status|update|cron-enable|cron-disable} [options]" + echo "Usage: Usage: $0 {enable|disable|status|update|cron-enable|cron-disable} [options] {enable|disable|ui-only|ui-disable|status|update|cron-enable|cron-disable} [options]" echo "" echo "Commands:" echo " enable - Enable policy (pin to n-0.1 minor version)" echo " disable - Disable policy (allow all updates)" + echo " ui-only - Suppress subscription warnings WITHOUT pinning" + echo " ui-disable - Remove the UI customisation, leave pinning alone" echo " status - Show current policy and available versions" echo " update - Refresh pinning based on current repo state" echo " cron-enable - Install daily cron job to auto-update pinning" @@ -1243,6 +1284,27 @@ case "${1:-help}" in done enable_policy "" "$no_ui" ;; + ui-only) + # Suppress the subscription warnings WITHOUT pinning anything. + # + # The two are independent concerns and were previously welded together: + # the hook gated on the pinning file, so a host that wanted latest + # packages silently got the warnings back with no way to say otherwise. + echo -e "${GREEN}=== UI Customisation Only (no version pinning) ===${NC}\n" + patch_ui "" || exit 1 + install_ui_hook "" || exit 1 + echo "" + echo "The subscription warnings are suppressed. No packages are pinned:" + echo "this host tracks whatever the repository offers." + echo "" + echo "To pin as well: sudo $0 enable" + echo "To undo: sudo $0 ui-disable" + ;; + ui-disable) + echo -e "${GREEN}=== Removing UI Customisation ===${NC}\n" + remove_ui_hook "" + unpatch_ui "" + ;; disable) disable_policy ;; diff --git a/tests/test-cleanup.sh b/tests/test-cleanup.sh index 9144db0..e0d501a 100755 --- a/tests/test-cleanup.sh +++ b/tests/test-cleanup.sh @@ -120,6 +120,69 @@ check "commented-out key ignored" \ check "writable neighbour not matched" \ "1" "$(has_readonly_keys 'kernel.random.write_wakeup_threshold=1024'; echo $?)" +############################################# +echo -e "\n${CYAN}sysctl_keys / orphaned_keys${NC}" +############################################# + +check "extracts settable keys" \ + $'net.ipv4.tcp_fin_timeout\nvm.swappiness' \ + "$(sysctl_keys $'vm.swappiness=10\nnet.ipv4.tcp_fin_timeout=30')" + +# A commented key is inert; counting it would make a file look like it still +# carried a setting it does not. +check "ignores commented keys" \ + "vm.swappiness" \ + "$(sysctl_keys $'vm.swappiness=10\n# kernel.random.entropy_avail=4096')" + +check "tolerates spaces around =" \ + "net.core.rmem_max" \ + "$(sysctl_keys 'net.core.rmem_max = 33554432')" + +# The real content from the live host. Removing the tweaks file because a +# newer generation exists would have dropped five keys the replacement has no +# equivalent for -- silently, and with only a backup to notice it by. +HOST_TWEAKS='vm.swappiness=10 +vm.dirty_background_ratio=5 +vm.dirty_ratio=10 +vm.dirty_expire_centisecs=12000 +vm.dirty_writeback_centisecs=1500 +net.core.netdev_max_backlog=8192 +net.ipv4.tcp_fin_timeout=30 +net.ipv4.tcp_keepalive_time=600 +net.ipv4.tcp_keepalive_probes=3 +net.ipv4.tcp_keepalive_intvl=30 +fs.inotify.max_user_watches=524288 +fs.inotify.max_user_instances=512' + +HOST_OPTIMIZE='vm.swappiness=10 +vm.vfs_cache_pressure=50 +vm.dirty_background_ratio=5 +vm.dirty_ratio=10 +net.core.netdev_max_backlog=8192 +net.core.somaxconn=8192 +net.ipv4.tcp_fin_timeout=30 +net.ipv4.tcp_keepalive_time=300 +net.ipv4.tcp_tw_reuse=1 +fs.file-max=2097152 +fs.inotify.max_user_watches=524288' + +check "finds the five keys the replacement lacks" \ + $'fs.inotify.max_user_instances\nnet.ipv4.tcp_keepalive_intvl\nnet.ipv4.tcp_keepalive_probes\nvm.dirty_expire_centisecs\nvm.dirty_writeback_centisecs' \ + "$(orphaned_keys "$HOST_TWEAKS" "$HOST_OPTIMIZE")" + +# A genuinely superseded file has nothing the replacement lacks, and must +# still be removable -- the guard must not block every cleanup. +check "fully covered file has no orphans" \ + "" "$(orphaned_keys $'vm.swappiness=10\nvm.dirty_ratio=10' "$HOST_OPTIMIZE")" + +check "empty old file has no orphans" \ + "" "$(orphaned_keys "" "$HOST_OPTIMIZE")" + +# A differing VALUE on a shared key is not an orphan: the replacement still +# sets it, so nothing is lost by removing the older file. +check "differing value is not an orphan" \ + "" "$(orphaned_keys 'net.ipv4.tcp_keepalive_time=600' "$HOST_OPTIMIZE")" + ############################################# echo -e "\n${CYAN}network_tier_from_path${NC}" #############################################