prep for next ai session

This commit is contained in:
2026-08-01 15:45:23 -05:00
parent 46c35106fb
commit 6244c1cc25
139 changed files with 16712 additions and 0 deletions
@@ -0,0 +1,9 @@
[Unit]
Description=Check_MK LibreNMS Agent Socket
[Socket]
ListenStream=6556
Accept=yes
[Install]
WantedBy=sockets.target
@@ -0,0 +1,7 @@
[Unit]
Description=Check_MK LibreNMS Agent Service
After=network.target
[Service]
ExecStart=/usr/bin/check_mk_agent
StandardOutput=socket
@@ -0,0 +1,659 @@
#!/bin/bash
# +------------------------------------------------------------------+
# | ____ _ _ __ __ _ __ |
# | / ___| |__ ___ ___| | __ | \/ | |/ / |
# | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
# | | |___| | | | __/ (__| < | | | | . \ |
# | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
# | |
# | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation in version 2. check_mk is distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
# out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more de-
# ails. You should have received a copy of the GNU General Public
# License along with GNU Make; see the file COPYING. If not, write
# to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301 USA.
# Remove locale settings to eliminate localized outputs where possible
export LC_ALL=C
unset LANG
export MK_LIBDIR="/usr/lib/check_mk_agent"
export MK_CONFDIR="/etc/check_mk"
export MK_VARDIR="/var/lib/check_mk_agent"
# Provide information about the remote host. That helps when data
# is being sent only once to each remote host.
if [ "$REMOTE_HOST" ] ; then
export REMOTE=$REMOTE_HOST
elif [ "$SSH_CLIENT" ] ; then
export REMOTE=${SSH_CLIENT%% *}
fi
# Make sure, locally installed binaries are found
PATH=$PATH:/usr/local/bin
# All executables in PLUGINSDIR will simply be executed and their
# ouput appended to the output of the agent. Plugins define their own
# sections and must output headers with '<<<' and '>>>'
PLUGINSDIR=$MK_LIBDIR/plugins
# All executables in LOCALDIR will by executabled and their
# output inserted into the section <<<local>>>. Please
# refer to online documentation for details about local checks.
LOCALDIR=$MK_LIBDIR/local
# All files in SPOOLDIR will simply appended to the agent
# output if they are not outdated (see below)
SPOOLDIR=$MK_VARDIR/spool
# close standard input (for security reasons) and stderr
if [ "$1" = -d ]
then
set -xv
else
exec </dev/null 2>/dev/null
fi
# Runs a command asynchronous by use of a cache file
function run_cached () {
local section=
if [ "$1" = -s ] ; then local section="echo '<<<$2>>>' ; " ; shift ; fi
local NAME=$1
local MAXAGE=$2
shift 2
local CMDLINE="$section$@"
if [ ! -d $MK_VARDIR/cache ]; then mkdir -p $MK_VARDIR/cache ; fi
CACHEFILE="$MK_VARDIR/cache/$NAME.cache"
# Check if the creation of the cache takes suspiciously long and return
# nothing if the age (access time) of $CACHEFILE.new is twice the MAXAGE
local NOW=$(date +%s)
if [ -e "$CACHEFILE.new" ] ; then
local CF_ATIME=$(stat -c %X "$CACHEFILE.new")
if [ $((NOW - CF_ATIME)) -ge $((MAXAGE * 2)) ] ; then
# Kill the process still accessing that file in case
# it is still running. This avoids overlapping processes!
fuser -k -9 "$CACHEFILE.new" >/dev/null 2>&1
rm -f "$CACHEFILE.new"
return
fi
fi
# Check if cache file exists and is recent enough
if [ -s "$CACHEFILE" ] ; then
local MTIME=$(stat -c %Y "$CACHEFILE")
if [ $((NOW - MTIME)) -le $MAXAGE ] ; then local USE_CACHEFILE=1 ; fi
# Output the file in any case, even if it is
# outdated. The new file will not yet be available
cat "$CACHEFILE"
fi
# Cache file outdated and new job not yet running? Start it
if [ -z "$USE_CACHEFILE" -a ! -e "$CACHEFILE.new" ] ; then
echo "set -o noclobber ; exec > \"$CACHEFILE.new\" || exit 1 ; $CMDLINE && mv \"$CACHEFILE.new\" \"$CACHEFILE\" || rm -f \"$CACHEFILE\" \"$CACHEFILE.new\"" | nohup bash >/dev/null 2>&1 &
fi
}
# Make run_cached available for subshells (plugins, local checks, etc.)
export -f run_cached
echo '<<<check_mk>>>'
echo Version: 1.2.6b5
echo AgentOS: linux
echo AgentDirectory: $MK_CONFDIR
echo DataDirectory: $MK_VARDIR
echo SpoolDirectory: $SPOOLDIR
echo PluginsDirectory: $PLUGINSDIR
echo LocalDirectory: $LOCALDIR
# If we are called via xinetd, try to find only_from configuration
if [ -n "$REMOTE_HOST" ]
then
echo -n 'OnlyFrom: '
echo $(sed -n '/^service[[:space:]]*check_mk/,/}/s/^[[:space:]]*only_from[[:space:]]*=[[:space:]]*\(.*\)/\1/p' /etc/xinetd.d/* | head -n1)
fi
# Print out Partitions / Filesystems. (-P gives non-wrapped POSIXed output)
# Heads up: NFS-mounts are generally supressed to avoid agent hangs.
# If hard NFS mounts are configured or you have too large nfs retry/timeout
# settings, accessing those mounts from the agent would leave you with
# thousands of agent processes and, ultimately, a dead monitored system.
# These should generally be monitored on the NFS server, not on the clients.
echo '<<<df>>>'
# The exclusion list is getting a bit of a problem. -l should hide any remote FS but seems
# to be all but working.
excludefs="-x smbfs -x cifs -x iso9660 -x udf -x nfsv4 -x nfs -x mvfs -x zfs"
df -PTlk $excludefs | sed 1d
# df inodes information
echo '<<<df>>>'
echo '[df_inodes_start]'
df -PTli $excludefs | sed 1d
echo '[df_inodes_end]'
# Filesystem usage for ZFS
if type zfs > /dev/null 2>&1 ; then
echo '<<<zfsget>>>'
zfs get -Hp name,quota,used,avail,mountpoint,type -t filesystem,volume || \
zfs get -Hp name,quota,used,avail,mountpoint,type
echo '[df]'
df -PTlk -t zfs | sed 1d
fi
# Check NFS mounts by accessing them with stat -f (System
# call statfs()). If this lasts more then 2 seconds we
# consider it as hanging. We need waitmax.
if type waitmax >/dev/null
then
STAT_VERSION=$(stat --version | head -1 | cut -d" " -f4)
STAT_BROKE="5.3.0"
echo '<<<nfsmounts>>>'
sed -n '/ nfs4\? /s/[^ ]* \([^ ]*\) .*/\1/p' < /proc/mounts |
sed 's/\\040/ /g' |
while read MP
do
if [ $STAT_VERSION != $STAT_BROKE ]; then
waitmax -s 9 2 stat -f -c "$MP ok %b %f %a %s" "$MP" || \
echo "$MP hanging 0 0 0 0"
else
waitmax -s 9 2 stat -f -c "$MP ok %b %f %a %s" "$MP" && \
printf '\n'|| echo "$MP hanging 0 0 0 0"
fi
done
echo '<<<cifsmounts>>>'
sed -n '/ cifs\? /s/[^ ]* \([^ ]*\) .*/\1/p' < /proc/mounts |
sed 's/\\040/ /g' |
while read MP
do
if [ $STAT_VERSION != $STAT_BROKE ]; then
waitmax -s 9 2 stat -f -c "$MP ok %b %f %a %s" "$MP" || \
echo "$MP hanging 0 0 0 0"
else
waitmax -s 9 2 stat -f -c "$MP ok %b %f %a %s" "$MP" && \
printf '\n'|| echo "$MP hanging 0 0 0 0"
fi
done
fi
# Check mount options. Filesystems may switch to 'ro' in case
# of a read error.
echo '<<<mounts>>>'
grep ^/dev < /proc/mounts
# processes including username, without kernel processes
echo '<<<ps>>>'
ps ax -o user,vsz,rss,cputime,pid,command --columns 10000 | sed -e 1d -e 's/ *\([^ ]*\) *\([^ ]*\) *\([^ ]*\) *\([^ ]*\) *\([^ ]*\) */(\1,\2,\3,\4,\5) /'
# Memory usage
echo '<<<mem>>>'
egrep -v '^Swap:|^Mem:|total:' < /proc/meminfo
# Load and number of processes
echo '<<<cpu>>>'
echo "$(cat /proc/loadavg) $(grep -E '^CPU|^processor' < /proc/cpuinfo | wc -l)"
# Uptime
echo '<<<uptime>>>'
cat /proc/uptime
# New variant: Information about speed and state in one section
echo '<<<lnx_if:sep(58)>>>'
sed 1,2d /proc/net/dev
if type ethtool > /dev/null
then
for eth in $(sed -e 1,2d < /proc/net/dev | cut -d':' -f1 | sort)
do
echo "[$eth]"
ethtool $eth | egrep '(Speed|Duplex|Link detected|Auto-negotiation):'
echo -en "\tAddress: " ; cat /sys/class/net/$eth/address ; echo
done
fi
# Current state of bonding interfaces
if [ -e /proc/net/bonding ] ; then
echo '<<<lnx_bonding:sep(58)>>>'
pushd /proc/net/bonding > /dev/null ; head -v -n 1000 * ; popd
fi
# Same for Open vSwitch bonding
if type ovs-appctl > /dev/null ; then
echo '<<<ovs_bonding:sep(58)>>>'
for bond in $(ovs-appctl bond/list | sed -e 1d | cut -f2) ; do
echo "[$bond]"
ovs-appctl bond/show $bond
done
fi
# Number of TCP connections in the various states
echo '<<<tcp_conn_stats>>>'
# waitmax 10 netstat -nt | awk ' /^tcp/ { c[$6]++; } END { for (x in c) { print x, c[x]; } }'
# New implementation: netstat is very slow for large TCP tables
cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | awk ' /:/ { c[$4]++; } END { for (x in c) { print x, c[x]; } }'
# Linux Multipathing
if type multipath >/dev/null ; then
echo '<<<multipath>>>'
multipath -l
fi
# Performancecounter Platten
echo '<<<diskstat>>>'
date +%s
egrep ' (x?[shv]d[a-z]*|cciss/c[0-9]+d[0-9]+|emcpower[a-z]+|dm-[0-9]+|VxVM.*|mmcblk.*) ' < /proc/diskstats
if type dmsetup >/dev/null ; then
echo '[dmsetup_info]'
dmsetup info -c --noheadings --separator ' ' -o name,devno,vg_name,lv_name
fi
if [ -d /dev/vx/dsk ] ; then
echo '[vx_dsk]'
stat -c "%t %T %n" /dev/vx/dsk/*/*
fi
# Performancecounter Kernel
echo '<<<kernel>>>'
date +%s
cat /proc/vmstat /proc/stat
# Hardware sensors via IPMI (need ipmitool)
if type ipmitool > /dev/null
then
run_cached -s ipmi 300 "ipmitool sensor list | grep -v 'command failed' | sed -e 's/ *| */|/g' -e 's/ /_/g' -e 's/_*"'$'"//' -e 's/|/ /g' | egrep -v '^[^ ]+ na ' | grep -v ' discrete '"
fi
# IPMI data via ipmi-sensors (of freeipmi). Please make sure, that if you
# have installed freeipmi that IPMI is really support by your hardware.
if type ipmi-sensors >/dev/null
then
echo '<<<ipmi_sensors>>>'
# Newer ipmi-sensors version have new output format; Legacy format can be used
if ipmi-sensors --help | grep -q legacy-output; then
IPMI_FORMAT="--legacy-output"
else
IPMI_FORMAT=""
fi
# At least with ipmi-sensoirs 0.7.16 this group is Power_Unit instead of "Power Unit"
run_cached -s ipmi_sensors 300 "for class in Temperature Power_Unit Fan
do
ipmi-sensors $IPMI_FORMAT --sdr-cache-directory /var/cache -g "$class" | sed -e 's/ /_/g' -e 's/:_\?/ /g' -e 's@ \([^(]*\)_(\([^)]*\))@ \2_\1@'
# In case of a timeout immediately leave loop.
if [ $? = 255 ] ; then break ; fi
done"
fi
# RAID status of Linux software RAID
echo '<<<md>>>'
cat /proc/mdstat
# RAID status of Linux RAID via device mapper
if type dmraid >/dev/null && DMSTATUS=$(dmraid -r)
then
echo '<<<dmraid>>>'
# Output name and status
dmraid -s | grep -e ^name -e ^status
# Output disk names of the RAID disks
DISKS=$(echo "$DMSTATUS" | cut -f1 -d\:)
for disk in $DISKS ; do
device=$(cat /sys/block/$(basename $disk)/device/model )
status=$(echo "$DMSTATUS" | grep ^${disk})
echo "$status Model: $device"
done
fi
# RAID status of LSI controllers via cfggen
if type cfggen > /dev/null ; then
echo '<<<lsi>>>'
cfggen 0 DISPLAY | egrep '(Target ID|State|Volume ID|Status of volume)[[:space:]]*:' | sed -e 's/ *//g' -e 's/:/ /'
fi
# RAID status of LSI MegaRAID controller via MegaCli. You can download that tool from:
# http://www.lsi.com/downloads/Public/MegaRAID%20Common%20Files/8.02.16_MegaCLI.zip
if type MegaCli >/dev/null ; then
MegaCli_bin="MegaCli"
elif type MegaCli64 >/dev/null ; then
MegaCli_bin="MegaCli64"
elif type megacli >/dev/null ; then
MegaCli_bin="megacli"
else
MegaCli_bin="unknown"
fi
if [ "$MegaCli_bin" != "unknown" ]; then
echo '<<<megaraid_pdisks>>>'
for part in $($MegaCli_bin -EncInfo -aALL -NoLog < /dev/null \
| sed -rn 's/:/ /g; s/[[:space:]]+/ /g; s/^ //; s/ $//; s/Number of enclosures on adapter ([0-9]+).*/adapter \1/g; /^(Enclosure|Device ID|adapter) [0-9]+$/ p'); do
[ $part = adapter ] && echo ""
[ $part = 'Enclosure' ] && echo -ne "\ndev2enc"
echo -n " $part"
done
echo
$MegaCli_bin -PDList -aALL -NoLog < /dev/null | egrep 'Enclosure|Raw Size|Slot Number|Device Id|Firmware state|Inquiry|Adapter'
echo '<<<megaraid_ldisks>>>'
$MegaCli_bin -LDInfo -Lall -aALL -NoLog < /dev/null | egrep 'Size|State|Number|Adapter|Virtual'
echo '<<<megaraid_bbu>>>'
$MegaCli_bin -AdpBbuCmd -GetBbuStatus -aALL -NoLog < /dev/null | grep -v Exit
fi
# RAID status of 3WARE disk controller (by Radoslaw Bak)
if type tw_cli > /dev/null ; then
for C in $(tw_cli show | awk 'NR < 4 { next } { print $1 }'); do
echo '<<<3ware_info>>>'
tw_cli /$C show all | egrep 'Model =|Firmware|Serial'
echo '<<<3ware_disks>>>'
tw_cli /$C show drivestatus | egrep 'p[0-9]' | sed "s/^/$C\//"
echo '<<<3ware_units>>>'
tw_cli /$C show unitstatus | egrep 'u[0-9]' | sed "s/^/$C\//"
done
fi
# RAID controllers from areca (Taiwan)
# cli64 can be found at ftp://ftp.areca.com.tw/RaidCards/AP_Drivers/Linux/CLI/
if type cli64 >/dev/null ; then
run_cached -s arc_raid_status 300 "cli64 rsf info | tail -n +3 | head -n -2"
fi
# VirtualBox Guests. Section must always been output. Otherwise the
# check would not be executed in case no guest additions are installed.
# And that is something the check wants to detect
echo '<<<vbox_guest>>>'
if type VBoxControl >/dev/null 2>&1 ; then
VBoxControl -nologo guestproperty enumerate | cut -d, -f1,2
[ ${PIPESTATUS[0]} = 0 ] || echo "ERROR"
fi
# OpenVPN Clients. Currently we assume that the configuration # is in
# /etc/openvpn. We might find a safer way to find the configuration later.
if [ -e /etc/openvpn/openvpn-status.log ] ; then
echo '<<<openvpn_clients:sep(44)>>>'
sed -n -e '/CLIENT LIST/,/ROUTING TABLE/p' < /etc/openvpn/openvpn-status.log | sed -e 1,3d -e '$d'
fi
# Time synchronization with NTP
if type ntpq > /dev/null 2>&1 ; then
# remove heading, make first column space separated
run_cached -s ntp 30 "waitmax 5 ntpq -np | sed -e 1,2d -e 's/^\(.\)/\1 /' -e 's/^ /%/'"
fi
# Time synchronization with Chrony
if type chronyc > /dev/null 2>&1 ; then
# Force successful exit code. Otherwise section will be missing if daemon not running
run_cached -s chrony 30 "waitmax 5 chronyc tracking || true"
fi
if type nvidia-settings >/dev/null && [ -S /tmp/.X11-unix/X0 ]
then
echo '<<<nvidia>>>'
for var in GPUErrors GPUCoreTemp
do
DISPLAY=:0 waitmax 2 nvidia-settings -t -q $var | sed "s/^/$var: /"
done
fi
if [ -e /proc/drbd ]; then
echo '<<<drbd>>>'
cat /proc/drbd
fi
# Status of CUPS printer queues
if type lpstat > /dev/null 2>&1; then
if pgrep cups > /dev/null 2>&1; then
echo '<<<cups_queues>>>'
CPRINTCONF=/etc/cups/printers.conf
if [ -r "$CPRINTCONF" ] ; then
LOCAL_PRINTERS=$(grep -E "<(Default)?Printer .*>" $CPRINTCONF | awk '{print $2}' | sed -e 's/>//')
lpstat -p | while read LINE
do
PRINTER=$(echo $LINE | awk '{print $2}')
if echo "$LOCAL_PRINTERS" | grep -q "$PRINTER"; then
echo $LINE
fi
done
echo '---'
lpstat -o | while read LINE
do
PRINTER=${LINE%%-*}
if echo "$LOCAL_PRINTERS" | grep -q "$PRINTER"; then
echo $LINE
fi
done
else
lpstat -p
echo '---'
lpstat -o | sort
fi
fi
fi
# Heartbeat monitoring
# Different handling for heartbeat clusters with and without CRM
# for the resource state
if [ -S /var/run/heartbeat/crm/cib_ro -o -S /var/run/crm/cib_ro ] || pgrep crmd > /dev/null 2>&1; then
echo '<<<heartbeat_crm>>>'
crm_mon -1 -r | grep -v ^$ | sed 's/^ //; /^\sResource Group:/,$ s/^\s//; s/^\s/_/g'
fi
if type cl_status > /dev/null 2>&1; then
echo '<<<heartbeat_rscstatus>>>'
cl_status rscstatus
echo '<<<heartbeat_nodes>>>'
for NODE in $(cl_status listnodes); do
if [ $NODE != $(echo $HOSTNAME | tr 'A-Z' 'a-z') ]; then
STATUS=$(cl_status nodestatus $NODE)
echo -n "$NODE $STATUS"
for LINK in $(cl_status listhblinks $NODE 2>/dev/null); do
echo -n " $LINK $(cl_status hblinkstatus $NODE $LINK)"
done
echo
fi
done
fi
# Postfix mailqueue monitoring
#
# Only handle mailq when postfix user is present. The mailq command is also
# available when postfix is not installed. But it produces different outputs
# which are not handled by the check at the moment. So try to filter out the
# systems not using postfix by searching for the postfix user.a
#
# Cannot take the whole outout. This could produce several MB of agent output
# on blocking queues.
# Only handle the last 6 lines (includes the summary line at the bottom and
# the last message in the queue. The last message is not used at the moment
# but it could be used to get the timestamp of the last message.
if type postconf >/dev/null ; then
echo '<<<postfix_mailq>>>'
postfix_queue_dir=$(postconf -h queue_directory)
postfix_count=$(find $postfix_queue_dir/deferred -type f | wc -l)
postfix_size=$(du -ks $postfix_queue_dir/deferred | awk '{print $1 }')
if [ $postfix_count -gt 0 ]
then
echo -- $postfix_size Kbytes in $postfix_count Requests.
else
echo Mail queue is empty
fi
elif [ -x /usr/sbin/ssmtp ] ; then
echo '<<<postfix_mailq>>>'
mailq 2>&1 | sed 's/^[^:]*: \(.*\)/\1/' | tail -n 6
fi
#Check status of qmail mailqueue
if type qmail-qstat >/dev/null
then
echo "<<<qmail_stats>>>"
qmail-qstat
fi
# Check status of OMD sites
if type omd >/dev/null
then
run_cached -s omd_status 60 "omd status --bare --auto"
fi
# Welcome the ZFS check on Linux
# We do not endorse running ZFS on linux if your vendor doesnt support it ;)
# check zpool status
if type zpool >/dev/null; then
echo "<<<zpool_status>>>"
zpool status -x
fi
# Fileinfo-Check: put patterns for files into /etc/check_mk/fileinfo.cfg
if [ -r "$MK_CONFDIR/fileinfo.cfg" ] ; then
echo '<<<fileinfo:sep(124)>>>'
date +%s
stat -c "%n|%s|%Y" $(cat "$MK_CONFDIR/fileinfo.cfg")
fi
# Get stats about OMD monitoring cores running on this machine.
# Since cd is a shell builtin the check does not affect the performance
# on non-OMD machines.
if cd /omd/sites
then
echo '<<<livestatus_status:sep(59)>>>'
for site in *
do
if [ -S "/omd/sites/$site/tmp/run/live" ] ; then
echo "[$site]"
echo -e "GET status" | waitmax 3 /omd/sites/$site/bin/unixcat /omd/sites/$site/tmp/run/live
fi
done
fi
# Get statistics about monitored jobs. Below the job directory there
# is a sub directory per user that ran a job. That directory must be
# owned by the user so that a symlink or hardlink attack for reading
# arbitrary files can be avoided.
if pushd $MK_VARDIR/job >/dev/null; then
echo '<<<job>>>'
for username in *
do
if [ -d "$username" ] && cd "$username" ; then
su "$username" -c "head -n -0 -v *"
cd ..
fi
done
popd > /dev/null
fi
# Gather thermal information provided e.g. by acpi
# At the moment only supporting thermal sensors
if ls /sys/class/thermal/thermal_zone* >/dev/null 2>&1; then
echo '<<<lnx_thermal>>>'
for F in /sys/class/thermal/thermal_zone*; do
echo -n "${F##*/} "
if [ ! -e $F/mode ] ; then echo -n "- " ; fi
cat $F/{mode,type,temp,trip_point_*} | tr \\n " "
echo
done
fi
# Libelle Business Shadow
if type trd >/dev/null; then
echo "<<<libelle_business_shadow:sep(58)>>>"
trd -s
fi
# MK's Remote Plugin Executor
if [ -e "$MK_CONFDIR/mrpe.cfg" ]
then
echo '<<<mrpe>>>'
grep -Ev '^[[:space:]]*($|#)' "$MK_CONFDIR/mrpe.cfg" | \
while read descr cmdline
do
PLUGIN=${cmdline%% *}
OUTPUT=$(eval "$cmdline")
echo -n "(${PLUGIN##*/}) $descr $? $OUTPUT" | tr \\n \\1
echo
done
fi
# Local checks
echo '<<<local>>>'
if cd $LOCALDIR ; then
for skript in $(ls) ; do
if [ -f "$skript" -a -x "$skript" ] ; then
./$skript
fi
done
# Call some plugins only every X'th minute
for skript in [1-9]*/* ; do
if [ -x "$skript" ] ; then
run_cached local_${skript//\//\\} ${skript%/*} "$skript"
fi
done
fi
# Plugins
if cd $PLUGINSDIR ; then
for skript in $(ls) ; do
if [ -f "$skript" -a -x "$skript" ] ; then
./$skript
fi
done
# Call some plugins only every Xth minute
for skript in [1-9]*/* ; do
if [ -x "$skript" ] ; then
run_cached plugins_${skript//\//\\} ${skript%/*} "$skript"
fi
done
fi
# Agent output snippets created by cronjobs, etc.
if [ -d "$SPOOLDIR" ]
then
pushd "$SPOOLDIR" > /dev/null
now=$(date +%s)
for file in *
do
# output every file in this directory. If the file is prefixed
# with a number, then that number is the maximum age of the
# file in seconds. If the file is older than that, it is ignored.
maxage=""
part="$file"
# Each away all digits from the front of the filename and
# collect them in the variable maxage.
while [ "${part/#[0-9]/}" != "$part" ]
do
maxage=$maxage${part:0:1}
part=${part:1}
done
# If there is at least one digit, than we honor that.
if [ "$maxage" ] ; then
mtime=$(stat -c %Y "$file")
if [ $((now - mtime)) -gt $maxage ] ; then
continue
fi
fi
# Output the file
cat "$file"
done
popd > /dev/null
fi
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
# Detects which OS and if it is Linux then it will detect which Linux Distribution.
OS=`uname -s`
REV=`uname -r`
MACH=`uname -m`
if [ "${OS}" = "SunOS" ] ; then
OS=Solaris
ARCH=`uname -p`
OSSTR="${OS} ${REV}(${ARCH} `uname -v`)"
elif [ "${OS}" = "AIX" ] ; then
OSSTR="${OS} `oslevel` (`oslevel -r`)"
elif [ "${OS}" = "Linux" ] ; then
KERNEL=`uname -r`
if [ -f /etc/fedora-release ]; then
DIST=$(cat /etc/fedora-release | awk '{print $1}')
REV=`cat /etc/fedora-release | sed s/.*release\ // | sed s/\ .*//`
elif [ -f /etc/redhat-release ] ; then
DIST=$(cat /etc/redhat-release | awk '{print $1}')
if [ "${DIST}" = "CentOS" ]; then
DIST="CentOS"
elif [ "${DIST}" = "Mandriva" ]; then
DIST="Mandriva"
PSEUDONAME=`cat /etc/mandriva-release | sed s/.*\(// | sed s/\)//`
REV=`cat /etc/mandriva-release | sed s/.*release\ // | sed s/\ .*//`
elif [ -f /etc/oracle-release ]; then
DIST="Oracle"
else
DIST="RedHat"
fi
PSEUDONAME=`cat /etc/redhat-release | sed s/.*\(// | sed s/\)//`
REV=`cat /etc/redhat-release | sed s/.*release\ // | sed s/\ .*//`
elif [ -f /etc/mandrake-release ] ; then
DIST='Mandrake'
PSEUDONAME=`cat /etc/mandrake-release | sed s/.*\(// | sed s/\)//`
REV=`cat /etc/mandrake-release | sed s/.*release\ // | sed s/\ .*//`
elif [ -f /etc/devuan_version ] ; then
DIST="Devuan `cat /etc/devuan_version`"
REV=""
elif [ -f /etc/debian_version ] ; then
DIST="Debian `cat /etc/debian_version`"
REV=""
ID=`lsb_release -i | awk -F ':' '{print $2}' | sed 's/ //g'`
if [ "${ID}" = "Raspbian" ] ; then
DIST="Raspbian `cat /etc/debian_version`"
fi
elif [ -f /etc/gentoo-release ] ; then
DIST="Gentoo"
REV=$(tr -d '[[:alpha:]]' </etc/gentoo-release | tr -d " ")
elif [ -f /etc/arch-release ] ; then
DIST="Arch Linux"
REV="" # Omit version since Arch Linux uses rolling releases
IGNORE_LSB=1 # /etc/lsb-release would overwrite $REV with "rolling"
elif [ -f /etc/os-release ] ; then
DIST=$(grep '^NAME=' /etc/os-release | cut -d= -f2- | tr -d '"')
REV=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2- | tr -d '"')
elif [ -f /etc/openwrt_version ] ; then
DIST="OpenWrt"
REV=$(cat /etc/openwrt_version)
elif [ -f /etc/pld-release ] ; then
DIST=$(cat /etc/pld-release)
REV=""
elif [ -f /etc/SuSE-release ] ; then
DIST=$(echo SLES $(grep VERSION /etc/SuSE-release | cut -d = -f 2 | tr -d " "))
REV=$(echo SP$(grep PATCHLEVEL /etc/SuSE-release | cut -d = -f 2 | tr -d " "))
fi
if [ -f /etc/lsb-release -a "${IGNORE_LSB}" != 1 ] ; then
LSB_DIST=$(lsb_release -si)
LSB_REV=$(lsb_release -sr)
if [ "$LSB_DIST" != "" ] ; then
DIST=$LSB_DIST
fi
if [ "$LSB_REV" != "" ] ; then
REV=$LSB_REV
fi
fi
if [ "`uname -a | awk '{print $(NF)}'`" = "DD-WRT" ] ; then
DIST="dd-wrt"
fi
if [ -n "${REV}" ]
then
OSSTR="${DIST} ${REV}"
else
OSSTR="${DIST}"
fi
elif [ "${OS}" = "Darwin" ] ; then
if [ -f /usr/bin/sw_vers ] ; then
OSSTR=`/usr/bin/sw_vers|grep -v Build|sed 's/^.*:.//'| tr "\n" ' '`
fi
elif [ "${OS}" = "FreeBSD" ] ; then
OSSTR=`/usr/bin/uname -mior`
fi
echo ${OSSTR}
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
echo '<<<dmi>>>'
# requires dmidecode
for FIELD in bios-vendor bios-version bios-release-date system-manufacturer system-product-name system-version system-serial-number system-uuid baseboard-manufacturer baseboard-product-name baseboard-version baseboard-serial-number baseboard-asset-tag chassis-manufacturer chassis-type chassis-version chassis-serial-number chassis-asset-tag processor-family processor-manufacturer processor-version processor-frequency
do
echo $FIELD="$(dmidecode -s $FIELD | grep -v '^#')"
done
@@ -0,0 +1,22 @@
#!/bin/bash
# Cache the file for 30 minutes
# If you want to override this, put the command in cron.
# We cache because it is a 1sec delay, which is painful for the poller
if [ -x /usr/bin/dpkg-query ]; then
DATE=$(date +%s)
FILE=/var/cache/librenms/agent-local-dpkg
[ -d /var/cache/librenms ] || mkdir -p /var/cache/librenms
if [ ! -e $FILE ]; then
dpkg-query -W --showformat='${Status} ${Package} ${Version} ${Architecture} ${Installed-Size}\n'|grep " installed "|cut -d\ -f4- > $FILE
fi
FILEMTIME=$(stat -c %Y $FILE)
FILEAGE=$(($DATE-$FILEMTIME))
if [ $FILEAGE -gt 1800 ]; then
dpkg-query -W --showformat='${Status} ${Package} ${Version} ${Architecture} ${Installed-Size}\n'|grep " installed "|cut -d\ -f4- > $FILE
fi
echo "<<<dpkg>>>"
cat $FILE
fi
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,34 @@
#!/bin/sh
# Please make sure the paths below are correct.
# Alternatively you can put them in $0.conf, meaning if you've named
# this script ntp-client then it must go in ntp-client.conf .
#
# NTPQV output version of "ntpq -c rv"
# Version 4 is the most common and up to date version.
#
# If you are unsure, which to set, run this script and make sure that
# the JSON output variables match that in "ntpq -c rv".
#
################################################################
# Don't change anything unless you know what are you doing #
################################################################
BIN_NTPQ='/usr/bin/env ntpq'
BIN_GREP='/usr/bin/env grep'
BIN_AWK='/usr/bin/env awk'
CONFIG=$0".conf"
if [ -f "$CONFIG" ]; then
# shellcheck disable=SC1090
. "$CONFIG"
fi
NTP_OFFSET=$($BIN_NTPQ -c rv | $BIN_GREP "offset" | $BIN_AWK -Foffset= '{print $2}' | $BIN_AWK -F, '{print $1}')
NTP_FREQUENCY=$($BIN_NTPQ -c rv | $BIN_GREP "frequency" | $BIN_AWK -Ffrequency= '{print $2}' | $BIN_AWK -F, '{print $1}')
NTP_SYS_JITTER=$($BIN_NTPQ -c rv | $BIN_GREP "sys_jitter" | $BIN_AWK -Fsys_jitter= '{print $2}' | $BIN_AWK -F, '{print $1}')
NTP_CLK_JITTER=$($BIN_NTPQ -c rv | $BIN_GREP "clk_jitter" | $BIN_AWK -Fclk_jitter= '{print $2}' | $BIN_AWK -F, '{print $1}')
NTP_WANDER=$($BIN_NTPQ -c rv | $BIN_GREP "clk_wander" | $BIN_AWK -Fclk_wander= '{print $2}' | $BIN_AWK -F, '{print $1}')
NTP_VERSION=$($BIN_NTPQ -c rv | $BIN_GREP "version" | $BIN_AWK -F'ntpd ' '{print $2}' | $BIN_AWK -F. '{print $1}')
echo '{"data":{"offset":"'"$NTP_OFFSET"'","frequency":"'"$NTP_FREQUENCY"'","sys_jitter":"'"$NTP_SYS_JITTER"'","clk_jitter":"'"$NTP_CLK_JITTER"'","clk_wander":"'"$NTP_WANDER"'"},"version":"'"$NTP_VERSION"'","error":"0","errorString":""}'
exit 0
@@ -0,0 +1,89 @@
#!/bin/sh
# Please make sure the paths below are correct.
# Alternatively you can put them in $0.conf, meaning if you've named
# this script ntp-client.sh then it must go in ntp-client.sh.conf .
#
# NTPQV output version of "ntpq -c rv"
# p1 DD-WRT and some other outdated linux distros
# p11 FreeBSD 11 and any linux distro that is up to date
#
# If you are unsure, which to set, run this script and make sure that
# the JSON output variables match that in "ntpq -c rv".
#
BIN_NTPD='/usr/bin/env ntpd'
BIN_NTPQ='/usr/bin/env ntpq'
BIN_NTPDC='/usr/bin/env ntpdc'
BIN_GREP='/usr/bin/env grep'
BIN_TR='/usr/bin/env tr'
BIN_CUT='/usr/bin/env cut'
BIN_SED="/usr/bin/env sed"
BIN_AWK='/usr/bin/env awk'
NTPQV="p11"
################################################################
# Don't change anything unless you know what are you doing #
################################################################
CONFIG=$0".conf"
if [ -f $CONFIG ]; then
. $CONFIG
fi
VERSION=1
STRATUM=`$BIN_NTPQ -c rv | $BIN_GREP -Eow "stratum=[0-9]+" | $BIN_CUT -d "=" -f 2`
# parse the ntpq info that requires version specific info
NTPQ_RAW=`$BIN_NTPQ -c rv | $BIN_GREP jitter | $BIN_SED 's/[[:alpha:]=,_]/ /g'`
if [ $NTPQV = "p11" ]; then
OFFSET=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $3}'`
FREQUENCY=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $4}'`
SYS_JITTER=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $5}'`
CLK_JITTER=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $6}'`
CLK_WANDER=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $7}'`
fi
if [ $NTPQV = "p1" ]; then
OFFSET=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $2}'`
FREQUENCY=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $3}'`
SYS_JITTER=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $4}'`
CLK_JITTER=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $5}'`
CLK_WANDER=`echo $NTPQ_RAW | $BIN_AWK -F ' ' '{print $6}'`
fi
VER=`$BIN_NTPD --version`
if [ "$VER" = '4.2.6p5' ]; then
USECMD=`echo $BIN_NTPDC -c iostats`
else
USECMD=`echo $BIN_NTPQ -c iostats localhost`
fi
CMD2=`$USECMD | $BIN_TR -d ' ' | $BIN_CUT -d : -f 2 | $BIN_TR '\n' ' '`
TIMESINCERESET=`echo $CMD2 | $BIN_AWK -F ' ' '{print $1}'`
RECEIVEDBUFFERS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $2}'`
FREERECEIVEBUFFERS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $3}'`
USEDRECEIVEBUFFERS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $4}'`
LOWWATERREFILLS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $5}'`
DROPPEDPACKETS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $6}'`
IGNOREDPACKETS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $7}'`
RECEIVEDPACKETS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $8}'`
PACKETSSENT=`echo $CMD2 | $BIN_AWK -F ' ' '{print $9}'`
PACKETSENDFAILURES=`echo $CMD2 | $BIN_AWK -F ' ' '{print $10}'`
INPUTWAKEUPS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $11}'`
USEFULINPUTWAKEUPS=`echo $CMD2 | $BIN_AWK -F ' ' '{print $12}'`
echo '{"data":{"offset":"'$OFFSET\
'","frequency":"'$FREQUENCY\
'","sys_jitter":"'$SYS_JITTER\
'","clk_jitter":"'$CLK_JITTER\
'","clk_wander":"'$CLK_WANDER\
'","stratum":"'$STRATUM\
'","time_since_reset":"'$TIMESINCERESET\
'","receive_buffers":"'$RECEIVEDBUFFERS\
'","free_receive_buffers":"'$FREERECEIVEBUFFERS\
'","used_receive_buffers":"'$USEDRECEIVEBUFFERS\
'","low_water_refills":"'$LOWWATERREFILLS\
'","dropped_packets":"'$DROPPEDPACKETS\
'","ignored_packets":"'$IGNOREDPACKETS\
'","received_packets":"'$RECEIVEDPACKETS\
'","packets_sent":"'$PACKETSSENT\
'","packet_send_failures":"'$PACKETSENDFAILURES\
'","input_wakeups":"'$PACKETSENDFAILURES\
'","useful_input_wakeups":"'$USEFULINPUTWAKEUPS\
'"},"error":"0","errorString":"","version":"'$VERSION'"}'
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
################################################################
# copy this script to /etc/snmp/ and make it executable: #
# chmod +x /etc/snmp/os-updates.sh #
# ------------------------------------------------------------ #
# edit your snmpd.conf and include: #
# extend osupdate /opt/os-updates.sh #
#--------------------------------------------------------------#
# restart snmpd and activate the app for desired host #
#--------------------------------------------------------------#
# please make sure you have the path/binaries below #
################################################################
BIN_WC='/usr/bin/wc'
BIN_GREP='/bin/grep'
CMD_GREP='-c'
CMD_WC='-l'
BIN_ZYPPER='/usr/bin/zypper'
CMD_ZYPPER='-q lu'
BIN_YUM='/usr/bin/yum'
CMD_YUM='-q check-update'
BIN_DNF='/usr/bin/dnf'
CMD_DNF='-q check-update'
BIN_APT='/usr/bin/apt-get'
CMD_APT='-qq -s upgrade'
BIN_PACMAN='/usr/bin/pacman'
CMD_PACMAN='-Sup'
################################################################
# Don't change anything unless you know what are you doing #
################################################################
if [ -f $BIN_ZYPPER ]; then
# OpenSUSE
UPDATES=`$BIN_ZYPPER $CMD_ZYPPER | $BIN_WC $CMD_WC`
if [ $UPDATES -ge 2 ]; then
echo $(($UPDATES-2));
else
echo "0";
fi
elif [ -f $BIN_DNF ]; then
# Fedora
UPDATES=`$BIN_DNF $CMD_DNF | $BIN_WC $CMD_WC`
if [ $UPDATES -ge 1 ]; then
echo $(($UPDATES-1));
else
echo "0";
fi
elif [ -f $BIN_PACMAN ]; then
# Arch
UPDATES=`$BIN_PACMAN $CMD_PACMAN | $BIN_WC $CMD_WC`
if [ $UPDATES -ge 1 ]; then
echo $(($UPDATES-1));
else
echo "0";
fi
elif [ -f $BIN_YUM ]; then
# CentOS / Redhat
UPDATES=`$BIN_YUM $CMD_YUM | $BIN_WC $CMD_WC`
if [ $UPDATES -ge 1 ]; then
echo $(($UPDATES-1));
else
echo "0";
fi
elif [ -f $BIN_APT ]; then
# Debian / Devuan / Ubuntu
UPDATES=`$BIN_APT $CMD_APT | $BIN_GREP $CMD_GREP 'Inst'`
if [ $UPDATES -ge 1 ]; then
echo $UPDATES;
else
echo "0";
fi
else
echo "0";
fi
@@ -0,0 +1,13 @@
#!/bin/bash
#Written by Valec 2006. Steal and share.
#Get postfix queue lengths
#extend mailq /opt/observer/scripts/getmailq.sh
QUEUES="incoming active deferred hold"
for i in $QUEUES; do
COUNT=$(qshape "$i" | grep TOTAL | awk '{print $2}')
printf "$COUNT\n"
done
@@ -0,0 +1,548 @@
#!/usr/bin/env perl
# add this to your snmpd.conf file as below
# extend postfixdetailed /etc/snmp/postfixdetailed
# The cache file to use.
my $cache='/var/cache/postfixdetailed';
# the location of pflogsumm
my $pflogsumm='/usr/bin/env pflogsumm';
#totals
# 847 received = received
# 852 delivered = delivered
# 0 forwarded = forwarded
# 3 deferred (67 deferrals)= deferred
# 0 bounced = bounced
# 593 rejected (41%) = rejected
# 0 reject warnings = rejectw
# 0 held = held
# 0 discarded (0%) = discarded
# 16899k bytes received = bytesr
# 18009k bytes delivered = bytesd
# 415 senders = senders
# 266 sending hosts/domains = sendinghd
# 15 recipients = recipients
# 9 recipient hosts/domains = recipienthd
######message deferral detail
#Connection refused = deferralcr
#Host is down = deferralhid
########message reject detail
#Client host rejected = chr
#Helo command rejected: need fully-qualified hostname = hcrnfqh
#Sender address rejected: Domain not found = sardnf
#Sender address rejected: not owned by user = sarnobu
#blocked using = bu
#Recipient address rejected: User unknown = raruu
#Helo command rejected: Invalid name = hcrin
#Sender address rejected: need fully-qualified address = sarnfqa
#Recipient address rejected: Domain not found = rardnf
#Recipient address rejected: need fully-qualified address = rarnfqa
#Improper use of SMTP command pipelining = iuscp
#Message size exceeds fixed limit = msefl
#Server configuration error = sce
#Server configuration problem = scp
#unknown reject reason = urr
my $old='';
#reads in the old data if it exists
if ( -f $cache ){
open(my $fh, "<", $cache) or die "Can't open '".$cache."'";
# if this is over 2048, something is most likely wrong
read($fh , $old , 2048);
close($fh);
}
my ( $received,
$delivered,
$forwarded,
$deferred,
$bounced,
$rejected,
$rejectw,
$held,
$discarded,
$bytesr,
$bytesd,
$senders,
$sendinghd,
$recipients,
$recipienthd,
$deferralcr,
$deferralhid,
$chr,
$hcrnfqh,
$sardnf,
$sarnobu,
$bu,
$raruu,
$hcrin,
$sarnfqa,
$rardnf,
$rarnfqa,
$iuscp,
$sce,
$scp,
$urr,
$msefl) = split ( /\n/, $old );
if ( ! defined( $received ) ){ $received=0; }
if ( ! defined( $delivered ) ){ $delivered=0; }
if ( ! defined( $forwarded ) ){ $forwarded=0; }
if ( ! defined( $deferred ) ){ $deferred=0; }
if ( ! defined( $bounced ) ){ $bounced=0; }
if ( ! defined( $rejected ) ){ $rejected=0; }
if ( ! defined( $rejectw ) ){ $rejectw=0; }
if ( ! defined( $held ) ){ $held=0; }
if ( ! defined( $discarded ) ){ $discarded=0; }
if ( ! defined( $bytesr ) ){ $bytesr=0; }
if ( ! defined( $bytesd ) ){ $bytesd=0; }
if ( ! defined( $senders ) ){ $senders=0; }
if ( ! defined( $sendinghd ) ){ $sendinghd=0; }
if ( ! defined( $recipients ) ){ $recipients=0; }
if ( ! defined( $recipienthd ) ){ $recipienthd=0; }
if ( ! defined( $deferralcr ) ){ $deferralcr=0; }
if ( ! defined( $deferralhid ) ){ $deferralhid=0; }
if ( ! defined( $chr ) ){ $chr=0; }
if ( ! defined( $hcrnfqh ) ){ $hcrnfqh=0; }
if ( ! defined( $sardnf ) ){ $sardnf=0; }
if ( ! defined( $sarnobu ) ){ $sarnobu=0; }
if ( ! defined( $bu ) ){ $bu=0; }
if ( ! defined( $raruu ) ){ $raruu=0; }
if ( ! defined( $hcrin ) ){ $hcrin=0; }
if ( ! defined( $sarnfqa ) ){ $sarnfqa=0; }
if ( ! defined( $rardnf ) ){ $rardnf=0; }
if ( ! defined( $rarnfqa ) ){ $rarnfqa=0; }
if ( ! defined( $iuscp ) ){ $iuscp=0; }
if ( ! defined( $msefl ) ){ $msefl=0; }
if ( ! defined( $sce ) ){ $sce=0; }
if ( ! defined( $scp ) ){ $scp=0; }
if ( ! defined( $urr ) ){ $urr=0; }
#init current variables
my $receivedC=0;
my $deliveredC=0;
my $forwardedC=0;
my $deferredC=0;
my $bouncedC=0;
my $rejectedC=0;
my $rejectwC=0;
my $heldC=0;
my $discardedC=0;
my $bytesrC=0;
my $bytesdC=0;
my $sendersC=0;
my $sendinghdC=0;
my $recipientsC=0;
my $recipienthdC=0;
my $deferralcrC=0;
my $deferralhidC=0;
my $hcrnfqhC=0;
my $sardnfC=0;
my $sarnobuC=0;
my $buC=0;
my $raruuC=0;
my $hcrinC=0;
my $sarnfqaC=0;
my $rardnfC=0;
my $rarnfqaC=0;
my $iuscpC=0;
my $mseflC=0;
my $sceC=0;
my $scpC=0;
my $urrC=0;
sub newValue{
my $old=$_[0];
my $new=$_[1];
#if new is undefined, just default to 0... this should never happen
if ( !defined( $new ) ){
warn('New not defined');
return 0;
}
#sets it to 0 if old is not defined
if ( !defined( $old ) ){
warn('Old not defined');
$old=0;
}
#make sure they are both numberic and if not set to zero
if( $old !~ /^[0123456789]*$/ ){
warn('Old not numeric');
$old=0;
}
if( $new !~ /^[0123456789]*$/ ){
warn('New not numeric');
$new=0;
}
#log rotation happened
if ( $old > $new ){
return $new;
};
return $new - $old;
}
my $output=`$pflogsumm /var/log/maillog`;
#holds RBL values till the end when it is compared to the old one
my $buNew=0;
#holds client host rejected values till the end when it is compared to the old one
my $chrNew=0;
# holds recipient address rejected values till the end when it is compared to the old one
my $raruuNew=0;
#holds the current values for checking later
my $current='';
my @outputA=split( /\n/, $output );
my $int=0;
while ( defined( $outputA[$int] ) ){
my $line=$outputA[$int];
$line=~s/^ *//;
$line=~s/ +/ /g;
$line=~s/\)$//;
my $handled=0;
#received line
if ( ( $line =~ /[0123456789] received$/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$receivedC=$line;
$received=newValue( $received, $line );
$handled=1;
}
#delivered line
if ( ( $line =~ /[0123456789] delivered$/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$deliveredC=$line;
$delivered=newValue( $delivered, $line );
$handled=1;
}
#forward line
if ( ( $line =~ /[0123456789] forwarded$/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$forwardedC=$line;
$forwarded=newValue( $forwarded, $line );
$handled=1;
}
#defereed line
if ( ( $line =~ /[0123456789] deferred \(/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$deferredC=$line;
$deferred=newValue( $deferred, $line );
$handled=1;
}
#bounced line
if ( ( $line =~ /[0123456789] bounced$/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$bouncedC=$line;
$bounced=newValue( $bounced, $line );
$handled=1;
}
#rejected line
if ( ( $line =~ /[0123456789] rejected \(/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$rejectedC=$line;
$rejected=newValue( $rejected, $line );
$handled=1;
}
#reject warning line
if ( ( $line =~ /[0123456789] reject warnings/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$rejectwC=$line;
$rejectw=newValue( $rejectw, $line );
$handled=1;
}
#held line
if ( ( $line =~ /[0123456789] held$/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$heldC=$line;
$held=newValue( $held, $line );
$handled=1;
}
#discarded line
if ( ( $line =~ /[0123456789] discarded \(/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$discardedC=$line;
$discarded=newValue( $discarded, $line );
$handled=1;
}
#bytes received line
if ( ( $line =~ /[0123456789kM] bytes received$/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$line=~s/k/000/;
$line=~s/M/000000/;
$bytesrC=$line;
$bytesr=newValue( $bytesr, $line );
$handled=1;
}
#bytes delivered line
if ( ( $line =~ /[0123456789kM] bytes delivered$/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$line=~s/k/000/;
$line=~s/M/000000/;
$bytesdC=$line;
$bytesd=newValue( $bytesd, $line );
$handled=1;
}
#senders line
if ( ( $line =~ /[0123456789] senders$/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$sendersC=$line;
$senders=newValue( $senders, $line );
$handled=1;
}
#sendering hosts/domains line
if ( ( $line =~ /[0123456789] sending hosts\/domains$/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$sendinghdC=$line;
$sendinghd=newValue( $sendinghd, $line );
$handled=1;
}
#recipients line
if ( ( $line =~ /[0123456789] recipients$/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$recipientsC=$line;
$recipients=newValue( $recipients, $line );
$handled=1;
}
#recipients line
if ( ( $line =~ /[0123456789] recipient hosts\/domains$/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$recipienthdC=$line;
$recipienthd=newValue( $recipienthd, $line );
$handled=1;
}
# deferrals connectios refused
if ( ( $line =~ /[0123456789] 25\: Connection refused$/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$deferralcrC=$line;
$deferralcr=newValue( $deferralcr, $line );
$handled=1;
}
# deferrals Host is down
if ( ( $line =~ /Host is down$/ ) && ( ! $handled ) ){
$line=~s/ .*//;
$deferralcrC=$line;
$deferralhidC=$line;
$deferralhid=newValue( $deferralhid, $line );
$handled=1;
}
# Client host rejected
if ( ( $line =~ /Client host rejected/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$chrNew=$chrNew + $line;
$handled=1;
}
#Helo command rejected: need fully-qualified hostname
if ( ( $line =~ /Helo command rejected\: need fully\-qualified hostname/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$hcrnfqhC=$line;
$hcrnfqh=newValue( $hcrnfqh, $line );
$handled=1;
}
#Sender address rejected: Domain not found
if ( ( $line =~ /Sender address rejected\: Domain not found/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$sardnfC=$line;
$sardnf=newValue( $sardnf, $line );
$handled=1;
}
#Sender address rejected: not owned by user
if ( ( $line =~ /Sender address rejected\: not owned by user/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$sarnobuC=$line;
$sarnobu=newValue( $sarnobu, $line );
$handled=1;
}
#blocked using
# These lines are RBLs so there will be more than one.
# Use $buNew to add them all up.
if ( ( $line =~ /blocked using/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$buNew=$buNew + $line;
$handled=1;
}
#Recipient address rejected: User unknown
if ( ( $line =~ /Recipient address rejected\: User unknown/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$raruuNew=$raruuNew + $line;
$handled=1;
}
#Helo command rejected: Invalid name
if ( ( $line =~ /Helo command rejected\: Invalid name/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$hcrinC=$line;
$hcrin=newValue( $hcrin, $line );
}
#Sender address rejected: need fully-qualified address
if ( ( $line =~ /Sender address rejected\: need fully-qualified address/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$sarnfqaC=$line;
$sarnfqa=newValue( $sarnfqa, $line );
}
#Recipient address rejected: Domain not found
if ( ( $line =~ /Recipient address rejected\: Domain not found/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$rardnfC=$line;
$rardnf=newValue( $rardnf, $line );
}
#Improper use of SMTP command pipelining
if ( ( $line =~ /Improper use of SMTP command pipelining/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$iuscpC=$line;
$iuscp=newValue( $iuscp, $line );
}
#Message size exceeds fixed limit
if ( ( $line =~ /Message size exceeds fixed limit/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$mseflC=$line;
$msefl=newValue( $msefl, $line );
}
#Server configuration error
if ( ( $line =~ /Server configuration error/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$sceC=$line;
$sce=newValue( $sce, $line );
}
#Server configuration problem
if ( ( $line =~ /Server configuration problem/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$scpC=$line;
$scp=newValue( $scp, $line );
}
#unknown reject reason
if ( ( $line =~ /unknown reject reason/ ) && ( ! $handled ) ){
$line=~s/.*\: //g;
$urrC=$line;
$urr=newValue( $urr, $line );
}
$int++;
}
# final client host rejected total
$chr=newValue( $chr, $chrNew );
# final RBL total
$bu=newValue( $bu, $buNew );
# final recipient address rejected total
$raruu=newValue( $raruu, $raruuNew );
my $data=$received."\n".
$delivered."\n".
$forwarded."\n".
$deferred."\n".
$bounced."\n".
$rejected."\n".
$rejectw."\n".
$held."\n".
$discarded."\n".
$bytesr."\n".
$bytesd."\n".
$senders."\n".
$sendinghd."\n".
$recipients."\n".
$recipienthd."\n".
$deferralcr."\n".
$deferralhid."\n".
$chr."\n".
$hcrnfqh."\n".
$sardnf."\n".
$sarnobu."\n".
$bu."\n".
$raruu."\n".
$hcrin."\n".
$sarnfqa."\n".
$rardnf."\n".
$rarnfqa."\n".
$iuscp."\n".
$sce."\n".
$scp."\n".
$urr."\n".
$msefl."\n";
print $data;
my $current=$receivedC."\n".
$deliveredC."\n".
$forwardedC."\n".
$deferredC."\n".
$bouncedC."\n".
$rejectedC."\n".
$rejectwC."\n".
$heldC."\n".
$discardedC."\n".
$bytesrC."\n".
$bytesdC."\n".
$sendersC."\n".
$sendinghdC."\n".
$recipientsC."\n".
$recipienthdC."\n".
$deferralcrC."\n".
$deferralhidC."\n".
$chrNew."\n".
$hcrnfqhC."\n".
$sardnfC."\n".
$sarnobuC."\n".
$buNew."\n".
$raruuNew."\n".
$hcrinC."\n".
$sarnfqaC."\n".
$rardnfC."\n".
$rarnfqaC."\n".
$iuscpC."\n".
$sceC."\n".
$scpC."\n".
$urrC."\n".
$mseflC."\n";
open(my $fh, ">", $cache) or die "Can't open '".$cache."'";
print $fh $current;
close($fh);
@@ -0,0 +1,46 @@
#!/bin/bash
#######################################
# please read DOCS to succesfully get #
# raspberry sensors into your host #
#######################################
picmd='/usr/bin/vcgencmd'
pised='/bin/sed'
getTemp='measure_temp'
getVoltsCore='measure_volts core'
getVoltsRamC='measure_volts sdram_c'
getVoltsRamI='measure_volts sdram_i'
getVoltsRamP='measure_volts sdram_p'
getFreqArm='measure_clock arm'
getFreqCore='measure_clock core'
getStatusH264='codec_enabled H264'
getStatusMPG2='codec_enabled MPG2'
getStatusWVC1='codec_enabled WVC1'
getStatusMPG4='codec_enabled MPG4'
getStatusMJPG='codec_enabled MJPG'
getStatusWMV9='codec_enabled WMV9'
$picmd $getTemp | $pised 's|[^0-9.]||g'
$picmd "$getVoltsCore" | $pised 's|[^0-9.]||g'
$picmd "$getVoltsRamC" | $pised 's|[^0-9.]||g'
$picmd "$getVoltsRamI" | $pised 's|[^0-9.]||g'
$picmd "$getVoltsRamP" | $pised 's|[^0-9.]||g'
$picmd "$getFreqArm" | $pised 's/frequency([0-9]*)=//g'
$picmd "$getFreqCore" | $pised 's/frequency([0-9]*)=//g'
$picmd "$getStatusH264" | $pised 's/H264=//g'
$picmd "$getStatusMPG2" | $pised 's/MPG2=//g'
$picmd "$getStatusWVC1" | $pised 's/WVC1=//g'
$picmd "$getStatusMPG4" | $pised 's/MPG4=//g'
$picmd "$getStatusMJPG" | $pised 's/MJPG=//g'
$picmd "$getStatusWMV9" | $pised 's/WMV9=//g'
$picmd "$getStatusH264" | $pised 's/enabled/2/g'
$picmd "$getStatusMPG2" | $pised 's/enabled/2/g'
$picmd "$getStatusWVC1" | $pised 's/enabled/2/g'
$picmd "$getStatusMPG4" | $pised 's/enabled/2/g'
$picmd "$getStatusMJPG" | $pised 's/enabled/2/g'
$picmd "$getStatusWMV9" | $pised 's/enabled/2/g'
$picmd "$getStatusH264" | $pised 's/disabled/1/g'
$picmd "$getStatusMPG2" | $pised 's/disabled/1/g'
$picmd "$getStatusWVC1" | $pised 's/disabled/1/g'
$picmd "$getStatusMPG4" | $pised 's/disabled/1/g'
$picmd "$getStatusMJPG" | $pised 's/disabled/1/g'
$picmd "$getStatusWMV9" | $pised 's/disabled/1/g'
+929
View File
@@ -0,0 +1,929 @@
#!/usr/bin/env perl
#Copyright (c) 2024, Zane C. Bowers-Hadley
#All rights reserved.
#
#Redistribution and use in source and binary forms, with or without modification,
#are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
#IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
#INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
#BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
#DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
#LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
#OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
#THE POSSIBILITY OF SUCH DAMAGE.
=for comment
Add this to snmpd.conf like below.
extend smart /etc/snmp/smart
Then add to root's cron tab, if you have more than a few disks.
*/5 * * * * /etc/snmp/extends/smart -u
You will also need to create the config file, which defaults to the same path as the script,
but with .config appended. So if the script is located at /etc/snmp/smart, the config file
will be /etc/snmp/extends/smart.config. Alternatively you can also specific a config via -c.
Anything starting with a # is comment. The format for variables is $variable=$value. Empty
lines are ignored. Spaces and tabes at either the start or end of a line are ignored. Any
line with out a matched variable or # are treated as a disk.
#This is a comment
cache=/var/cache/smart
smartctl=/usr/local/sbin/smartctl
useSN=0
ada0
da5 /dev/da5 -d sat
twl0,0 /dev/twl0 -d 3ware,0
twl0,1 /dev/twl0 -d 3ware,1
twl0,2 /dev/twl0 -d 3ware,2
The variables are as below.
cache = The path to the cache file to use. Default: /var/cache/smart
smartctl = The path to use for smartctl. Default: /usr/bin/env smartctl
useSN = If set to 1, it will use the disks SN for reporting instead of the device name.
1 is the default. 0 will use the device name.
A disk line is can be as simple as just a disk name under /dev/. Such as in the config above
The line "ada0" would resolve to "/dev/ada0" and would be called with no special argument. If
a line has a space in it, everything before the space is treated as the disk name and is what
used for reporting and everything after that is used as the argument to be passed to smartctl.
If you want to guess at the configuration, call it with -g and it will print out what it thinks
it should be.
Switches:
-c <config> The config file to use.
-u Update
-p Pretty print the JSON.
-Z GZip+Base64 compress the results.
-g Guess at the config and print it to STDOUT
-C Enable manual checking for guess and cciss.
-S Set useSN to 0 when using -g
-t <test> Run the specified smart self test on all the devices.
-U When calling cciss_vol_status, call it with -u.
-G <modes> Guess modes to use. This is a comma seperated list.
Default :: scan-open,cciss-vol-status
Guess Modes:
- scan :: Use "--scan" with smartctl. "scan-open" will take presidence.
- scan-open :: Call smartctl with "--scan-open".
- cciss-vol-status :: Freebsd/Linux specific and if it sees /dev/sg0(on Linux) or
/dev/ciss0(on FreebSD) it will attempt to find drives via cciss-vol-status,
and then optionally checking for disks via smrtctl if -C is given. Should be noted
though that -C will not find drives that are currently missing/failed. If -U is given,
cciss_vol_status will be called with -u.
=cut
##
## You should not need to touch anything below here.
##
use warnings;
use strict;
use Getopt::Std;
use JSON;
use MIME::Base64;
use IO::Compress::Gzip qw(gzip $GzipError);
my $cache = '/var/cache/smart';
my $smartctl = '/usr/bin/env smartctl';
my @disks;
my $useSN = 1;
$Getopt::Std::STANDARD_HELP_VERSION = 1;
sub main::VERSION_MESSAGE {
print "SMART SNMP extend 0.3.2\n";
}
sub main::HELP_MESSAGE {
&VERSION_MESSAGE;
print "\n" . "-u Update '" . $cache . "'\n" . '-g Guess at the config and print it to STDOUT
-c <config> The config file to use.
-p Pretty print the JSON.
-Z GZip+Base64 compress the results.
-C Enable manual checking for guess and cciss.
-S Set useSN to 0 when using -g
-t <test> Run the specified smart self test on all the devices.
-U When calling cciss_vol_status, call it with -u.
-G <modes> Guess modes to use. This is a comma seperated list.
Default :: scan-open,cciss-vol-status
Scan Modes:
- scan :: Use "--scan" with smartctl. "scan-open" will take presidence.
- scan-open :: Call smartctl with "--scan-open".
- cciss-vol-status :: Freebsd/Linux specific and if it sees /dev/sg0(on Linux) or
/dev/ciss0(on FreebSD) it will attempt to find drives via cciss-vol-status,
and then optionally checking for disks via smrtctl if -C is given. Should be noted
though that -C will not find drives that are currently missing/failed. If -U is given,
cciss_vol_status will be called with -u.
';
} ## end sub main::HELP_MESSAGE
#gets the options
my %opts = ();
getopts( 'ugc:pZhvCSGt:U', \%opts );
if ( $opts{h} ) {
&HELP_MESSAGE;
exit;
}
if ( $opts{v} ) {
&VERSION_MESSAGE;
exit;
}
#
# figure out what scan modes to use if -g specified
#
my $scan_modes = {
'scan-open' => 0,
'scan' => 0,
'cciss_vol_status' => 0,
};
if ( $opts{g} ) {
if ( !defined( $opts{G} ) ) {
$opts{G} = 'scan-open,cciss_vol_status';
}
$opts{G} =~ s/[\ \t]//g;
my @scan_modes_split = split( /,/, $opts{G} );
foreach my $mode (@scan_modes_split) {
if ( !defined $scan_modes->{$mode} ) {
die( '"' . $mode . '" is not a recognized scan mode' );
}
$scan_modes->{$mode} = 1;
}
} ## end if ( $opts{g} )
# configure JSON for later usage
# only need to do this if actually running as in -g is not specified
my $json;
if ( !$opts{g} ) {
$json = JSON->new->allow_nonref->canonical(1);
if ( $opts{p} ) {
$json->pretty;
}
}
#
#
# guess if asked
#
#
if ( defined( $opts{g} ) ) {
#get what path to use for smartctl
$smartctl = `which smartctl`;
chomp($smartctl);
if ( $? != 0 ) {
warn("'which smartctl' failed with a exit code of $?");
exit 1;
}
#try to touch the default cache location and warn if it can't be done
system( 'touch ' . $cache . '>/dev/null' );
if ( $? != 0 ) {
$cache = '#Could not touch ' . $cache . "You will need to manually set it\n" . "cache=?\n";
} else {
system( 'rm -f ' . $cache . '>/dev/null' );
$cache = 'cache=' . $cache . "\n";
}
my $drive_lines = '';
#
#
# scan-open and scan guess mode handling
#
#
if ( $scan_modes->{'scan-open'} || $scan_modes->{'scan'} ) {
# used for checking if a disk has been found more than once
my %found_disks_names;
my @argumentsA;
# use scan-open if it is set, overriding scan if it is also set
my $mode = 'scan';
if ( $scan_modes->{'scan-open'} ) {
$mode = 'scan-open';
}
#have smartctl scan and see if it finds anythings not get found
my $scan_output = `$smartctl --$mode`;
my @scan_outputA = split( /\n/, $scan_output );
# remove non-SMART devices sometimes returned
@scan_outputA = grep( !/ses[0-9]/, @scan_outputA ); # not a disk, but may or may not have SMART attributes
@scan_outputA = grep( !/pass[0-9]/, @scan_outputA ); # very likely a duplicate and a disk under another name
@scan_outputA = grep( !/cd[0-9]/, @scan_outputA ); # CD drive
if ( $^O eq 'freebsd' ) {
@scan_outputA = grep( !/sa[0-9]/, @scan_outputA ); # tape drive
@scan_outputA = grep( !/ctl[0-9]/, @scan_outputA ); # CAM target layer
} elsif ( $^O eq 'linux' ) {
@scan_outputA = grep( !/st[0-9]/, @scan_outputA ); # SCSI tape drive
@scan_outputA = grep( !/ht[0-9]/, @scan_outputA ); # ATA tape drive
}
# make the first pass, figuring out what all we have and trimming comments
foreach my $arguments (@scan_outputA) {
my $name = $arguments;
$arguments =~ s/ \#.*//; # trim the comment out of the argument
$name =~ s/ .*//;
$name =~ s/\/dev\///;
if ( defined( $found_disks_names{$name} ) ) {
$found_disks_names{$name}++;
} else {
$found_disks_names{$name} = 0;
}
push( @argumentsA, $arguments );
} ## end foreach my $arguments (@scan_outputA)
# second pass, putting the lines together
my %current_disk;
foreach my $arguments (@argumentsA) {
my $not_virt = 1;
# check to see if we have a virtual device
my @virt_check = split( /\n/, `smartctl -i $arguments 2> /dev/null` );
foreach my $virt_check_line (@virt_check) {
if ( $virt_check_line =~ /(?i)Product\:.*LOGICAL VOLUME/ ) {
$not_virt = 0;
}
}
my $name = $arguments;
$name =~ s/ .*//;
$name =~ s/\/dev\///;
# only add it if not a virtual RAID drive
# HP RAID virtual disks will show up with very basical but totally useless smart data
if ($not_virt) {
if ( $found_disks_names{$name} == 0 ) {
# If no other devices, just name it after the base device.
$drive_lines = $drive_lines . $name . " " . $arguments . "\n";
} else {
# if more than one, start at zero and increment, apennding comma number to the base device name
if ( defined( $current_disk{$name} ) ) {
$current_disk{$name}++;
} else {
$current_disk{$name} = 0;
}
$drive_lines = $drive_lines . $name . "," . $current_disk{$name} . " " . $arguments . "\n";
}
} ## end if ($not_virt)
} ## end foreach my $arguments (@argumentsA)
} ## end if ( $scan_modes->{'scan-open'} || $scan_modes...)
#
#
# scan mode handler for cciss_vol_status
# /dev/sg* devices for cciss on Linux
# /dev/ccis* devices for cciss on FreeBSD
#
#
if ( $scan_modes->{'cciss_vol_status'} && ( $^O eq 'linux' || $^O eq 'freebsd' ) ) {
my $cciss;
if ( $^O eq 'freebsd' ) {
$cciss = 'ciss';
} elsif ( $^O eq 'linux' ) {
$cciss = 'sg';
}
my $uarg = '';
if ( $opts{U} ) {
$uarg = '-u';
}
# generate the initial device path that will be checked
my $sg_int = 0;
my $device = '/dev/' . $cciss . $sg_int;
my $sg_process = 1;
if ( -e $device ) {
my $output = `which cciss_vol_status 2> /dev/null`;
if ( $? != 0 && !$opts{C} ) {
$sg_process = 0;
$drive_lines
= $drive_lines
. "# -C not given, but "
. $device
. " exists and cciss_vol_status is not present\n"
. "# in path or 'ccis_vol_status -V "
. $device
. "' is failing\n";
} ## end if ( $? != 0 && !$opts{C} )
} ## end if ( -e $device )
my $seen_lines = {};
my $ignore_lines = {};
while ( -e $device && $sg_process ) {
my $output = `cciss_vol_status -V $uarg $device 2> /dev/null`;
if ( $? != 0 && $output eq '' && !$opts{C} ) {
# just empty here as we just want to skip it if it fails and there is no C
# warning is above
} elsif ( $? != 0 && $output eq '' && $opts{C} ) {
my $drive_count = 0;
my $continue = 1;
while ($continue) {
my $output = `$smartctl -i $device -d cciss,$drive_count 2> /dev/null`;
if ( $? != 0 ) {
$continue = 0;
} else {
my $add_it = 0;
my $id;
while ( $output =~ /(?i)Serial Number:(.*)/g ) {
$id = $1;
$id =~ s/^\s+|\s+$//g;
}
if ( defined($id) && !defined( $seen_lines->{$id} ) ) {
$add_it = 1;
$seen_lines->{$id} = 1;
}
if ( $continue && $add_it ) {
$drive_lines
= $drive_lines
. $cciss . '0-'
. $drive_count . ' '
. $device
. ' -d cciss,'
. $drive_count . "\n";
}
} ## end else [ if ( $? != 0 ) ]
$drive_count++;
} ## end while ($continue)
} else {
my $drive_count = 0;
# count the connector lines, this will make sure failed are founded as well
my $seen_conectors = {};
while ( $output =~ /(connector +\d+[IA]\ +box +\d+\ +bay +\d+.*)/g ) {
my $cciss_drive_line = $1;
my $connector = $cciss_drive_line;
$connector =~ s/(.*\ bay +\d+).*/$1/;
if ( !defined( $seen_lines->{$cciss_drive_line} )
&& !defined( $seen_conectors->{$connector} )
&& !defined( $ignore_lines->{$cciss_drive_line} ) )
{
$seen_lines->{$cciss_drive_line} = 1;
$seen_conectors->{$connector} = 1;
$drive_count++;
} else {
# going to be a connector we've already seen
# which will happen when it is processing replacement drives
# so save this as a device to ignore
$ignore_lines->{$cciss_drive_line} = 1;
}
} ## end while ( $output =~ /(connector +\d+[IA]\ +box +\d+\ +bay +\d+.*)/g)
my $drive_int = 0;
while ( $drive_int < $drive_count ) {
$drive_lines
= $drive_lines
. $cciss
. $sg_int . '-'
. $drive_int . ' '
. $device
. ' -d cciss,'
. $drive_int . "\n";
$drive_int++;
} ## end while ( $drive_int < $drive_count )
} ## end else [ if ( $? != 0 && $output eq '' && !$opts{C})]
$sg_int++;
$device = '/dev/' . $cciss . $sg_int;
} ## end while ( -e $device && $sg_process )
} ## end if ( $scan_modes->{'cciss_vol_status'} && ...)
my $useSN = 1;
if ( $opts{S} ) {
$useSN = 0;
}
print '# scan_modes='
. $opts{G}
. "\nuseSN="
. $useSN . "\n"
. 'smartctl='
. $smartctl . "\n"
. $cache
. $drive_lines;
exit 0;
} ## end if ( defined( $opts{g} ) )
#get which config file to use
my $config = $0 . '.config';
if ( defined( $opts{c} ) ) {
$config = $opts{c};
}
#reads the config file, optionally
my $config_file = '';
open( my $readfh, "<", $config ) or die "Can't open '" . $config . "'";
read( $readfh, $config_file, 1000000 );
close($readfh);
#
#
# parse the config file and remove comments and empty lines
#
#
my @configA = split( /\n/, $config_file );
@configA = grep( !/^$/, @configA );
@configA = grep( !/^\#/, @configA );
@configA = grep( !/^[\s\t]*$/, @configA );
my $configA_int = 0;
while ( defined( $configA[$configA_int] ) ) {
my $line = $configA[$configA_int];
chomp($line);
$line =~ s/^[\t\s]+//;
$line =~ s/[\t\s]+$//;
my ( $var, $val ) = split( /=/, $line, 2 );
my $matched;
if ( $var eq 'cache' ) {
$cache = $val;
$matched = 1;
}
if ( $var eq 'smartctl' ) {
$smartctl = $val;
$matched = 1;
}
if ( $var eq 'useSN' ) {
$useSN = $val;
$matched = 1;
}
if ( !defined($val) ) {
push( @disks, $line );
}
$configA_int++;
} ## end while ( defined( $configA[$configA_int] ) )
#
#
# run the specified self test on all disks if asked
#
#
if ( defined( $opts{t} ) ) {
# make sure we have something that atleast appears sane for the test name
my $valid_tesks = {
'offline' => 1,
'short' => 1,
'long' => 1,
'conveyance' => 1,
'afterselect,on' => 1,
};
if ( !defined( $valid_tesks->{ $opts{t} } ) && $opts{t} !~ /select,(\d+[\-\+]\d+|next|next\+\d+|redo\+\d+)/ ) {
print '"' . $opts{t} . "\" does not appear to be a valid test\n";
exit 1;
}
print "Running the SMART $opts{t} on all devices in the config...\n\n";
foreach my $line (@disks) {
my $disk;
my $name;
if ( $line =~ /\ / ) {
( $name, $disk ) = split( /\ /, $line, 2 );
} else {
$disk = $line;
$name = $line;
}
if ( $disk !~ /\// ) {
$disk = '/dev/' . $disk;
}
print "\n------------------------------------------------------------------\nDoing "
. $smartctl . ' -t '
. $opts{t} . ' '
. $disk
. " ...\n\n";
print `$smartctl -t $opts{t} $disk` . "\n";
} ## end foreach my $line (@disks)
exit 0;
} ## end if ( defined( $opts{t} ) )
#if set to 1, no cache will be written and it will be printed instead
my $noWrite = 0;
#
#
# if no -u, it means we are being called from snmped
#
#
if ( !defined( $opts{u} ) ) {
# if the cache file exists, print it, otherwise assume one is not being used
if ( -f $cache ) {
my $old = '';
open( my $readfh, "<", $cache ) or die "Can't open '" . $cache . "'";
read( $readfh, $old, 1000000 );
close($readfh);
print $old;
exit 0;
} else {
$opts{u} = 1;
$noWrite = 1;
}
} ## end if ( !defined( $opts{u} ) )
#
#
# Process each disk
#
#
my $to_return = {
data => { disks => {}, exit_nonzero => 0, unhealthy => 0, useSN => $useSN },
version => 1,
error => 0,
errorString => '',
};
foreach my $line (@disks) {
my $disk;
my $name;
if ( $line =~ /\ / ) {
( $name, $disk ) = split( /\ /, $line, 2 );
} else {
$disk = $line;
$name = $line;
}
if ( $disk !~ /\// ) {
$disk = '/dev/' . $disk;
}
my $output = `$smartctl -A $disk`;
my %IDs = (
'5' => 'null',
'10' => 'null',
'173' => 'null',
'177' => 'null',
'183' => 'null',
'184' => 'null',
'187' => 'null',
'188' => 'null',
'190' => 'null',
'194' => 'null',
'196' => 'null',
'197' => 'null',
'198' => 'null',
'199' => 'null',
'231' => 'null',
'232' => 'null',
'233' => 'null',
'9' => 'null',
'disk' => $disk,
'serial' => undef,
'selftest_log' => undef,
'health_pass' => 0,
max_temp => 'null',
exit => $?,
);
$IDs{'disk'} =~ s/^\/dev\///;
# if polling exited non-zero above, no reason running the rest of the checks
my $disk_id = $name;
if ( $IDs{exit} != 0 ) {
$to_return->{data}{exit_nonzero}++;
} else {
my @outputA;
if ( $output =~ /NVMe Log/ ) {
# we have an NVMe drive with annoyingly different output
my %mappings = (
'Temperature' => 194,
'Power Cycles' => 12,
'Power On Hours' => 9,
'Percentage Used' => 231,
);
foreach ( split( /\n/, $output ) ) {
if (/:/) {
my ( $key, $val ) = split(/:/);
$val =~ s/^\s+|\s+$|\D+//g;
if ( exists( $mappings{$key} ) ) {
if ( $mappings{$key} == 231 ) {
$IDs{ $mappings{$key} } = 100 - $val;
} else {
$IDs{ $mappings{$key} } = $val;
}
}
} ## end if (/:/)
} ## end foreach ( split( /\n/, $output ) )
} else {
@outputA = split( /\n/, $output );
my $outputAint = 0;
while ( defined( $outputA[$outputAint] ) ) {
my $line = $outputA[$outputAint];
$line =~ s/^ +//;
$line =~ s/ +/ /g;
if ( $line =~ /^[0123456789]+ / ) {
my @lineA = split( /\ /, $line, 10 );
my $raw = $lineA[9];
my $normalized = $lineA[3];
my $id = $lineA[0];
# Crucial SSD
# 202, Percent_Lifetime_Remain, same as 231, SSD Life Left
if ( $id == 202
&& $line =~ /Percent_Lifetime_Remain/ )
{
$IDs{231} = $raw;
}
# single int raw values
if ( ( $id == 5 )
|| ( $id == 10 )
|| ( $id == 173 )
|| ( $id == 183 )
|| ( $id == 184 )
|| ( $id == 187 )
|| ( $id == 196 )
|| ( $id == 197 )
|| ( $id == 198 )
|| ( $id == 199 ) )
{
my @rawA = split( /\ /, $raw );
$IDs{$id} = $rawA[0];
} ## end if ( ( $id == 5 ) || ( $id == 10 ) || ( $id...))
# single int normalized values
if ( ( $id == 177 )
|| ( $id == 230 )
|| ( $id == 231 )
|| ( $id == 232 )
|| ( $id == 233 ) )
{
# annoying non-standard disk
# WDC WDS500G2B0A
# 230 Media_Wearout_Indicator 0x0032 100 100 --- Old_age Always - 0x002e000a002e
# 232 Available_Reservd_Space 0x0033 100 100 004 Pre-fail Always - 100
# 233 NAND_GB_Written_TLC 0x0032 100 100 --- Old_age Always - 9816
if ( $id == 230
&& $line =~ /Media_Wearout_Indicator/ )
{
$IDs{233} = int($normalized);
} elsif ( $id == 232
&& $line =~ /Available_Reservd_Space/ )
{
$IDs{232} = int($normalized);
} else {
# only set 233 if it has not been set yet
# if it was set already then the above did it and we don't want
# to overwrite it
if ( $id == 233 && $IDs{233} eq "null" ) {
$IDs{$id} = int($normalized);
} elsif ( $id != 233 ) {
$IDs{$id} = int($normalized);
}
} ## end else [ if ( $id == 230 && $line =~ /Media_Wearout_Indicator/)]
} ## end if ( ( $id == 177 ) || ( $id == 230 ) || (...))
# 9, power on hours
if ( $id == 9 ) {
my @runtime = split( /[\ h]/, $raw );
$IDs{$id} = $runtime[0];
}
# 188, Command_Timeout
if ( $id == 188 ) {
my $total = 0;
my @rawA = split( /\ /, $raw );
my $rawAint = 0;
while ( defined( $rawA[$rawAint] ) ) {
$total = $total + $rawA[$rawAint];
$rawAint++;
}
$IDs{$id} = $total;
} ## end if ( $id == 188 )
# 190, airflow temp
# 194, temp
if ( ( $id == 190 )
|| ( $id == 194 ) )
{
my ($temp) = split( /\ /, $raw );
$IDs{$id} = $temp;
}
} ## end if ( $line =~ /^[0123456789]+ / )
# SAS Wrapping
# Section by Cameron Munroe (munroenet[at]gmail.com)
# Elements in Grown Defect List.
# Marking as 5 Reallocated_Sector_Ct
if ( $line =~ "Elements in grown defect list:" ) {
my @lineA = split( /\ /, $line, 10 );
my $raw = $lineA[5];
# Reallocated Sector Count ID
$IDs{5} = $raw;
}
# Current Drive Temperature
# Marking as 194 Temperature_Celsius
if ( $line =~ "Current Drive Temperature:" ) {
my @lineA = split( /\ /, $line, 10 );
my $raw = $lineA[3];
# Temperature C ID
$IDs{194} = $raw;
}
# End of SAS Wrapper
$outputAint++;
} ## end while ( defined( $outputA[$outputAint] ) )
} ## end else [ if ( $output =~ /NVMe Log/ ) ]
#get the selftest logs
$output = `$smartctl -l selftest $disk`;
@outputA = split( /\n/, $output );
my @completed = grep( /Completed/, @outputA );
$IDs{'completed'} = scalar @completed;
my @interrupted = grep( /Interrupted/, @outputA );
$IDs{'interrupted'} = scalar @interrupted;
my @read_failure = grep( /read failure/, @outputA );
$IDs{'read_failure'} = scalar @read_failure;
my @read_failure2 = grep( /Failed in segment/, @outputA );
$IDs{'read_failure'} = $IDs{'read_failure'} + scalar @read_failure2;
my @unknown_failure = grep( /unknown failure/, @outputA );
$IDs{'unknown_failure'} = scalar @unknown_failure;
my @extended = grep( /\d.*\ ([Ee]xtended|[Ll]ong).*(?![Dd]uration)/, @outputA );
$IDs{'extended'} = scalar @extended;
my @short = grep( /[Ss]hort/, @outputA );
$IDs{'short'} = scalar @short;
my @conveyance = grep( /[Cc]onveyance/, @outputA );
$IDs{'conveyance'} = scalar @conveyance;
my @selective = grep( /[Ss]elective/, @outputA );
$IDs{'selective'} = scalar @selective;
my @offline = grep( /(\d|[Bb]ackground|[Ff]oreground)+\ +[Oo]ffline/, @outputA );
$IDs{'offline'} = scalar @offline;
# if we have logs, actually grab the log output
if ( $IDs{'completed'} > 0
|| $IDs{'interrupted'} > 0
|| $IDs{'read_failure'} > 0
|| $IDs{'extended'} > 0
|| $IDs{'short'} > 0
|| $IDs{'conveyance'} > 0
|| $IDs{'selective'} > 0
|| $IDs{'offline'} > 0 )
{
my @headers = grep( /(Num\ +Test.*LBA| Description .*[Hh]ours)/, @outputA );
my @log_lines;
push( @log_lines, @extended, @short, @conveyance, @selective, @offline );
$IDs{'selftest_log'} = join( "\n", @headers, sort(@log_lines) );
} ## end if ( $IDs{'completed'} > 0 || $IDs{'interrupted'...})
# get the drive serial number, if needed
$disk_id = $name;
$output = `$smartctl -i $disk`;
# generally upper case, HP branded drives seem to report with lower case n
while ( $output =~ /(?i)Serial Number:(.*)/g ) {
$IDs{'serial'} = $1;
$IDs{'serial'} =~ s/^\s+|\s+$//g;
}
if ($useSN) {
$disk_id = $IDs{'serial'};
}
while ( $output =~ /(?i)Model Family:(.*)/g ) {
$IDs{'model_family'} = $1;
$IDs{'model_family'} =~ s/^\s+|\s+$//g;
}
while ( $output =~ /(?i)Device Model:(.*)/g ) {
$IDs{'device_model'} = $1;
$IDs{'device_model'} =~ s/^\s+|\s+$//g;
}
while ( $output =~ /(?i)Model Number:(.*)/g ) {
$IDs{'model_number'} = $1;
$IDs{'model_number'} =~ s/^\s+|\s+$//g;
}
while ( $output =~ /(?i)Firmware Version:(.*)/g ) {
$IDs{'fw_version'} = $1;
$IDs{'fw_version'} =~ s/^\s+|\s+$//g;
}
# mainly HP drives
while ( $output =~ /(?i)Vendor:(.*)/g ) {
$IDs{'vendor'} = $1;
$IDs{'vendor'} =~ s/^\s+|\s+$//g;
}
# mainly HP drives
while ( $output =~ /(?i)Product:(.*)/g ) {
$IDs{'product'} = $1;
$IDs{'product'} =~ s/^\s+|\s+$//g;
}
# mainly HP drives
while ( $output =~ /(?i)Revision:(.*)/g ) {
$IDs{'revision'} = $1;
$IDs{'revision'} =~ s/^\s+|\s+$//g;
}
# figure out what to use for the max temp, if there is one
if ( $IDs{'190'} =~ /^\d+$/ ) {
$IDs{max_temp} = $IDs{'190'};
} elsif ( $IDs{'194'} =~ /^\d+$/ ) {
$IDs{max_temp} = $IDs{'194'};
}
if ( $IDs{'194'} =~ /^\d+$/ && defined( $IDs{max_temp} ) && $IDs{'194'} > $IDs{max_temp} ) {
$IDs{max_temp} = $IDs{'194'};
}
$output = `$smartctl -H $disk`;
if ( $output =~ /SMART\ overall\-health\ self\-assessment\ test\ result\:\ PASSED/ ) {
$IDs{'health_pass'} = 1;
} elsif ( $output =~ /SMART\ Health\ Status\:\ OK/ ) {
$IDs{'health_pass'} = 1;
}
if ( !$IDs{'health_pass'} ) {
$to_return->{data}{unhealthy}++;
}
} ## end else [ if ( $IDs{exit} != 0 ) ]
# only bother to save this if useSN is not being used
if ( !$useSN ) {
$to_return->{data}{disks}{$disk_id} = \%IDs;
} elsif ( $IDs{exit} == 0 && defined($disk_id) ) {
$to_return->{data}{disks}{$disk_id} = \%IDs;
}
# smartctl will in some cases exit zero when it can't pull data for cciss
# so if we get a zero exit, but no serial then it means something errored
# and the device is likely dead
if ( $IDs{exit} == 0 && !defined( $IDs{serial} ) ) {
$to_return->{data}{unhealthy}++;
}
} ## end foreach my $line (@disks)
my $toReturn = $json->encode($to_return);
if ( !$opts{p} ) {
$toReturn = $toReturn . "\n";
}
if ( $opts{Z} ) {
my $toReturnCompressed;
gzip \$toReturn => \$toReturnCompressed;
my $compressed = encode_base64($toReturnCompressed);
$compressed =~ s/\n//g;
$compressed = $compressed . "\n";
if ( length($compressed) < length($toReturn) ) {
$toReturn = $compressed;
}
} ## end if ( $opts{Z} )
if ( !$noWrite ) {
open( my $writefh, ">", $cache ) or die "Can't open '" . $cache . "'";
print $writefh $toReturn;
close($writefh);
} else {
print $toReturn;
}
@@ -0,0 +1,3 @@
smartctl=/usr/sbin/smartctl
cache=/var/cache/smart
sda
File diff suppressed because one or more lines are too long
@@ -0,0 +1,45 @@
#!/bin/sh
################################################################
# Instructions: #
# 1. copy this script to /etc/snmp/ and make it executable: #
# chmod +x ups-nut.sh #
# 2. make sure UPS_NAME below matches the name of your UPS #
# 3. edit your snmpd.conf to include this line: #
# extend ups-nut /etc/snmp/ups-nut.sh #
# 4. restart snmpd on the host #
# 5. activate the app for the desired host in LibreNMS #
################################################################
UPS_NAME="${1:-APCUPS}"
PATH=$PATH:/usr/bin:/bin
TMP=$(upsc $UPS_NAME 2>/dev/null)
for value in "battery\.charge: [0-9.]+" "battery\.(runtime\.)?low: [0-9]+" "battery\.runtime: [0-9]+" "battery\.voltage: [0-9.]+" "battery\.voltage\.nominal: [0-9]+" "input\.voltage\.nominal: [0-9.]+" "input\.voltage: [0-9.]+" "ups\.load: [0-9.]+"
do
OUT=$(echo "$TMP" | grep -Eo "$value" | awk '{print $2}' | LANG=C sort | head -n 1)
if [ -n "$OUT" ]; then
echo "$OUT"
else
echo "Unknown"
fi
done
for value in "ups\.status:[A-Z ]{0,}OL" "ups\.status:[A-Z ]{0,}OB" "ups\.status:[A-Z ]{0,}LB" "ups\.status:[A-Z ]{0,}HB" "ups\.status:[A-Z ]{0,}RB" "ups\.status:[A-Z ]{0,}CHRG" "ups\.status:[A-Z ]{0,}DISCHRG" "ups\.status:[A-Z ]{0,}BYPASS" "ups\.status:[A-Z ]{0,}CAL" "ups\.status:[A-Z ]{0,}OFF" "ups\.status:[A-Z ]{0,}OVER" "ups\.status:[A-Z ]{0,}TRIM" "ups\.status:[A-Z ]{0,}BOOST" "ups\.status:[A-Z ]{0,}FSD" "ups\.alarm:[A-Z ]"
do
UNKNOWN=$(echo "$TMP" | grep -Eo "ups\.status:")
if [ -z "$UNKNOWN" ]; then
echo "Unknown"
else
OUT=$(echo "$TMP" | grep -Eo "$value")
if [ -n "$OUT" ]; then
echo "1"
else
echo "0"
fi
fi
done
UPSTEMP="ups\.temperature: [0-9.]+"
OUT=$(echo "$TMP" | grep -Eo "$UPSTEMP" | awk '{print $2}' | LANG=C sort | head -n 1)
[ -n "$OUT" ] && echo "$OUT" || echo "Unknown"
@@ -0,0 +1,23 @@
# PFV NFS tuning sysctl overrides
#
# Applied AFTER tuned via pfv-nfs-tuning.service (systemd oneshot).
# These override tuned's network-throughput/virtual-host 16MB TCP buffer
# caps with 128MB for high-BDP NFS over 1-4 GbE LACP links.
#
# Install on ALL Proxmox hosts:
# cp 99-pfv-nfs.conf /etc/sysctl.d/99-pfv-nfs.conf
# cp pfv-nfs-tuning.service /etc/systemd/system/pfv-nfs-tuning.service
# systemctl daemon-reload && systemctl enable --now pfv-nfs-tuning.service
#
# Created: 2026-07-31
# Deployed: tsys1, tsys3, tsys4, tsys5, tsys6, tsys7, tsys9
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.core.rmem_default = 26214400
net.core.wmem_default = 26214400
net.core.netdev_max_backlog = 250000
net.core.somaxconn = 65535
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
net.ipv4.tcp_max_syn_backlog = 4096
@@ -0,0 +1,46 @@
#
# Known Element Enterprises Customized Config File
# auditd
# Initial version 2025-06-27
#
local_events = yes
write_logs = yes
log_file = /var/log/audit/audit.log
log_group = adm
log_format = ENRICHED
flush = INCREMENTAL_ASYNC
freq = 50
max_log_file = 8
num_logs = 5
priority_boost = 4
name_format = NONE
max_log_file_action = keep_logs
space_left = 75
space_left_action = email
action_mail_acct = root
admin_space_left_action = halt
disk_full_action = SUSPEND
disk_error_action = SUSPEND
admin_space_left = 50
verify_email = yes
use_libwrap = yes
tcp_listen_queue = 5
tcp_max_per_addr = 1
tcp_client_max_idle = 0
transport = TCP
distribute_network = no
q_depth = 2000
overflow_action = SYSLOG
max_restarts = 10
plugin_dir = /etc/audit/plugins.d
end_of_event_timeout = 2
##tcp_client_ports = 1024-65535
##tcp_listen_port = 60
##krb5_key_file = /etc/audit/audit.key
krb5_principal = auditd
##name = mydomain
@@ -0,0 +1,5 @@
This system is the property of Known Element Enterprises LLC.
Authorized uses only. All activity may be monitored and reported.
All activities subject to monitoring/recording/review in real time and/or at a later time.
@@ -0,0 +1,5 @@
This system is the property of Known Element Enterprises LLC.
Authorized uses only. All activity may be monitored and reported.
All activities subject to monitoring/recording/review in real time and/or at a later time.
@@ -0,0 +1,5 @@
This system is the property of Known Element Enterprises LLC.
Authorized uses only. All activity may be monitored and reported.
All activities subject to monitoring/recording/review in real time and/or at a later time.
@@ -0,0 +1,2 @@
#/etc/cockpit/disallowed-users
# List of users which are not allowed to login to Cockpit
@@ -0,0 +1,14 @@
option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
send host-name = gethostname();
request subnet-mask, broadcast-address, time-offset, routers,
domain-name, host-name,
domain-name-servers, domain-search, ntp-servers,
rfc3442-classless-static-routes;
# Pin DNS and NTP to the redundant pfv-netinfra-01/02 pair regardless of what
# the DHCP server advertises, so every host on this build uses the same
# authoritative recursive resolvers and time sources.
supersede domain-name-servers 192.168.3.252, 192.168.3.253;
supersede domain-search "knel.net";
supersede ntp-servers 192.168.3.252, 192.168.3.253;
@@ -0,0 +1,23 @@
# see "man logrotate" for details
# global options do not affect preceding include directives
# rotate log files weekly
weekly
# keep 4 weeks worth of backlogs
rotate 4
# create new (empty) log files after rotating old ones
create 0640 root utmp
# use date as a suffix of the rotated file
#dateext
# uncomment this if you want your log files compressed
#compress
# packages drop log rotation information into this directory
include /etc/logrotate.d
# system-specific logs may also be configured here.
@@ -0,0 +1 @@
install cramfs /bin/true
@@ -0,0 +1 @@
install dccp /bin/true
@@ -0,0 +1 @@
install freevxfs /bin/true
@@ -0,0 +1 @@
install hfs /bin/true
@@ -0,0 +1 @@
install hfsplus /bin/true
@@ -0,0 +1 @@
install jffs2 /bin/true
@@ -0,0 +1 @@
install rds /bin/true
@@ -0,0 +1 @@
install sctp /bin/true
@@ -0,0 +1 @@
install squashfs /bin/true
@@ -0,0 +1 @@
install tipc /bin/true
@@ -0,0 +1 @@
install udf /bin/true
@@ -0,0 +1 @@
install usb-storage /bin/true
@@ -0,0 +1,21 @@
driftfile /var/lib/ntp/ntp.drift
leapfile /usr/share/zoneinfo/leap-seconds.list
# Redundant upstream time sources: pfv-netinfra-01/02 (Technitium/Pi-hole hosts
# also serving NTP). IPs are used (not hostnames) because the knel.net name for
# these hosts resolves to a Tailscale CGNAT address, not the LAN address, and
# because NTP must come up before DNS is available. iburst speeds initial sync.
server 192.168.3.252 iburst
server 192.168.3.253 iburst
# Hardened client: sync from the configured servers but never serve time to
# anyone else. Note: `interface listen 127.0.0.1` must NOT be used here — it
# binds ntpd to loopback, making outbound queries carry a 127.0.0.1 source
# address that upstream servers cannot reply to (symptoms: peers stuck in
# .INIT. with reach 0). Use restrict rules to control access instead.
restrict default ignore
restrict 127.0.0.1
restrict ::1
restrict 192.168.3.252 nomodify notrap nopeer
restrict 192.168.3.253 nomodify notrap nopeer
@@ -0,0 +1,2 @@
# Uncomment to start SNMP subagent and enable CDP, SONMP and EDP protocol
DAEMON_ARGS="-x -c -s -e"
@@ -0,0 +1,11 @@
# Managed by KNELServerBuild — do not edit; changes will be overwritten.
#
# Redundant recursive DNS via pfv-netinfra-01/02 (Technitium + Pi-hole).
# IPs are used (required: nameserver directives must be addresses, and the
# knel.net name for these hosts resolves to a Tailscale CGNAT address rather
# than the LAN address). If the primary is unreachable, glibc's resolver
# automatically falls through to the secondary.
domain knel.net
search knel.net
nameserver 192.168.3.252
nameserver 192.168.3.253
@@ -0,0 +1,3 @@
# See man 5 aliases for format
postmaster: root
root: coo@turnsys.com
@@ -0,0 +1 @@
/.*/ tsysrootaccount@knel.net
@@ -0,0 +1 @@
Debian-snmp ALL = NOPASSWD: /bin/cat
@@ -0,0 +1,46 @@
##########################################################################
# snmpd.conf
# Created by CNW on 11/3/2018 via snmpconf wizard and manual post tweaks
###########################################################################
# SECTION: Monitor Various Aspects of the Running Host
#
# disk: Check for disk space usage of a partition.
# The agent can check the amount of available disk space, and make
# sure it is above a set limit.
#
load 3 3 3
rocommunity kn3lmgmt
sysservices 76
#syslocation Rack, Room, Building, City, Country [Lat, Lon]
syslocation R4, Server Room, SITER, Pflugerville, United States
syscontact coo@turnsys.com
#NTP
extend ntp-client /usr/lib/check_mk_agent/local/ntp-client
#SMTP
extend mailq /usr/lib/check_mk_agent/local/postfix-queues
extend postfixdetailed /usr/lib/check_mk_agent/local/postfixdetailed
#OS Distribution Detection
extend distro /usr/local/bin/distro
extend osupdate /usr/lib/check_mk_agent/local/os-updates.sh
#Hardware Detection
extend manufacturer /usr/bin/sudo /usr/bin/cat /sys/devices/virtual/dmi/id/sys_vendor
extend hardware /usr/bin/sudo /usr/bin/cat /sys/devices/virtual/dmi/id/product_name
extend serial /usr/bin/sudo /usr/bin/cat /sys/devices/virtual/dmi/id/product_serial
#SMART
extend smart /usr/lib/check_mk_agent/local/smart
#Temperature
pass_persist .1.3.6.1.4.1.9.9.13.1.3 /usr/local/bin/temper-snmp
# Allow Systems Management Data Engine SNMP to connect to snmpd using SMUX
# smuxpeer .1.3.6.1.4.1.674.10892.1
# LLDP collection
master agentx
@@ -0,0 +1,40 @@
##########################################################################
# snmpd.conf
# Created by CNW on 11/3/2018 via snmpconf wizard and manual post tweaks
###########################################################################
# SECTION: Monitor Various Aspects of the Running Host
#
# disk: Check for disk space usage of a partition.
# The agent can check the amount of available disk space, and make
# sure it is above a set limit.
#
load 3 3 3
rocommunity kn3lmgmt
sysservices 76
#syslocation Rack, Room, Building, City, Country [Lat, Lon]
syslocation SITER, Pflugerville, United States
syscontact coo@turnsys.com
#NTP
extend ntp-client /usr/lib/check_mk_agent/local/ntp-client
#SMTP
extend mailq /usr/lib/check_mk_agent/local/postfix-queues
extend postfixdetailed /usr/lib/check_mk_agent/local/postfixdetailed
#OS Distribution Detection
extend distro /usr/local/bin/distro
extend osupdate /usr/lib/check_mk_agent/local/os-updates.sh
#Hardware Detection
extend hardware /usr/bin/sudo /usr/bin/cat /sys/firmware/devicetree/base/model
extend serial /usr/bin/sudo /usr/bin/cat /sys/firmware/devicetree/base/serial-number
# Allow Systems Management Data Engine SNMP to connect to snmpd using SMUX
# smuxpeer .1.3.6.1.4.1.674.10892.1
# LLDP collection
master agentx
@@ -0,0 +1,44 @@
##########################################################################
# snmpd.conf
# Created by CNW on 11/3/2018 via snmpconf wizard and manual post tweaks
###########################################################################
# SECTION: Monitor Various Aspects of the Running Host
#
# disk: Check for disk space usage of a partition.
# The agent can check the amount of available disk space, and make
# sure it is above a set limit.
#
load 3 3 3
rocommunity kn3lmgmt
sysservices 76
#syslocation Rack, Room, Building, City, Country [Lat, Lon]
syslocation R4, Server Room, SITER, Pflugerville, United States
syscontact coo@turnsys.com
#NTP
extend ntp-client /usr/lib/check_mk_agent/local/ntp-client
#SMTP
extend mailq /usr/lib/check_mk_agent/local/postfix-queues
extend postfixdetailed /usr/lib/check_mk_agent/local/postfixdetailed
#OS Distribution Detection
extend distro /usr/local/bin/distro
extend osupdate /usr/lib/check_mk_agent/local/os-updates.sh
# Socket statistics
extend ss /usr/lib/check_mk_agent/local/ss.py
#Hardware Detection
# (uncomment for x86 platforms)
extend manufacturer /usr/bin/sudo /usr/bin/cat /sys/devices/virtual/dmi/id/sys_vendor
extend hardware /usr/bin/sudo /usr/bin/cat /sys/devices/virtual/dmi/id/product_name
extend serial /usr/bin/sudo /usr/bin/cat /sys/devices/virtual/dmi/id/product_serial
# Allow Systems Management Data Engine SNMP to connect to snmpd using SMUX
# smuxpeer .1.3.6.1.4.1.674.10892.1
# LLDP collection
master agentx
@@ -0,0 +1,2 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDHaBNuLS+GYGRPc9wne63Ocr+R+/Q01Y9V0FTv0RnG3
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPyMR0lFgiMKhQJ5aqy68nR0BQp1cNzi/wIThyuTV4a8 tsyscto@ultix-control
@@ -0,0 +1,2 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDHaBNuLS+GYGRPc9wne63Ocr+R+/Q01Y9V0FTv0RnG3
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPyMR0lFgiMKhQJ5aqy68nR0BQp1cNzi/wIThyuTV4a8 tsyscto@ultix-control
@@ -0,0 +1,19 @@
# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com
# hardening guide.
KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,gss-curve25519-sha256-,diffie-hellman-group16-sha512,gss-group16-sha512-,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com
HostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256
RequiredRSASize 3072
CASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256
GSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-
HostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256
PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256
@@ -0,0 +1,20 @@
Include /etc/ssh/sshd_config.d/*.conf
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ed25519_key
KbdInteractiveAuthentication no
PrintMotd no
PasswordAuthentication no
AllowTcpForwarding no
X11Forwarding no
ChallengeResponseAuthentication no
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
UsePAM yes
Banner /etc/issue.net
MaxAuthTries 2
MaxStartups 10:30:100
PermitRootLogin prohibit-password
ClientAliveInterval 300
ClientAliveCountMax 3
AllowUsers root localuser subodev
LoginGraceTime 60
@@ -0,0 +1,6 @@
module(load="imuxsock") # provides support for local system logging
module(load="imklog") # provides kernel logging support
#module(load="immark") # provides --MARK-- message capability
*.* @tsys-librenms.knel.net:514
:omusrmsg:EOF
@@ -0,0 +1,31 @@
[Journal]
#Compress=yes
#Seal=yes
#SplitMode=uid
#SyncIntervalSec=5m
#RateLimitIntervalSec=30s
#RateLimitBurst=10000
#SystemMaxUse=
#SystemKeepFree=
#SystemMaxFileSize=
#SystemMaxFiles=100
#RuntimeMaxUse=
#RuntimeKeepFree=
#RuntimeMaxFileSize=
#RuntimeMaxFiles=100
#MaxRetentionSec=
#MaxFileSec=1month
#ForwardToSyslog=yes
#ForwardToKMsg=no
#ForwardToConsole=no
#ForwardToWall=yes
#TTYPath=/dev/console
#MaxLevelStore=debug
#MaxLevelSyslog=debug
#MaxLevelKMsg=notice
#MaxLevelConsole=info
#MaxLevelWall=emerg
#LineMax=48K
#ReadKMsg=yes
#Audit=no
Storage=persistent
@@ -0,0 +1,258 @@
# ~/.zshrc file for zsh interactive shells.
# see /usr/share/doc/zsh/examples/zshrc for examples
setopt autocd # change directory just by typing its name
#setopt correct # auto correct mistakes
setopt interactivecomments # allow comments in interactive mode
setopt magicequalsubst # enable filename expansion for arguments of the form anything=expression
setopt nonomatch # hide error message if there is no match for the pattern
setopt notify # report the status of background jobs immediately
setopt numericglobsort # sort filenames numerically when it makes sense
setopt promptsubst # enable command substitution in prompt
WORDCHARS=${WORDCHARS//\/} # Don't consider certain characters part of the word
# hide EOL sign ('%')
PROMPT_EOL_MARK=""
# configure key keybindings
bindkey -v # emacs key bindings
bindkey ' ' magic-space # do history expansion on space
bindkey '^U' backward-kill-line # ctrl + U
bindkey '^[[3;5~' kill-word # ctrl + Supr
bindkey '^[[3~' delete-char # delete
bindkey '^[[1;5C' forward-word # ctrl + ->
bindkey '^[[1;5D' backward-word # ctrl + <-
bindkey '^[[5~' beginning-of-buffer-or-history # page up
bindkey '^[[6~' end-of-buffer-or-history # page down
bindkey '^[[H' beginning-of-line # home
bindkey '^[[F' end-of-line # end
bindkey '^[[Z' undo # shift + tab undo last action
# enable completion features
autoload -Uz compinit
compinit -d ~/.cache/zcompdump
zstyle ':completion:*:*:*:*:*' menu select
zstyle ':completion:*' auto-description 'specify: %d'
zstyle ':completion:*' completer _expand _complete
zstyle ':completion:*' format 'Completing %d'
zstyle ':completion:*' group-name ''
zstyle ':completion:*' list-colors ''
zstyle ':completion:*' list-prompt %SAt %p: Hit TAB for more, or the character to insert%s
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}'
zstyle ':completion:*' rehash true
zstyle ':completion:*' select-prompt %SScrolling active: current selection at %p%s
zstyle ':completion:*' use-compctl false
zstyle ':completion:*' verbose true
zstyle ':completion:*:kill:*' command 'ps -u $USER -o pid,%cpu,tty,cputime,cmd'
# History configurations
HISTFILE=~/.zsh_history
HISTSIZE=10000
SAVEHIST=200000
setopt hist_expire_dups_first # delete duplicates first when HISTFILE size exceeds HISTSIZE
setopt hist_ignore_dups # ignore duplicated commands history list
setopt hist_ignore_space # ignore commands that start with space
setopt hist_verify # show command with history expansion to user before running it
#setopt share_history # share command history data
# force zsh to show the complete history
alias history="history 0"
# configure `time` format
TIMEFMT=$'\nreal\t%E\nuser\t%U\nsys\t%S\ncpu\t%P'
# make less more friendly for non-text input files, see lesspipe(1)
#[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"
# set variable identifying the chroot you work in (used in the prompt below)
if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
debian_chroot=$(cat /etc/debian_chroot)
fi
# set a fancy prompt (non-color, unless we know we "want" color)
case "$TERM" in
xterm-color|*-256color) color_prompt=yes;;
esac
# uncomment for a colored prompt, if the terminal has the capability; turned
# off by default to not distract the user: the focus in a terminal window
# should be on the output of commands, not on the prompt
force_color_prompt=yes
if [ -n "$force_color_prompt" ]; then
if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
# We have color support; assume it's compliant with Ecma-48
# (ISO/IEC-6429). (Lack of such support is extremely rare, and such
# a case would tend to support setf rather than setaf.)
color_prompt=yes
else
color_prompt=
fi
fi
configure_prompt() {
prompt_symbol=㉿
# Skull emoji for root terminal
#[ "$EUID" -eq 0 ] && prompt_symbol=💀
case "$PROMPT_ALTERNATIVE" in
twoline)
PROMPT=$'%F{%(#.blue.green)}┌──${debian_chroot:+($debian_chroot)─}${VIRTUAL_ENV:+($(basename $VIRTUAL_ENV))─}(%B%F{%(#.red.blue)}%n'$prompt_symbol$'%m%b%F{%(#.blue.green)})-[%B%F{reset}%(6~.%-1~/…/%4~.%5~)%b%F{%(#.blue.green)}]\n└─%B%(#.%F{red}#.%F{blue}$)%b%F{reset} '
# Right-side prompt with exit codes and background processes
#RPROMPT=$'%(?.. %? %F{red}%B%b%F{reset})%(1j. %j %F{yellow}%B⚙%b%F{reset}.)'
;;
oneline)
PROMPT=$'${debian_chroot:+($debian_chroot)}${VIRTUAL_ENV:+($(basename $VIRTUAL_ENV))}%B%F{%(#.red.blue)}%n@%m%b%F{reset}:%B%F{%(#.blue.green)}%~%b%F{reset}%(#.#.$) '
RPROMPT=
;;
backtrack)
PROMPT=$'${debian_chroot:+($debian_chroot)}${VIRTUAL_ENV:+($(basename $VIRTUAL_ENV))}%B%F{red}%n@%m%b%F{reset}:%B%F{blue}%~%b%F{reset}%(#.#.$) '
RPROMPT=
;;
esac
unset prompt_symbol
}
# The following block is surrounded by two delimiters.
# These delimiters must not be modified. Thanks.
# START KALI CONFIG VARIABLES
PROMPT_ALTERNATIVE=twoline
NEWLINE_BEFORE_PROMPT=yes
# STOP KALI CONFIG VARIABLES
if [ "$color_prompt" = yes ]; then
# override default virtualenv indicator in prompt
VIRTUAL_ENV_DISABLE_PROMPT=1
configure_prompt
# enable syntax-highlighting
if [ -f /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh ]; then
. /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
ZSH_HIGHLIGHT_HIGHLIGHTERS=(main brackets pattern)
ZSH_HIGHLIGHT_STYLES[default]=none
ZSH_HIGHLIGHT_STYLES[unknown-token]=underline
ZSH_HIGHLIGHT_STYLES[reserved-word]=fg=cyan,bold
ZSH_HIGHLIGHT_STYLES[suffix-alias]=fg=green,underline
ZSH_HIGHLIGHT_STYLES[global-alias]=fg=green,bold
ZSH_HIGHLIGHT_STYLES[precommand]=fg=green,underline
ZSH_HIGHLIGHT_STYLES[commandseparator]=fg=blue,bold
ZSH_HIGHLIGHT_STYLES[autodirectory]=fg=green,underline
ZSH_HIGHLIGHT_STYLES[path]=bold
ZSH_HIGHLIGHT_STYLES[path_pathseparator]=
ZSH_HIGHLIGHT_STYLES[path_prefix_pathseparator]=
ZSH_HIGHLIGHT_STYLES[globbing]=fg=blue,bold
ZSH_HIGHLIGHT_STYLES[history-expansion]=fg=blue,bold
ZSH_HIGHLIGHT_STYLES[command-substitution]=none
ZSH_HIGHLIGHT_STYLES[command-substitution-delimiter]=fg=magenta,bold
ZSH_HIGHLIGHT_STYLES[process-substitution]=none
ZSH_HIGHLIGHT_STYLES[process-substitution-delimiter]=fg=magenta,bold
ZSH_HIGHLIGHT_STYLES[single-hyphen-option]=fg=green
ZSH_HIGHLIGHT_STYLES[double-hyphen-option]=fg=green
ZSH_HIGHLIGHT_STYLES[back-quoted-argument]=none
ZSH_HIGHLIGHT_STYLES[back-quoted-argument-delimiter]=fg=blue,bold
ZSH_HIGHLIGHT_STYLES[single-quoted-argument]=fg=yellow
ZSH_HIGHLIGHT_STYLES[double-quoted-argument]=fg=yellow
ZSH_HIGHLIGHT_STYLES[dollar-quoted-argument]=fg=yellow
ZSH_HIGHLIGHT_STYLES[rc-quote]=fg=magenta
ZSH_HIGHLIGHT_STYLES[dollar-double-quoted-argument]=fg=magenta,bold
ZSH_HIGHLIGHT_STYLES[back-double-quoted-argument]=fg=magenta,bold
ZSH_HIGHLIGHT_STYLES[back-dollar-quoted-argument]=fg=magenta,bold
ZSH_HIGHLIGHT_STYLES[assign]=none
ZSH_HIGHLIGHT_STYLES[redirection]=fg=blue,bold
ZSH_HIGHLIGHT_STYLES[comment]=fg=black,bold
ZSH_HIGHLIGHT_STYLES[named-fd]=none
ZSH_HIGHLIGHT_STYLES[numeric-fd]=none
ZSH_HIGHLIGHT_STYLES[arg0]=fg=cyan
ZSH_HIGHLIGHT_STYLES[bracket-error]=fg=red,bold
ZSH_HIGHLIGHT_STYLES[bracket-level-1]=fg=blue,bold
ZSH_HIGHLIGHT_STYLES[bracket-level-2]=fg=green,bold
ZSH_HIGHLIGHT_STYLES[bracket-level-3]=fg=magenta,bold
ZSH_HIGHLIGHT_STYLES[bracket-level-4]=fg=yellow,bold
ZSH_HIGHLIGHT_STYLES[bracket-level-5]=fg=cyan,bold
ZSH_HIGHLIGHT_STYLES[cursor-matchingbracket]=standout
fi
else
PROMPT='${debian_chroot:+($debian_chroot)}%n@%m:%~%(#.#.$) '
fi
unset color_prompt force_color_prompt
toggle_oneline_prompt(){
if [ "$PROMPT_ALTERNATIVE" = oneline ]; then
PROMPT_ALTERNATIVE=twoline
else
PROMPT_ALTERNATIVE=oneline
fi
configure_prompt
zle reset-prompt
}
zle -N toggle_oneline_prompt
bindkey ^P toggle_oneline_prompt
# If this is an xterm set the title to user@host:dir
case "$TERM" in
xterm*|rxvt*|Eterm|aterm|kterm|gnome*|alacritty)
TERM_TITLE=$'\e]0;${debian_chroot:+($debian_chroot)}${VIRTUAL_ENV:+($(basename $VIRTUAL_ENV))}%n@%m: %~\a'
;;
*)
;;
esac
precmd() {
# Print the previously configured title
print -Pnr -- "$TERM_TITLE"
# Print a new line before the prompt, but only if it is not the first line
if [ "$NEWLINE_BEFORE_PROMPT" = yes ]; then
if [ -z "$_NEW_LINE_BEFORE_PROMPT" ]; then
_NEW_LINE_BEFORE_PROMPT=1
else
print ""
fi
fi
}
# enable color support of ls, less and man, and also add handy aliases
if [ -x /usr/bin/dircolors ]; then
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
export LS_COLORS="$LS_COLORS:ow=30;44:" # fix ls color for folders with 777 permissions
alias ls='ls --color=auto'
#alias dir='dir --color=auto'
#alias vdir='vdir --color=auto'
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
alias diff='diff --color=auto'
alias ip='ip --color=auto'
export LESS_TERMCAP_mb=$'\E[1;31m' # begin blink
export LESS_TERMCAP_md=$'\E[1;36m' # begin bold
export LESS_TERMCAP_me=$'\E[0m' # reset bold/blink
export LESS_TERMCAP_so=$'\E[01;33m' # begin reverse video
export LESS_TERMCAP_se=$'\E[0m' # reset reverse video
export LESS_TERMCAP_us=$'\E[1;32m' # begin underline
export LESS_TERMCAP_ue=$'\E[0m' # reset underline
# Take advantage of $LS_COLORS for completion as well
zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}"
zstyle ':completion:*:*:kill:*:processes' list-colors '=(#b) #([0-9]#)*=0=01;31'
fi
# some more ls aliases
alias ll='ls -l'
alias la='ls -A'
alias l='ls -CF'
# enable auto-suggestions based on the history
if [ -f /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh ]; then
. /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh
# change suggestion color
ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=#999'
fi
# enable command-not-found if installed
if [ -f /etc/zsh_command_not_found ]; then
. /etc/zsh_command_not_found
fi
@@ -0,0 +1,28 @@
# PFV NFS tuning service
#
# Systemd oneshot that runs AFTER tuned.service to apply TCP buffer
# overrides. The tuned daemon's profiles (network-throughput for storage
# hosts, virtual-host for compute hosts) set 16MB TCP buffer caps which
# are too small for high-BDP NFS over LACP links. This service force-
# applies 128MB buffers after tuned has finished its configuration.
#
# Install:
# cp pfv-nfs-tuning.service /etc/systemd/system/pfv-nfs-tuning.service
# systemctl daemon-reload
# systemctl enable --now pfv-nfs-tuning.service
#
# Created: 2026-07-31
# Deployed: all 7 Proxmox hosts (tsys1/3/4/5/6/7/9)
[Unit]
Description=PFV NFS tuning (override tuned TCP buffer caps)
After=tuned.service
Requires=tuned.service
[Service]
Type=oneshot
ExecStart=/sbin/sysctl -p /etc/sysctl.d/99-pfv-nfs.conf
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,24 @@
#!/bin/bash
#magic to detect main int
echo "Determining management interface..."
#export MAIN_INT=$(brctl show $(netstat -rn|grep 0.0.0.0|head -n1|awk '{print $NF}') | awk '{print $NF}'|tail -1|awk -F '.' '{print $1}')
MAIN_INT=$(brctl show|grep vmbr0|awk '{print $NF}'|awk -F '.' '{print $1}')
export MAIN_INT
echo "Management interface is: $MAIN_INT"
#fix the issue
echo "Fixing management interface..."
ethtool -K "$MAIN_INT" tso off
ethtool -K "$MAIN_INT" gro off
ethtool -K "$MAIN_INT" gso off
ethtool -K "$MAIN_INT" tx off
ethtool -K "$MAIN_INT" rx off
#https://forum.proxmox.com/threads/e1000-driver-hang.58284/
#https://serverfault.com/questions/616485/e1000e-reset-adapter-unexpectedly-detected-hardware-unit-hang
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
#curl -s http://dl.turnsys.net/omsa.sh|/bin/bash
gpg --keyserver hkp://pool.sks-keyservers.net:80 --recv-key 1285491434D8786F
gpg -a --export 1285491434D8786F | apt-key add -
echo "deb https://linux.dell.com/repo/community/openmanage/930/bionic bionic main" > /etc/apt/sources.list.d/linux.dell.com.sources.list
wget https://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/libwsman-curl-client-transport1_2.6.5-0ubuntu3_amd64.deb
wget https://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/libwsman-client4_2.6.5-0ubuntu3_amd64.deb
wget https://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/libwsman1_2.6.5-0ubuntu3_amd64.deb
wget https://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/libwsman-server1_2.6.5-0ubuntu3_amd64.deb
wget https://archive.ubuntu.com/ubuntu/pool/universe/s/sblim-sfcc/libcimcclient0_2.2.8-0ubuntu2_amd64.deb
wget https://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/openwsman_2.6.5-0ubuntu3_amd64.deb
wget https://archive.ubuntu.com/ubuntu/pool/multiverse/c/cim-schema/cim-schema_2.48.0-0ubuntu1_all.deb
wget https://archive.ubuntu.com/ubuntu/pool/universe/s/sblim-sfc-common/libsfcutil0_1.0.1-0ubuntu4_amd64.deb
wget https://archive.ubuntu.com/ubuntu/pool/multiverse/s/sblim-sfcb/sfcb_1.4.9-0ubuntu5_amd64.deb
wget https://archive.ubuntu.com/ubuntu/pool/universe/s/sblim-cmpi-devel/libcmpicppimpl0_2.0.3-0ubuntu2_amd64.deb
dpkg -i libwsman-curl-client-transport1_2.6.5-0ubuntu3_amd64.deb
dpkg -i libwsman-client4_2.6.5-0ubuntu3_amd64.deb
dpkg -i libwsman1_2.6.5-0ubuntu3_amd64.deb
dpkg -i libwsman-server1_2.6.5-0ubuntu3_amd64.deb
dpkg -i libcimcclient0_2.2.8-0ubuntu2_amd64.deb
dpkg -i openwsman_2.6.5-0ubuntu3_amd64.deb
dpkg -i cim-schema_2.48.0-0ubuntu1_all.deb
dpkg -i libsfcutil0_1.0.1-0ubuntu4_amd64.deb
dpkg -i sfcb_1.4.9-0ubuntu5_amd64.deb
dpkg -i libcmpicppimpl0_2.0.3-0ubuntu2_amd64.deb
apt update
apt -y install srvadmin-all
touch /opt/dell/srvadmin/lib64/openmanage/IGNORE_GENERATION
#logout,login, then run
# srvadmin-services.sh enable && srvadmin-services.sh start
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
#Script to set performance.
cpufreq-set -r -g performance
cpupower frequency-set --governor performance
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# auth-cloudron-ldap.sh — placeholder module (Cloudron LDAP auth integration).
# Intentionally empty; populated when the auth stack is deployed.
true
@@ -0,0 +1,66 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PROJECT_ROOT_PATH
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)"
export GIT_VENDOR_PATH_ROOT
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
export KNELShellFrameworkRoot
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
export AGENTS_PATH
AGENTS_PATH="$PROJECT_ROOT_PATH/provisioning/Agents"
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
source "$framework_include_file"
done
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
source "$project_include_file"
done
print_info "Setting up librenms agent..."
cat "$AGENTS_PATH/librenms/distro" > /usr/local/bin/distro
chmod +x /usr/local/bin/distro
if [ ! -d /usr/lib/check_mk_agent ]; then
mkdir -p /usr/lib/check_mk_agent
fi
if [ ! -d /usr/lib/check_mk_agent/plugins ]; then
mkdir -p /usr/lib/check_mk_agent/plugins
fi
if [ ! -d /usr/lib/check_mk_agent/local ]; then
mkdir -p /usr/lib/check_mk_agent/local
fi
cat "$AGENTS_PATH/librenms/check_mk_agent" > /usr/bin/check_mk_agent
chmod +x /usr/bin/check_mk_agent
cat "$AGENTS_PATH/librenms/check_mk@.service" > /etc/systemd/system/check_mk@.service
cat "$AGENTS_PATH/librenms/check_mk.socket" > /etc/systemd/system/check_mk.socket
systemctl enable check_mk.socket
systemctl start check_mk.socket
#Modules commented out below, we will roll out on systems that use them, most of the fleet doesn't use those modules
cat "$AGENTS_PATH/librenms/dmi.sh" > /usr/lib/check_mk_agent/local/dmi.sh
cat "$AGENTS_PATH/librenms/dpkg.sh" > /usr/lib/check_mk_agent/local/dpkg.sh
#cat "$AGENTS_PATH/librenms/mysql.sh" > /usr/lib/check_mk_agent/local/mysql.sh
cat "$AGENTS_PATH/librenms/ntp-client" > /usr/lib/check_mk_agent/local/ntp-client
#cat "$AGENTS_PATH/librenms/ntp-server.sh" > /usr/lib/check_mk_agent/local/ntp-server.sh
cat "$AGENTS_PATH/librenms/os-updates.sh" > /usr/lib/check_mk_agent/local/os-updates.sh
cat "$AGENTS_PATH/librenms/postfixdetailed" > /usr/lib/check_mk_agent/local/postfixdetailed
cat "$AGENTS_PATH/librenms/postfix-queues" > /usr/lib/check_mk_agent/local/postfix-queues
#cat "$AGENTS_PATH/librenms/smart.sh" > /usr/lib/check_mk_agent/local/smart
#cat "$AGENTS_PATH/librenms/smart.sh.config" > /usr/lib/check_mk_agent/local/smart.config
chmod +x /usr/lib/check_mk_agent/local/*
@@ -0,0 +1,82 @@
#!/bin/bash
# shellcheck disable=SC2103 # legacy R&D build script; cd/cd- sequence is intentional
#Made from instructions at https://www.tunetheweb.com/performance/http2/
OPENSSL_URL_BASE="https://www.openssl.org/source/"
OPENSSL_FILE="openssl-1.1.0h.tar.gz"
NGHTTP_URL_BASE="https://github.com/nghttp2/nghttp2/releases/download/v1.31.0/"
NGHTTP_FILE="nghttp2-1.31.0.tar.gz"
APR_URL_BASE="https://archive.apache.org/dist/apr/"
APR_FILE="apr-1.6.3.tar.gz"
APR_UTIL_URL_BASE="https://archive.apache.org/dist/apr/"
APR_UTIL_FILE="apr-util-1.6.1.tar.gz"
APACHE_URL_BASE="https://archive.apache.org/dist/httpd/"
APACHE_FILE="httpd-2.4.33.tar.gz"
CURL_URL_BASE="https://curl.haxx.se/download/"
CURL_FILE="curl-7.60.0.tar.gz"
#Download and install latest version of openssl
wget $OPENSSL_URL_BASE/$OPENSSL_FILE
tar xzf $OPENSSL_FILE
cd openssl-1.1.0h || exit
./config enable-weak-ssl-ciphers shared zlib-dynamic -DOPENSSL_TLS_SECURITY_LEVEL=0 --prefix=/usr/local/custom-ssl/openssl-1.1.0h ; make ; make install
ln -s /usr/local/custom-ssl/openssl-1.1.0h /usr/local/openssl
cd - || exit
#Download and install nghttp2 (needed for mod_http2).
wget $NGHTTP_URL_BASE/$NGHTTP_FILE
tar xzf $NGHTTP_FILE
cd nghttp2-1.31.0 || exit
./configure --prefix=/usr/local/custom-ssl/nghttp ; make ; make install
cd - || exit
#Updated ldconfig so curl build
cat <<custom-ssl > /etc/ld.so.conf.d/custom-ssl.conf
/usr/local/custom-ssl/openssl-1.1.0h/lib
/usr/local/custom-ssl/nghttp/lib
custom-ssl
ldconfig
#Download and install curl
wget $CURL_URL_BASE/$CURL_FILE
tar xzf curl-7.60.0.tar.gz
cd curl-7.60.0 || exit
./configure --prefix=/usr/local/custom-ssl/curl --with-nghttp2=/usr/local/custom-ssl/nghttp/ --with-ssl=/usr/local/custom-ssl/openssl-1.1.0h/ ; make ; make install
cd - || exit
#Download and install latest apr
wget $APR_URL_BASE/$APR_FILE
tar xzf $APR_FILE
cd apr-1.6.3 || exit
./configure --prefix=/usr/local/custom-ssl/apr ; make ; make install
cd - || exit
#Download and install latest apr-util
wget $APR_UTIL_URL_BASE/$APR_UTIL_FILE
tar xzf apr-util-1.6.1.tar.gz
cd apr-util-1.6.1 || exit
./configure --prefix=/usr/local/custom-ssl/apr-util --with-apr=/usr/local/custom-ssl/apr ; make; make install
cd - || exit
#Download and install apache
wget $APACHE_URL_BASE/$APACHE_FILE
tar xzf httpd-2.4.33.tar.gz
cd httpd-2.4.33 || exit
cp -r ../apr-1.6.3 srclib/apr
cp -r ../apr-util-1.6.1 srclib/apr-util
./configure --prefix=/usr/local/custom-ssl/apache --with-ssl=/usr/local/custom-ssl/openssl-1.1.0h/ --with-pcre=/usr/bin/pcre-config --enable-unique-id --enable-ssl --enable-so --with-included-apr --enable-http2 --with-nghttp2=/usr/local/custom-ssl/nghttp/
make
make install
ln -s /usr/local/custom-ssl/apache /usr/local/apache
cd - || exit
@@ -0,0 +1,426 @@
#!/bin/bash
# TSYS Security Hardening - Two-Factor Authentication
# Implements 2FA for SSH, Cockpit, and Webmin services
# Uses Google Authenticator (TOTP) for time-based tokens
#####
#Core framework functions...
#####
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PROJECT_ROOT_PATH
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)"
export GIT_VENDOR_PATH_ROOT
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
export KNELShellFrameworkRoot
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
source "$framework_include_file"
done
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
source "$project_include_file"
done
# 2FA Configuration
BACKUP_DIR="/root/backup/2fa"
PAM_CONFIG_DIR="/etc/pam.d"
SSH_CONFIG="/etc/ssh/sshd_config"
COCKPIT_CONFIG="/etc/cockpit/cockpit.conf"
# Create backup directory
mkdir -p "$BACKUP_DIR"
print_info "TSYS Two-Factor Authentication Setup"
# Backup existing configurations
function backup_configs() {
print_info "Creating backup of existing configurations..."
# Backup SSH configuration
if [[ -f "$SSH_CONFIG" ]]; then
cp "$SSH_CONFIG" "$BACKUP_DIR/sshd_config.bak"
print_info "SSH config backed up"
fi
# Backup PAM configurations
if [[ -d "$PAM_CONFIG_DIR" ]]; then
cp -r "$PAM_CONFIG_DIR" "$BACKUP_DIR/pam.d.bak"
print_info "PAM configs backed up"
fi
# Backup Cockpit configuration if exists
if [[ -f "$COCKPIT_CONFIG" ]]; then
cp "$COCKPIT_CONFIG" "$BACKUP_DIR/cockpit.conf.bak"
print_info "Cockpit config backed up"
fi
print_info "Backup completed: $BACKUP_DIR"
}
# Install required packages
function install_2fa_packages() {
print_info "Installing 2FA packages..."
# Update package cache
apt-get update
# Install Google Authenticator PAM module
# Install QR code generator for terminal display
apt-get install -y libpam-google-authenticator qrencode
print_info "2FA packages installed successfully"
}
# Configure SSH for 2FA
function configure_ssh_2fa() {
print_info "Configuring SSH for 2FA..."
# Configure SSH daemon
print_info "Updating SSH configuration..."
# Enable challenge-response authentication
if ! grep -q "^ChallengeResponseAuthentication yes" "$SSH_CONFIG"; then
sed -i 's/^ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' "$SSH_CONFIG" || \
echo "ChallengeResponseAuthentication yes" >> "$SSH_CONFIG"
fi
if ! grep -q "^KbdInteractiveAuthentication yes" "$SSH_CONFIG"; then
sed -i 's/^KbdInteractiveAuthentication.*/KbdInteractiveAuthentication yes/' "$SSH_CONFIG" || \
echo "KbdInteractiveAuthentication yes" >> "$SSH_CONFIG"
fi
# Enable PAM authentication
if ! grep -q "^UsePAM yes" "$SSH_CONFIG"; then
sed -i 's/^UsePAM.*/UsePAM yes/' "$SSH_CONFIG" || \
echo "UsePAM yes" >> "$SSH_CONFIG"
fi
# Configure authentication methods (key + 2FA)
if ! grep -q "^AuthenticationMethods" "$SSH_CONFIG"; then
echo "AuthenticationMethods publickey,keyboard-interactive" >> "$SSH_CONFIG"
else
sed -i 's/^AuthenticationMethods.*/AuthenticationMethods publickey,keyboard-interactive/' "$SSH_CONFIG"
fi
print_info "SSH configuration updated"
}
# Configure PAM for 2FA
function configure_pam_2fa() {
print_info "Configuring PAM for 2FA..."
# Create backup of original PAM SSH config
cp "$PAM_CONFIG_DIR/sshd" "$PAM_CONFIG_DIR/sshd.bak.$(date +%Y%m%d)"
# Configure PAM to use Google Authenticator
cat > "$PAM_CONFIG_DIR/sshd" << 'EOF'
# PAM configuration for SSH with 2FA
# Standard Un*x authentication
@include common-auth
# Google Authenticator 2FA
auth required pam_google_authenticator.so nullok
# Standard Un*x authorization
@include common-account
# SELinux needs to be the first session rule
session required pam_selinux.so close
session required pam_loginuid.so
# Standard Un*x session setup and teardown
@include common-session
# Print the message of the day upon successful login
session optional pam_motd.so motd=/run/motd.dynamic
session optional pam_motd.so noupdate
# Print the status of the user's mailbox upon successful login
session optional pam_mail.so standard noenv
# Set up user limits from /etc/security/limits.conf
session required pam_limits.so
# SELinux needs to intervene at login time
session required pam_selinux.so open
# Standard Un*x password updating
@include common-password
EOF
print_info "PAM configuration updated for SSH 2FA"
}
# Configure Cockpit for 2FA
function configure_cockpit_2fa() {
print_info "Configuring Cockpit for 2FA..."
# Create Cockpit config directory if it doesn't exist
mkdir -p "$(dirname "$COCKPIT_CONFIG")"
# Configure Cockpit to use PAM with 2FA
cat > "$COCKPIT_CONFIG" << 'EOF'
[WebService]
# Enable 2FA for Cockpit web interface
LoginTitle = TSYS Server Management
LoginTo = 300
RequireHost = true
[Session]
# Use PAM for authentication (includes 2FA)
Banner = /etc/cockpit/issue.cockpit
IdleTimeout = 15
EOF
# Create PAM configuration for Cockpit
cat > "$PAM_CONFIG_DIR/cockpit" << 'EOF'
# PAM configuration for Cockpit with 2FA
auth requisite pam_nologin.so
auth required pam_env.so
auth required pam_faillock.so preauth
auth sufficient pam_unix.so try_first_pass
auth required pam_google_authenticator.so nullok
auth required pam_faillock.so authfail
auth required pam_deny.so
account required pam_nologin.so
account include system-auth
account required pam_faillock.so
session required pam_selinux.so close
session required pam_loginuid.so
session optional pam_keyinit.so force revoke
session include system-auth
session required pam_selinux.so open
session optional pam_motd.so
EOF
print_info "Cockpit 2FA configuration completed"
}
# Configure Webmin for 2FA (if installed)
function configure_webmin_2fa() {
print_info "Checking for Webmin installation..."
local webmin_config="/etc/webmin/miniserv.conf"
if [[ -f "$webmin_config" ]]; then
print_info "Webmin found, configuring 2FA..."
# Stop webmin service
systemctl stop webmin || true
# Enable 2FA in Webmin configuration. `sed -i ... || echo` would never
# append, because sed returns 0 even when it matches nothing; guard with
# grep so the directive is added when absent and updated when present.
if grep -q '^twofactor_provider=' "$webmin_config"; then
sed -i 's/^twofactor_provider=.*/twofactor_provider=totp/' "$webmin_config"
else
echo "twofactor_provider=totp" >> "$webmin_config"
fi
# Enable 2FA requirement
if grep -q '^twofactor=' "$webmin_config"; then
sed -i 's/^twofactor=.*/twofactor=1/' "$webmin_config"
else
echo "twofactor=1" >> "$webmin_config"
fi
# Start webmin service
systemctl start webmin || true
print_info "Webmin 2FA configuration completed"
else
print_info "Webmin not found, skipping configuration"
fi
}
# Setup 2FA for users
function setup_user_2fa() {
print_info "Setting up 2FA for system users..."
local users=("localuser" "root")
for user in "${users[@]}"; do
if id "$user" &>/dev/null; then
print_info "Setting up 2FA for user: $user"
local user_home
user_home="$(getent passwd "$user" | cut -d: -f6)"
if [[ -z "$user_home" ]]; then
print_info "No home directory for $user, skipping"
continue
fi
# Create 2FA setup script for user
cat > "/tmp/setup-2fa-$user.sh" << 'EOF'
#!/bin/bash
echo "Setting up Google Authenticator for user: $USER"
echo "Please follow the prompts to configure 2FA:"
echo "1. Answer 'y' to update your time-based token"
echo "2. Scan the QR code with your authenticator app"
echo "3. Save the backup codes in a secure location"
echo "4. Answer 'y' to the remaining questions for security"
echo ""
google-authenticator -t -d -f -r 3 -R 30 -W
EOF
chmod +x "/tmp/setup-2fa-$user.sh"
# Instructions for user setup
cat > "$user_home/2fa-setup-instructions.txt" << EOF
TSYS Two-Factor Authentication Setup Instructions
==============================================
Your system has been configured for 2FA. To complete setup:
1. Install an authenticator app on your phone:
- Google Authenticator
- Authy
- Microsoft Authenticator
2. Run the setup command:
sudo /tmp/setup-2fa-$user.sh
3. Follow the prompts:
- Scan the QR code with your app
- Save the backup codes securely
- Answer 'y' to security questions
4. Test your setup:
- SSH to the server
- Enter your 6-digit code when prompted
IMPORTANT: Save backup codes in a secure location!
Without them, you may be locked out if you lose your phone.
For support, contact your system administrator.
EOF
chown "$user:$user" "$user_home/2fa-setup-instructions.txt"
print_info "2FA setup prepared for user: $user"
else
print_info "User $user not found, skipping"
fi
done
}
# Restart services
function restart_services() {
print_info "Restarting services..."
# Test SSH configuration
if sshd -t; then
systemctl restart sshd
print_info "SSH service restarted"
else
print_error "SSH configuration test failed"
return 1
fi
# Restart Cockpit if installed
if systemctl is-enabled cockpit.socket &>/dev/null; then
systemctl restart cockpit.socket
print_info "Cockpit service restarted"
fi
# Restart Webmin if installed
if systemctl is-enabled webmin &>/dev/null; then
systemctl restart webmin
print_info "Webmin service restarted"
fi
}
# Validation and testing
function validate_2fa_setup() {
print_info "Validating 2FA setup..."
# Check if Google Authenticator is installed
if command -v google-authenticator &>/dev/null; then
print_info "Google Authenticator installed"
else
print_error "Google Authenticator not found"
return 1
fi
# Check SSH configuration
if grep -q "AuthenticationMethods publickey,keyboard-interactive" "$SSH_CONFIG"; then
print_info "SSH 2FA configuration valid"
else
print_error "SSH 2FA configuration invalid"
return 1
fi
# Check PAM configuration
if grep -q "pam_google_authenticator.so" "$PAM_CONFIG_DIR/sshd"; then
print_info "PAM 2FA configuration valid"
else
print_error "PAM 2FA configuration invalid"
return 1
fi
# Check service status
if systemctl is-active sshd &>/dev/null; then
print_info "SSH service is running"
else
print_error "SSH service is not running"
return 1
fi
print_info "2FA validation completed successfully"
}
# Display final instructions
function show_final_instructions() {
print_info "2FA Setup Completed"
print_info "Two-Factor Authentication has been configured for:"
print_info "- SSH (requires key + 2FA token)"
print_info "- Cockpit web interface"
if [[ -f "/etc/webmin/miniserv.conf" ]]; then
print_info "- Webmin administration panel"
fi
print_info "IMPORTANT: Complete user setup immediately!"
print_info "1. Check /home/*/2fa-setup-instructions.txt for user setup"
print_info "2. Run setup scripts for each user"
print_info "3. Test 2FA before logging out"
print_info "Backup location: $BACKUP_DIR"
print_info "To disable 2FA, restore configurations from backup"
print_info "2FA setup completed successfully!"
}
# Main execution
function main() {
# Check if running as root
if [[ $EUID -ne 0 ]]; then
print_error "This script must be run as root"
exit 1
fi
# Execute setup steps
backup_configs
install_2fa_packages
configure_ssh_2fa
configure_pam_2fa
configure_cockpit_2fa
configure_webmin_2fa
setup_user_2fa
restart_services
validate_2fa_setup
show_final_instructions
}
# Run main function
main "$@"
@@ -0,0 +1,50 @@
#!/bin/bash
#####
#Core framework functions...
#####
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PROJECT_ROOT_PATH
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)"
export GIT_VENDOR_PATH_ROOT
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
export KNELShellFrameworkRoot
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
export CONFIGFILES_PATH
CONFIGFILES_PATH="$PROJECT_ROOT_PATH/provisioning/ConfigFiles"
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
source "$framework_include_file"
done
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
source "$project_include_file"
done
# Material herein Sourced from
# https://cisofy.com/documentation/lynis/
# https://jbcsec.com/configure-linux-ssh/
# https://opensource.com/article/20/5/linux-security-lynis
# https://forum.greenbone.net/t/ssh-authentication/13536
# openvas
#lynis
#Auditd
cat "$CONFIGFILES_PATH/AuditD/auditd.conf" > /etc/audit/auditd.conf
# Systemd
cat "$CONFIGFILES_PATH/Systemd/journald.conf" > /etc/systemd/journald.conf
# logrotate
cat "$CONFIGFILES_PATH/Logrotate/logrotate.conf" > /etc/logrotate.conf
@@ -0,0 +1,3 @@
#!/bin/bash
# Sourced from https://wiki.debian.org/UnattendedUpgrades
@@ -0,0 +1,126 @@
#!/bin/bash
#########################################
#Core framework functions...
#########################################
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PROJECT_ROOT_PATH
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)"
export GIT_VENDOR_PATH_ROOT
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
export KNELShellFrameworkRoot
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
export CONFIGFILES_PATH
CONFIGFILES_PATH="$PROJECT_ROOT_PATH/provisioning/ConfigFiles"
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
source "$framework_include_file"
done
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
source "$project_include_file"
done
#########################################
# Core script code begins here
#########################################
# Sourced from
# https://complianceascode.readthedocs.io/en/latest/manual/developer/01_introduction.html
# https://github.com/ComplianceAsCode/content
# https://github.com/ComplianceAsCode
#apparmor
#enforcing
#enabled in bootloader config
#aide
#auditd
#disable auto mounting
#disable usb storage
#motd
#remote login warning banner
#Ensure time sync is working
#systemd-timesync
#ntp
#chrony
#password complexity
#password expiration warning
#password expiration time
#password hashing algo
#fix grub perms
if [ "$IS_RASPI" = 0 ] ; then
chown root:root /boot/grub/grub.cfg
chmod og-rwx /boot/grub/grub.cfg
chmod 0400 /boot/grub/grub.cfg
fi
#disable auto mounting
systemctl --now disable autofs || true
apt-get -y --purge remove autofs || true
#disable usb storage
cat "$CONFIGFILES_PATH/ModProbe/usb_storage.conf" > /etc/modprobe.d/usb_storage.conf
cat "$CONFIGFILES_PATH/ModProbe/dccp.conf" > /etc/modprobe.d/dccp.conf
cat "$CONFIGFILES_PATH/ModProbe/rds.conf" > /etc/modprobe.d/rds.conf
cat "$CONFIGFILES_PATH/ModProbe/sctp.conf" > /etc/modprobe.d/sctp.conf
cat "$CONFIGFILES_PATH/ModProbe/tipc.conf" > /etc/modprobe.d/tipc.conf
cat "$CONFIGFILES_PATH/ModProbe/cramfs.conf" > /etc/modprobe.d/cramfs.conf
cat "$CONFIGFILES_PATH/ModProbe/freevxfs.conf" > /etc/modprobe.d/freevxfs.conf
cat "$CONFIGFILES_PATH/ModProbe/hfs.conf" > /etc/modprobe.d/hfs.conf
cat "$CONFIGFILES_PATH/ModProbe/hfsplus.conf" > /etc/modprobe.d/hfsplus.conf
cat "$CONFIGFILES_PATH/ModProbe/jffs2.conf" > /etc/modprobe.d/jffs2.conf
cat "$CONFIGFILES_PATH/ModProbe/squashfs.conf" > /etc/modprobe.d/squashfs.conf
cat "$CONFIGFILES_PATH/ModProbe/udf.conf" > /etc/modprobe.d/udf.conf
#banners
cat "$CONFIGFILES_PATH/BANNERS/issue" > /etc/issue
cat "$CONFIGFILES_PATH/BANNERS/issue.net" > /etc/issue.net
cat "$CONFIGFILES_PATH/BANNERS/motd" > /etc/motd
#Cron perms
if [ -f /etc/cron.deny ]; then
rm /etc/cron.deny || true
fi
touch /etc/cron.allow
chmod g-wx,o-rwx /etc/cron.allow
chown root:root /etc/cron.allow
chmod og-rwx /etc/crontab
chmod og-rwx /etc/cron.hourly/
chmod og-rwx /etc/cron.daily/
chmod og-rwx /etc/cron.weekly/
chmod og-rwx /etc/cron.monthly/
chown root:root /etc/cron.d/
chmod og-rwx /etc/cron.d/
# At perms
rm -f /etc/at.deny || true
touch /etc/at.allow
chmod g-wx,o-rwx /etc/at.allow
chown root:root /etc/at.allow
@@ -0,0 +1,105 @@
#!/bin/bash
#########################################
#Core framework functions...
#########################################
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PROJECT_ROOT_PATH
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)"
export GIT_VENDOR_PATH_ROOT
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
export KNELShellFrameworkRoot
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
export CONFIGFILES_PATH
CONFIGFILES_PATH="$PROJECT_ROOT_PATH/provisioning/ConfigFiles"
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
source "$framework_include_file"
done
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
source "$project_include_file"
done
#########################################
# Core script code begins here
#########################################
export SUBODEV_CHECK
SUBODEV_CHECK="$(getent passwd | grep -c subodev || true)"
export LOCALUSER_CHECK
LOCALUSER_CHECK="$(getent passwd | grep -c localuser || true)"
export ROOT_SSH_DIR
ROOT_SSH_DIR="/root/.ssh"
export LOCALUSER_SSH_DIR
LOCALUSER_SSH_DIR="/home/localuser/.ssh"
export SUBODEV_SSH_DIR
SUBODEV_SSH_DIR="/home/subodev/.ssh"
if [ ! -d $ROOT_SSH_DIR ]; then
mkdir /root/.ssh/
fi
cat "$CONFIGFILES_PATH/SSH/AuthorizedKeys/root-ssh-authorized-keys" >/root/.ssh/authorized_keys
chmod 400 /root/.ssh/authorized_keys
chown root: /root/.ssh/authorized_keys
if [ "$LOCALUSER_CHECK" -gt 0 ]; then
if [ ! -d $LOCALUSER_SSH_DIR ]; then
mkdir -p /home/localuser/.ssh/
fi
cat "$CONFIGFILES_PATH/SSH/AuthorizedKeys/localuser-ssh-authorized-keys" >/home/localuser/.ssh/authorized_keys
chown localuser /home/localuser/.ssh/authorized_keys &&
chmod 400 /home/localuser/.ssh/authorized_keys
fi
if [ "$SUBODEV_CHECK" = 1 ]; then
if [ ! -d $SUBODEV_SSH_DIR ]; then
mkdir /home/subodev/.ssh/
fi
cat "$CONFIGFILES_PATH/SSH/AuthorizedKeys/localuser-ssh-authorized-keys" >/home/subodev/.ssh/authorized_keys
chmod 400 /home/subodev/.ssh/authorized_keys &&
chown subodev: /home/subodev/.ssh/authorized_keys
fi
export DEV_WORKSTATION_CHECK
DEV_WORKSTATION_CHECK="$(hostname | grep -Ec 'subopi-dev|CharlesDevServer' || true)"
if [ "$DEV_WORKSTATION_CHECK" -eq 0 ]; then
cat "$CONFIGFILES_PATH/SSH/Configs/tsys-sshd-config" >/etc/ssh/sshd_config
fi
#Don't deploy this config to a ubuntu server, it breaks openssh server. Works on kali/debian.
export UBUNTU_CHECK
UBUNTU_CHECK="$(distro | grep -c Ubuntu||true)"
if [ "$UBUNTU_CHECK" -ne 1 ]; then
cat "$CONFIGFILES_PATH/SSH/Configs/ssh-audit-hardening.conf" >/etc/ssh/sshd_config.d/ssh-audit_hardening.conf
chmod og-rwx /etc/ssh/sshd_config.d/*
fi
# Perms on sshd_config
chmod og-rwx /etc/ssh/sshd_config
#todo
# only strong MAC algos are used
@@ -0,0 +1,57 @@
#!/bin/bash
#########################################
#Core framework functions...
#########################################
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PROJECT_ROOT_PATH
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)"
export GIT_VENDOR_PATH_ROOT
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
export KNELShellFrameworkRoot
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
source "$framework_include_file"
done
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
source "$project_include_file"
done
#########################################
# Core script code begins here
#########################################
# We don't want to run this on the wazuh server, otherwise bad things happen...
export TSYS_NSM_CHECK
TSYS_NSM_CHECK="$(hostname |grep -c tsys-nsm ||true)"
if [ "$TSYS_NSM_CHECK" -eq 0 ]; then
if [ -f /usr/share/keyrings/wazuh.gpg ]; then
rm -f /usr/share/keyrings/wazuh.gpg
fi
curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | gpg --no-default-keyring --keyring gnupg-ring:/usr/share/keyrings/wazuh.gpg --import
chmod 644 /usr/share/keyrings/wazuh.gpg
echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" > /etc/apt/sources.list.d/wazuh.list
apt-get update
WAZUH_MANAGER="tsys-nsm.knel.net" apt-get -y install wazuh-agent
systemctl daemon-reload
systemctl enable wazuh-agent
systemctl start wazuh-agent || true
echo "wazuh-agent hold" | dpkg --set-selections
fi
@@ -0,0 +1,3 @@
export DL_ROOT
DL_ROOT="https://dl.knownelement.com/KNEL/FetchApply/"
@@ -0,0 +1,13 @@
#!/bin/bash
function LocalHelp()
{
echo "$0 is <description here>"
echo "$0 takes <num> arguments: "
echo "1) <stuff>"
echo "2) <other stuff>"
echo "<additional info on arguments...>:"
echo "<put>"
echo "<stuff>"
echo "<here>"
}
@@ -0,0 +1,19 @@
#!/bin/bash
function PreflightCheck()
{
export curr_user="$USER"
export user_check
user_check="$(echo "$curr_user" | grep -c root)"
if [ "$user_check" -ne 1 ]; then
print_error "Must run as root."
error_out
fi
echo "All checks passed...."
}
@@ -0,0 +1,13 @@
# shellcheck shell=bash disable=SC2148 # sourced function file (no shebang by design)
function pi-detect()
{
print_info Now running "${FUNCNAME[0]}"....
if [ -f /sys/firmware/devicetree/base/model ] ; then
export IS_RASPI="1"
fi
if [ ! -f /sys/firmware/devicetree/base/model ] ; then
export IS_RASPI="0"
fi
print_info Completed running "${FUNCNAME[0]}"
}
+431
View File
@@ -0,0 +1,431 @@
#!/usr/bin/bash
#####
#Core framework functions...
#####
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PROJECT_ROOT_PATH
PROJECT_ROOT_PATH="$(cd "$SCRIPT_DIR/.." && pwd)"
export GIT_VENDOR_PATH_ROOT
GIT_VENDOR_PATH_ROOT="$PROJECT_ROOT_PATH/vendor/git@git.knownelement.com/29418/"
export KNELShellFrameworkRoot
KNELShellFrameworkRoot="$GIT_VENDOR_PATH_ROOT/KNEL/KNELShellFramework"
export CONFIGFILES_PATH
CONFIGFILES_PATH="$PROJECT_ROOT_PATH/provisioning/ConfigFiles"
export MODULES_PATH
MODULES_PATH="$PROJECT_ROOT_PATH/provisioning/Modules"
export SCRIPTS_PATH
SCRIPTS_PATH="$PROJECT_ROOT_PATH/provisioning/scripts"
source "$KNELShellFrameworkRoot/Framework-ConfigFiles/FrameworkVars"
for framework_include_file in "$KNELShellFrameworkRoot"/Framework-Includes/*; do
source "$framework_include_file"
done
for project_include_file in "$PROJECT_ROOT_PATH"/Project-Includes/*; do
source "$project_include_file"
done
# Start actual script logic here...
#################
#Global variables
#################
apt-get -y install git sudo dmidecode curl
export UBUNTU_CHECK
UBUNTU_CHECK="$(distro | grep -c Ubuntu || true)"
export IS_PHYSICAL_HOST
IS_PHYSICAL_HOST="$(/usr/sbin/dmidecode -t System | grep -c Dell || true)"
export SUBODEV_CHECK
SUBODEV_CHECK="$(getent passwd | grep -c subodev || true)"
export LOCALUSER_CHECK
LOCALUSER_CHECK="$(getent passwd | grep -c localuser || true)"
#######################
# Support functions
#######################
function global-oam() {
print_info "Now running ${FUNCNAME[0]}...."
cat "$SCRIPTS_PATH/up2date.sh" >/usr/local/bin/up2date.sh && chmod +x /usr/local/bin/up2date.sh
bash "$MODULES_PATH/OAM/oam-librenms.sh"
print_info "Completed running ${FUNCNAME[0]}"
}
function global-systemServiceConfigurationFiles() {
print_info "Now running ${FUNCNAME[0]}...."
cat "$CONFIGFILES_PATH/ZSH/tsys-zshrc" >/etc/zshrc
cat "$CONFIGFILES_PATH/SMTP/aliases" >/etc/aliases
cat "$CONFIGFILES_PATH/Syslog/rsyslog.conf" >/etc/rsyslog.conf
newaliases
print_info "Completed running ${FUNCNAME[0]}"
}
function global-installPackages() {
print_info "Now running ${FUNCNAME[0]}...."
# Setup webmin repo, used for RBAC/2fa PAM
curl https://raw.githubusercontent.com/webmin/webmin/master/webmin-setup-repo.sh >/tmp/webmin-setup.sh
sh /tmp/webmin-setup.sh -f && rm -f /tmp/webmin-setup.sh
# Setup tailscale
curl -fsSL https://tailscale.com/install.sh | sh
#
#Patch the system
#
/usr/local/bin/up2date.sh
#Remove stuff we don't want
export DEBIAN_FRONTEND="noninteractive" \
&& apt-get -qq --yes --purge \
remove \
systemd-timesyncd \
chrony \
telnet \
inetutils-telnet \
wpasupplicant \
modemmanager \
nano \
multipath-tools \
|| true
apt-get -y --purge autoremove
# add stuff we want
print_info ""Now installing all the packages...""
DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes -o Dpkg::Options::="--force-confold" install \
virt-what \
auditd \
audispd-plugins \
cloud-guest-utils \
aide \
htop \
snmpd \
ncdu \
iftop \
iotop \
cockpit \
cockpit-bridge \
cockpit-doc \
cockpit-networkmanager \
cockpit-packagekit \
cockpit-pcp \
cockpit-sosreport \
cockpit-storaged \
cockpit-system \
cockpit-ws \
nethogs \
sysstat \
ngrep \
acct \
lsb-release \
screen \
tailscale \
tmux \
vim \
command-not-found \
lldpd \
ansible-core \
net-tools \
dos2unix \
gpg \
molly-guard \
lshw \
fzf \
ripgrep \
sudo \
mailutils \
clamav \
sl \
logwatch \
git \
net-tools \
tshark \
tcpdump \
lynis \
glances \
zsh \
zsh-autosuggestions \
zsh-syntax-highlighting \
fonts-powerline \
webmin \
usermin \
ntpsec \
ntpsec-ntpdate \
tuned \
cockpit \
iptables \
netfilter-persistent \
iptables-persistent \
pflogsumm \
postfix
export KALI_CHECK
KALI_CHECK="$(distro | grep -c kali || true)"
export VIRT_TYPE
VIRT_TYPE="$(virt-what)"
export IS_VIRT_GUEST
IS_VIRT_GUEST="$(echo "$VIRT_TYPE" | grep -Ec 'hyperv|kvm' || true)"
export IS_KVM_GUEST
IS_KVM_GUEST="$(echo "$VIRT_TYPE" | grep -c 'kvm' || true)"
if [[ $IS_KVM_GUEST = 1 ]]; then
apt -y install qemu-guest-agent
fi
if [[ $KALI_CHECK -eq 0 ]];then
DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes -o Dpkg::Options::="--force-confold" install \
latencytop \
cockpit-tests || true
fi
if [[ $IS_PHYSICAL_HOST -gt 0 ]]; then
export DEBIAN_FRONTEND="noninteractive" && apt-get -qq --yes -o Dpkg::Options::="--force-confold" install \
i7z \
thermald \
cpufrequtils \
linux-cpupower
# power-profiles-daemon
fi
############################
# Secrets agents
############################
# bitwarden cli
# vault cli
print_info "Completed running ${FUNCNAME[0]}"
}
function global-postPackageConfiguration() {
print_info "Now running ${FUNCNAME[0]}"
systemctl --now enable auditd
systemctl stop postfix
cat "$CONFIGFILES_PATH/SMTP/postfix_generic" >/etc/postfix/generic
postmap /etc/postfix/generic
postconf -e "inet_protocols = ipv4"
postconf -e "inet_interfaces = 127.0.0.1"
postconf -e "mydestination= 127.0.0.1"
postconf -e "relayhost = tsys-cloudron.knel.net"
postconf -e "smtp_generic_maps = hash:/etc/postfix/generic"
# smtp_generic_maps = hash:/etc/postfix/generic
systemctl restart postfix
#This is under test/dev and may fail
echo "hi from root to root" | mail -s "hi directly to root from $(hostname)" root
chsh -s "$(which zsh)" root
if [ "$LOCALUSER_CHECK" -gt 0 ]; then
chsh -s "$(which zsh)" localuser
fi
if [ "$SUBODEV_CHECK" -gt 0 ]; then
chsh -s "$(which zsh)" subodev
fi
###Post package deployment bits
cat "$CONFIGFILES_PATH/DHCP/dhclient.conf" >/etc/dhcp/dhclient.conf
# Authoritative recursive DNS via the redundant pfv-netinfra-01/02 pair.
# Replace whatever is at /etc/resolv.conf (including a systemd-resolved or
# NetworkManager symlink) with the managed static file so every lookup goes
# to our servers and nothing else rewrites it behind our backs.
rm -f /etc/resolv.conf
cat "$CONFIGFILES_PATH/Resolv/resolv.conf" >/etc/resolv.conf
chmod 644 /etc/resolv.conf
systemctl stop snmpd && /etc/init.d/snmpd stop
cat "$CONFIGFILES_PATH/SNMP/snmp-sudo.conf" >/etc/sudoers.d/Debian-snmp
sed -i "s|-Lsd|-LS6d|" /lib/systemd/system/snmpd.service
pi-detect
if [ "$IS_RASPI" = 1 ]; then
cat "$CONFIGFILES_PATH/SNMP/snmpd-rpi.conf" >/etc/snmp/snmpd.conf || true
fi
if [ "$IS_PHYSICAL_HOST" = 1 ]; then
cat "$CONFIGFILES_PATH/SNMP/snmpd-physicalhost.conf" >/etc/snmp/snmpd.conf || true
fi
if [ "$IS_VIRT_GUEST" = 1 ]; then
cat "$CONFIGFILES_PATH/SNMP/snmpd.conf" >/etc/snmp/snmpd.conf || true
fi
systemctl daemon-reload && systemctl restart snmpd && /etc/init.d/snmpd restart
cat "$CONFIGFILES_PATH/NetworkDiscovery/lldpd" >/etc/default/lldpd
systemctl restart lldpd
cat "$CONFIGFILES_PATH/Cockpit/disallowed-users" >/etc/cockpit/disallowed-users
systemctl restart cockpit
export LIBRENMS_CHECK
LIBRENMS_CHECK="$(hostname | grep -c tsys-librenms || true)"
if [ "$LIBRENMS_CHECK" -eq 0 ]; then
DEBIAN_FRONTEND="noninteractive" apt-get -qq --yes -o Dpkg::Options::="--force-confold" install rsyslog
systemctl stop rsyslog
systemctl start rsyslog
fi
export NTP_SERVER_CHECK
NTP_SERVER_CHECK="$(hostname | grep -Ec 'pfv-netboot|pfvsvrpi|pfv-netinfra' || true)"
if [ "$NTP_SERVER_CHECK" -eq 0 ]; then
cat "$CONFIGFILES_PATH/NTP/ntp.conf" >/etc/ntpsec/ntp.conf
systemctl restart ntpsec.service
fi
systemctl stop postfix
systemctl start postfix
/usr/sbin/accton on
if [ "$IS_PHYSICAL_HOST" -gt 0 ]; then
cpufreq-set -r -g performance
cpupower frequency-set --governor performance
# Potentially merge the below if needed.
# power-profiles-daemon
# powerprofilesctl set performance
#tsys1# systemctl enable power-profiles-daemon
#tsys1# systemctl start power-profiles-daemon
fi
if [ "$IS_VIRT_GUEST" = 1 ]; then
tuned-adm profile virtual-guest
fi
print_info "Completed running ${FUNCNAME[0]}"
}
####################################################################################################
# Run various modules
####################################################################################################
####################################################################################################
# Security Hardening
####################################################################################################
# SSH
function secharden-ssh() {
print_info "Now running ${FUNCNAME[0]}"
bash "$MODULES_PATH/Security/secharden-ssh.sh"
print_info "Completed running ${FUNCNAME[0]}"
}
function secharden-wazuh() {
print_info "Now running ${FUNCNAME[0]}"
bash "$MODULES_PATH/Security/secharden-wazuh.sh"
print_info "Completed running ${FUNCNAME[0]}"
}
function secharden-2fa() {
print_info "Now running ${FUNCNAME[0]}"
bash "$MODULES_PATH/Security/secharden-2fa.sh"
print_info "Completed running ${FUNCNAME[0]}"
}
function secharden-scap-stig() {
print_info "Now running ${FUNCNAME[0]}"
bash "$MODULES_PATH/Security/secharden-scap-stig.sh"
print_info "Completed running ${FUNCNAME[0]}"
}
function secharden-agents() {
print_info "Now running ${FUNCNAME[0]}"
bash "$MODULES_PATH/Security/secharden-audit-agents.sh"
print_info "Completed running ${FUNCNAME[0]}"
}
function secharden-auto-upgrades() {
print_info "Now running ${FUNCNAME[0]}"
#curl --silent ${DL_ROOT}/Modules/Security/secharden-ssh.sh|$(which bash)
print_info "Completed running ${FUNCNAME[0]}"
}
####################################################################################################
# Authentication
####################################################################################################
function auth-cloudron-ldap() {
print_info "Now running ${FUNCNAME[0]}"
#curl --silent ${DL_ROOT}/Modules/Auth/auth-cloudron-ldap.sh|$(which bash)
print_info "Completed running ${FUNCNAME[0]}"
}
####################################################################################################
# RUn the various functions in the correct order
####################################################################################################
echo >"$LOGFILENAME"
print_info "Execution starting at $CURRENT_TIMESTAMP..."
PreflightCheck
global-oam
global-installPackages
global-systemServiceConfigurationFiles
global-postPackageConfiguration
secharden-ssh
secharden-wazuh
secharden-scap-stig
secharden-2fa
#secharden-agents
#secharden-auto-upgrades
#auth-cloudron-ldap
print_info "Execution ended at $CURRENT_TIMESTAMP..."
@@ -0,0 +1,5 @@
# shellcheck shell=bash disable=SC2148 # sourced .bashrc profile fragment
if command -v tmux &> /dev/null && [ -n "$PS1" ] && [[ ! "$TERM" =~ screen ]] && [[ ! "$TERM" =~ tmux ]] && [ -z "$TMUX" ]; then
tmux a -t default || exec tmux new -s default && exit;
fi
@@ -0,0 +1,2 @@
# shellcheck shell=bash disable=SC2148 # sourced .bashrc profile fragment
export HISTTIMEFORMAT="%m/%d/%Y %T "
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
rm -f /etc/apt/sources.list.d/*
echo "deb https://download.proxmox.com/debian/pve bookworm pve-no-subscription" > /etc/apt/sources.list.d/pve-install-repo.list
wget https://download.proxmox.com/debian/proxmox-release-bookworm.gpg -O /etc/apt/trusted.gpg.d/proxmox-release-bookworm.gpg
apt update && apt -y full-upgrade
apt-get -y install ifupdown2 ipmitool ethtool net-tools lshw
#curl -s http://dl.turnsys.net/newSrv.sh|/bin/bash
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
echo "Running apt-get update"
export DEBIAN_FRONTEND="noninteractive" && apt-get -qq --yes update
echo "Running apt-get dist-upgrade"
export DEBIAN_FRONTEND="noninteractive" && apt-get -qq --yes dist-upgrade
echo "Running apt-get upgrade"
export DEBIAN_FRONTEND="noninteractive" && apt-get -qq --yes upgrade
echo "Running apt-get purge"
export DEBIAN_FRONTEND="noninteractive" && apt-get -qq --purge autoremove --yes
export DEBIAN_FRONTEND="noninteractive" && apt-get -qq autoclean --yes
+130
View File
@@ -0,0 +1,130 @@
# Console Management (ser2net + conman)
Network-accessible serial console management for all production network
switches and routers, running on **pfv-tsys4** (storage server).
## Architecture
```
USB-DB9 adapters → udev symlinks (/dev/consoles/<name>) → ser2net telnet(rfc2217) TCP → conman (logging + multiplexing)
```
ser2net owns the physical serial devices and exposes them on TCP ports
using the **telnet(rfc2217) protocol** bound to the **Tailscale interface
only** (`100.70.77.93:200X`). conman connects to those TCP ports via
telnet for session logging, output capture, and multi-user console
sharing.
**Why telnet(rfc2217)?** The serial devices send `
␍` (LF+CR) line
endings instead of standard `
`. Raw TCP transport caused conman's
telnet NVT to strip bare CR characters, producing stair-stepped output.
With telnet(rfc2217) on both sides, binary mode is negotiated and CR/LF
translation is handled correctly by the telnet layer.
**conman and ser2net do NOT share ports** — only one process can open a
serial device at a time. ser2net owns the physical device; conman connects
over TCP.
## The USB Enumeration Problem (SOLVED)
The 9 Prolific USB-to-DB9 adapters (`067b:2303`) on pfv-tsys4 have **no
unique USB serial numbers** and get assigned `/dev/ttyUSB0-8` based on
enumeration order, which shifts on every boot. This made the old
`/root/conmap` + manual `screen` workflow break after every reboot.
**Fix:** udev rules pin each adapter by its **ID_PATH** (physical USB port
topology), which is stable across reboots regardless of enumeration order.
Each adapter gets a named symlink in `/dev/consoles/` that never changes.
The udev rules are generated from `mapping.txt`, which maps each adapter's
ID_PATH to a console name and TCP port. To re-map after physically moving
an adapter, update `mapping.txt` and re-run `setup.sh`.
**Fallback:** if udev trigger doesn't create symlinks for already-discovered
devices (common on first run), `setup.sh` creates them manually by matching
ID_PATH. On subsequent boots, udev creates them automatically.
## Port Assignments
| TCP Port | Console Name | ID_PATH | Description |
|----------|-------------|---------|-------------|
| 2001 | pfv-core-sw01 | usb-0:1.5.4.4 | Dell PowerConnect 5448 (core switch) |
| 2002 | pfv-tor3-mgmt | usb-0:1.6.3.1 | Rack 3 management TOR switch |
| 2003 | pfv-tor3-stor | usb-0:1.6.3.3.2 | Rack 3 storage TOR switch |
| 2004 | pfv-rrinfra-rtr | usb-0:1.6.3.3.1 | Cisco router (rrinfra) |
| 2005 | pfv-r2-tor-top | usb-0:1.6.3.3.3 | Rack 2 top-of-rack switch |
| 2006 | subodev-torsw | usb-0:1.5.4.1 | Suborbital device TOR switch |
| 2007 | pfv-r2-sw | usb-0:1.6.3.2 | Rack 2 old Dell switch |
All ports listen on the Tailscale IP (`100.70.77.93`) using telnet(rfc2217).
## Scripts
| Script | Purpose |
|--------|---------|
| [`mapping.txt`](mapping.txt) | Source of truth: TCP port ↔ ID_PATH ↔ name ↔ baud |
| [`generate-config.sh`](generate-config.sh) | Generates udev rules, ser2net.yaml, conman.conf from mapping.txt |
| [`setup.sh`](setup.sh) | Full deploy: generate configs, create symlinks, restart services |
| [`discover.sh`](discover.sh) | Read-only discovery of USB adapters, existing config, services |
## Usage
### Connect to a console
**Primary method — conman client (with logging + multiplexing):**
```bash
# From any Tailscale-connected workstation:
conman -d pfv-tsys4:7890 -f pfv-core-sw01 # connect to console
conman -d pfv-tsys4:7890 -q # list all consoles
```
Escape sequence: `&.` to disconnect, `&?` for help.
**Direct telnet (emergency only — conflicts with conman):**
```bash
# Direct telnet to ser2net works ONLY when conmand is stopped, because
# conmand maintains persistent connections to all 7 TCP ports. Use:
ssh pfv-tsys4 'systemctl stop conmand'
telnet pfv-tsys4 2001 # pfv-core-sw01
ssh pfv-tsys4 'systemctl start conmand' # restart when done
```
**Do NOT use telnet while conmand is running** — conmand will reconnect
and kick your telnet session immediately ("Connection closed by foreign host").
The correct workflow is conman client → conmand → ser2net → device.
### Re-deploy after changing mapping.txt
```bash
PROX_HOST=pfv-tsys4 bash tests/remote.sh prox 'bash /root/console/setup.sh'
```
### Find the ID_PATH for a new adapter
```bash
PROX_HOST=pfv-tsys4 bash tests/remote.sh prox-file console/discover.sh
```
Then match the new adapter's ID_PATH to its physical location and add a line
to `mapping.txt`.
## Files on pfv-tsys4
| File | Purpose |
|------|---------|
| `/etc/udev/rules.d/99-console-ports.rules` | Stable symlinks by ID_PATH |
| `/etc/ser2net.yaml` | ser2net config (telnet rfc2217 TCP ports → serial symlinks) |
| `/etc/conman.conf` | conman config (CONSOLE entries between markers) |
| `/etc/systemd/system/conmand.service` | systemd unit for conmand |
| `/root/console/mapping.txt` | Copy of the source-of-truth mapping |
| `/root/console/setup.sh` | Setup script (re-runnable) |
| `/root/console/generate-config.sh` | Config generator |
## Old workflow (replaced)
The old `/root/conmap` file and manual `screen` sessions are no longer
needed. The new setup is fully automated and survives reboots.
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/bash
# shellcheck disable=SC2010,SC2012 # diagnostic script; ls|grep/ls -la on sysfs & log dirs is intentional for human-readable output
#
# console/discover.sh — READ-ONLY discovery of console setup on pfv-tsys4
#
# Usage: PROX_HOST=pfv-tsys4 bash tests/remote.sh prox-file console/discover.sh
#
# This script is strictly read-only. No writes to the system.
#
set -uo pipefail
echo "============================================"
echo " Console Setup Discovery"
echo " Host: $(hostname)"
echo " Date: $(date)"
echo " READ-ONLY"
echo "============================================"
echo ""
echo "=== 1. USB devices ==="
lsusb 2>/dev/null || echo "(lsusb not available)"
echo ""
echo "=== 2. All ttyUSB* devices (with major/minor) ==="
ls -la /dev/ttyUSB* 2>/dev/null || echo "(no /dev/ttyUSB* devices)"
echo ""
echo "=== 3. USB-serial driver bindings ==="
echo "-- pl2303 --"
ls -la /sys/bus/usb-serial/drivers/pl2303/ 2>/dev/null | grep -v '^total\|^d\|module\|new_id\|uevent' || echo "(none)"
echo "-- cp210x --"
ls -la /sys/bus/usb-serial/drivers/cp210x/ 2>/dev/null | grep -v '^total\|^d\|module\|new_id\|uevent' || echo "(none)"
echo "-- ftdi_sio --"
ls -la /sys/bus/usb-serial/drivers/ftdi_sio/ 2>/dev/null | grep -v '^total\|^d\|module\|new_id\|uevent' || echo "(none)"
echo "-- ch341 --"
ls -la /sys/bus/usb-serial/drivers/ch341/ 2>/dev/null | grep -v '^total\|^d\|module\|new_id\|uevent' || echo "(none)"
echo ""
echo "=== 4. USB serial adapter details (vendor/model/serial per port) ==="
for tty in /dev/ttyUSB*; do
[ -e "$tty" ] || continue
echo "--- $tty ---"
udevadm info -q all -n "$tty" 2>/dev/null | grep -E 'ID_VENDOR_ID|ID_MODEL_ID|ID_SERIAL|ID_USB_DRIVER|ID_PATH=' | sed 's/^/ /'
done
echo ""
echo "=== 5. Existing /root/conmap ==="
if [ -f /root/conmap ]; then
cat /root/conmap
else
echo "(no /root/conmap)"
fi
ls -la /root/conmap* 2>/dev/null
echo ""
echo "=== 6. Screen sessions (running) ==="
screen -ls 2>&1 || echo "(screen not running or not installed)"
echo ""
echo "=== 7. Existing screen wrappers/scripts in /root ==="
ls -la /root/ 2>/dev/null | grep -iE 'screen|con|console|tty|usb' || echo "(no obvious console scripts in /root)"
echo ""
echo "=== 8. ser2net ==="
which ser2net 2>/dev/null || echo "(ser2net not installed)"
dpkg -l ser2net 2>/dev/null | tail -2 || echo "(ser2net not in dpkg)"
cat /etc/ser2net/ser2net.yaml 2>/dev/null || cat /etc/ser2net.conf 2>/dev/null || cat /etc/ser2net/ser2net.conf 2>/dev/null || echo "(no ser2net config)"
systemctl is-active ser2net 2>/dev/null || echo "(ser2net service not found)"
echo ""
echo "=== 9. conman ==="
which conman 2>/dev/null || echo "(conman not installed)"
which conmand 2>/dev/null || echo "(conmand not installed)"
dpkg -l conman 2>/dev/null | tail -2 || echo "(conman not in dpkg)"
echo "--- /etc/conman.conf (console lines only) ---"
grep -nE 'CONSOLE|SERVER|LOG|SERIAL|DEV|BAUD|^[^#].*name=' /etc/conman.conf 2>/dev/null | head -60 || echo "(no conman.conf or no console entries)"
echo "--- conmand service ---"
systemctl is-active conmand 2>/dev/null || echo "(conmand not running)"
systemctl is-enabled conmand 2>/dev/null || echo "(conmand not enabled)"
echo ""
echo "=== 10. Existing console logs ==="
ls -la /var/log/conman/ 2>/dev/null | head -20 || echo "(no /var/log/conman)"
ls -la /var/consoles/ 2>/dev/null | head -20 || echo "(no /var/consoles)"
echo ""
echo "=== 11. udev rules for ttyUSB ==="
grep -r ttyUSB /etc/udev/rules.d/ 2>/dev/null || echo "(no udev rules for ttyUSB)"
grep -r 'console' /etc/udev/rules.d/ 2>/dev/null | head -10 || true
echo ""
echo "=== 12. expect availability ==="
command -v expect && expect -v 2>&1 || echo "expect: NOT installed"
command -v socat && socat -V 2>&1 | head -1 || echo "socat: NOT installed"
echo ""
echo "=== 13. Ports in use (2001-2099, 7000-7999, 7820-7899) ==="
ss -tlnp 2>/dev/null | grep -E ':200[0-9]|:700[0-9]|:782[0-9]|:789[0-9]' || echo "(no relevant ports listening)"
echo ""
echo "============================================"
echo " Discovery complete (read-only)."
echo "============================================"
+254
View File
@@ -0,0 +1,254 @@
#!/usr/bin/bash
#
# console/generate-config.sh — generate udev rules + ser2net.yaml + conman.conf
#
# Reads console/mapping.txt (the source of truth) and generates all three
# config files. This is the fix for the USB enumeration shift problem:
#
# 1. udev rules pin each adapter by its STABLE ID_PATH (physical USB port)
# to a named symlink like /dev/consoles/pfv-core-sw01
# 2. ser2net opens those stable symlinks and exposes them on TCP ports
# (2001, 2002, ...) bound to the Tailscale IP
# 3. conman connects to those TCP ports for logging + multiplexing
#
# Run this script ON the target host. It writes to:
# /etc/udev/rules.d/99-console-ports.rules
# /etc/ser2net.yaml
# /etc/conman/console-consoles.conf (included by /etc/conman.conf)
#
# Usage:
# PROX_HOST=pfv-tsys4 bash tests/remote.sh prox-file console/generate-config.sh
#
# Environment overrides:
# MAPPING_FILE — path to mapping.txt (default: auto-detect next to this script)
# TS_IP — Tailscale IP to bind ser2net on (default: auto-detect)
# CONMAN_LOGDIR — conman log directory (default: /var/log/conman)
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
MAPPING_FILE="${MAPPING_FILE:-$SCRIPT_DIR/mapping.txt}"
CONMAN_LOGDIR="${CONMAN_LOGDIR:-/var/log/conman}"
UDEV_RULES="/etc/udev/rules.d/99-console-ports.rules"
SER2NET_CONF="/etc/ser2net.yaml"
CONMAN_CONF="/etc/conman.conf"
echo "============================================"
echo " Console Config Generator"
echo " Host: $(hostname) $(date)"
echo "============================================"
# --- Locate mapping file ---
# When run via remote.sh prox-file, $0 is bash and $SCRIPT_DIR may be wrong.
# Search common locations.
if [ ! -f "$MAPPING_FILE" ]; then
for candidate in \
"/root/console/mapping.txt" \
"/tmp/mapping.txt" \
"$(dirname "$0")/mapping.txt"; do
if [ -f "$candidate" ]; then
MAPPING_FILE="$candidate"
break
fi
done
fi
if [ ! -f "$MAPPING_FILE" ]; then
echo "FATAL: mapping file not found. Tried: $MAPPING_FILE"
echo "Copy mapping.txt to the target host first."
exit 1
fi
echo " Mapping file: $MAPPING_FILE"
# --- Auto-detect Tailscale IP ---
if [ -z "${TS_IP:-}" ]; then
TS_IP=$(tailscale ip -4 2>/dev/null || true)
if [ -z "$TS_IP" ]; then
echo "FATAL: could not auto-detect Tailscale IP. Set TS_IP manually."
exit 1
fi
fi
echo " Tailscale IP: $TS_IP"
echo " ser2net will bind to: $TS_IP"
# --- Parse mapping file (skip comments and blank lines) ---
echo ""
echo "--- Parsing mapping file ---"
ENTRIES=()
while IFS= read -r line; do
# Skip comments and blank lines
line="${line%%#*}"
line="$(echo "$line" | xargs)" # trim whitespace
[ -z "$line" ] && continue
ENTRIES+=("$line")
echo " $line"
done < "$MAPPING_FILE"
if [ "${#ENTRIES[@]}" -eq 0 ]; then
echo "FATAL: no entries found in mapping file."
exit 1
fi
echo ""
echo " ${#ENTRIES[@]} console ports configured."
# ============================================================
# 1. Generate udev rules
# ============================================================
echo ""
echo "--- [1/3] Generating udev rules: $UDEV_RULES ---"
cat > "$UDEV_RULES" <<'UDEV_HEADER'
# Stable symlinks for USB-DB9 console adapters
# Generated by console/generate-config.sh
# DO NOT EDIT — edit mapping.txt and re-run generate-config.sh
#
# These rules pin each adapter to a named symlink based on its physical
# USB port path (ID_PATH), which is stable across reboots regardless of
# enumeration order. This is the fix for the "USB adapters shift on reboot"
# problem.
#
# To find the ID_PATH for a device:
# udevadm info -q all -n /dev/ttyUSBN | grep ID_PATH
UDEV_HEADER
for entry in "${ENTRIES[@]}"; do
IFS='|' read -r tcp_port name id_path baud comment <<< "$entry"
# Build the full ID_PATH match. The mapping stores a substring like "usb-0:1.5.4.4"
# The actual ID_PATH is like "pci-0000:00:1a.0-usb-0:1.5.4.4:1.0"
# We match on the substring to be portable across PCI bus changes.
{
echo ""
echo "# $name (TCP $tcp_port): $comment"
echo "SUBSYSTEM==\"tty\", ENV{ID_PATH}==\"*$id_path*\", SYMLINK+=\"consoles/$name\""
} >> "$UDEV_RULES"
done
echo " Written: $UDEV_RULES"
echo " Symlinks: /dev/consoles/<name> for each device"
# ============================================================
# 2. Generate ser2net.yaml
# ============================================================
echo ""
echo "--- [2/3] Generating ser2net config: $SER2NET_CONF ---"
# Backup existing config if not already backed up
if [ -f "$SER2NET_CONF" ] && [ ! -f "${SER2NET_CONF}.orig" ]; then
cp "$SER2NET_CONF" "${SER2NET_CONF}.orig"
echo " Backed up original to ${SER2NET_CONF}.orig"
fi
{
echo "%YAML 1.1"
echo "---"
echo "# ser2net configuration for pfv-tsys4 console ports"
echo "# Generated by console/generate-config.sh on $(date)"
echo "#"
echo "# All ports use telnet(rfc2217) accepter so conman and telnet clients"
echo "# negotiate proper telnet binary mode — this prevents CR stripping"
printf '%s\n' "# and stair-stepping on devices that send \\n\\r (LF+CR) line endings."
echo "# Ports bound to Tailscale IP ($TS_IP) for secure remote access."
echo "#"
echo "# Direct telnet: telnet $TS_IP 2001"
echo "# Via conman: conman -f <name>"
echo ""
printf '%s\n' "define: &banner \\r\\nPFV console port \\p device \\d [\\B]\\r\\n\\r\\n"
echo ""
for entry in "${ENTRIES[@]}"; do
IFS='|' read -r tcp_port name id_path baud comment <<< "$entry"
# ser2net connection block — telnet(rfc2217) accepter so conman and
# telnet clients negotiate proper telnet binary mode. This prevents
# CR stripping that occurs with raw TCP + conman's telnet NVT.
echo "connection: &con${tcp_port}"
echo " accepter: telnet(rfc2217),tcp,${TS_IP},${tcp_port}"
echo " enable: on"
echo " options:"
echo " banner: *banner"
echo " kickolduser: true"
echo " telnet-brk-on-sync: true"
echo " connector: serialdev,"
echo " /dev/consoles/${name},"
echo " ${baud},local"
echo ""
done
} > "$SER2NET_CONF"
echo " Written: $SER2NET_CONF"
echo " ${#ENTRIES[@]} TCP ports configured ($TS_IP:2001-20XX)"
# ============================================================
# 3. Write conman console entries directly into conman.conf
# ============================================================
# conman 0.3.x does NOT support the 'include' directive, so we write
# CONSOLE entries directly into /etc/conman.conf between idempotent markers.
echo ""
echo "--- [3/3] Writing conman consoles into $CONMAN_CONF ---"
# Ensure logdir exists
mkdir -p "$CONMAN_LOGDIR" 2>/dev/null || true
# Ensure LOGDIR is set in conman.conf (server-level directive for log file paths)
if ! grep -qiE '^\s*server\s+logdir\s*=' "$CONMAN_CONF" 2>/dev/null; then
# Insert near the top, after the first SERVER directives
sed -i "1i\\server logdir = \"$CONMAN_LOGDIR\"" "$CONMAN_CONF"
echo " Added server logdir = \"$CONMAN_LOGDIR\" to $CONMAN_CONF"
fi
# Ensure loopback=off so conmand is reachable over Tailscale (not localhost-only)
if ! grep -qiE '^\s*server\s+loopback\s*=' "$CONMAN_CONF" 2>/dev/null; then
sed -i "/^server logdir/a server loopback=off" "$CONMAN_CONF"
echo " Added server loopback=off to $CONMAN_CONF (enables remote access)"
fi
# Remove any previous auto-generated block (between markers)
# Then append the new block
MARKER_BEGIN="# BEGIN PFV CONSOLE DEFINITIONS (auto-generated — do not edit between markers)"
MARKER_END="# END PFV CONSOLE DEFINITIONS"
# Strip old block if present
if grep -q "$MARKER_BEGIN" "$CONMAN_CONF" 2>/dev/null; then
sed -i "/$MARKER_BEGIN/,/$MARKER_END/d" "$CONMAN_CONF"
echo " Removed previous console definitions."
fi
# Append new block
{
echo ""
echo "$MARKER_BEGIN"
echo "# Generated by console/generate-config.sh on $(date)"
echo "# Each console connects to a ser2net TCP port via telnet protocol."
echo "# ser2net uses telnet(rfc2217) accepter so binary mode is negotiated"
echo "# and CR/LF translation is handled correctly by the telnet NVT layer."
echo "# Access: conman -f <name>"
echo ""
for entry in "${ENTRIES[@]}"; do
IFS='|' read -r tcp_port name id_path baud comment <<< "$entry"
echo "CONSOLE name=\"${name}\" dev=\"${TS_IP}:${tcp_port}\" log=\"${name}.log\" logopts=\"timestamp\""
done
echo "$MARKER_END"
} >> "$CONMAN_CONF"
CONSOLE_COUNT=$(grep -c "^CONSOLE " "$CONMAN_CONF" 2>/dev/null || echo 0)
echo " Written $CONSOLE_COUNT CONSOLE entries to $CONMAN_CONF"
# ============================================================
# Summary
# ============================================================
echo ""
echo "============================================"
echo " Configuration generated successfully."
echo ""
echo " Files written:"
echo " $UDEV_RULES ($(wc -l < "$UDEV_RULES") lines)"
echo " $SER2NET_CONF ($(wc -l < "$SER2NET_CONF") lines)"
echo " $CONMAN_CONF (CONSOLE entries appended between markers)"
echo ""
echo " Next steps:"
echo " 1. Reload udev: udevadm control --reload-rules && udevadm trigger"
echo " 2. Restart ser2net: systemctl restart ser2net"
echo " 3. Start conman: systemctl enable --now conmand"
echo " 4. Or run: bash $(basename "$0" .sh | sed 's/generate-config/setup/') .sh"
echo "============================================"
+29
View File
@@ -0,0 +1,29 @@
# console/mapping.txt — Source of Truth for console port assignments
#
# Format: <tcp_port>|<name>|<id_path_substring>|<baud>|<comment>
#
# Delimiter is | (pipe) because ID_PATH values contain colons.
#
# - tcp_port: TCP port ser2net listens on (also the conman console name suffix)
# - name: Device name (used for /dev/console/<name> symlink, conman console name)
# - id_path_substring: Stable USB physical path from `udevadm info -q all -n /dev/ttyUSBN | grep ID_PATH`
# These are STABLE across reboots as long as adapters aren't moved
# to different physical USB ports.
# - baud: Serial baud rate (9600n81 = 9600 8N1, no flow control)
# - comment: Free-form description
#
# To RE-MAP after physically moving an adapter:
# 1. Run: bash console/discover.sh (find the new ID_PATH for the device)
# 2. Update the id_path_substring in this file
# 3. Run: bash console/generate-config.sh && udevadm trigger && systemctl restart ser2net conmand
#
2001|pfv-core-sw01|usb-0:1.5.4.4|9600n81|Dell PowerConnect 5448 (core switch)
2002|pfv-tor3-mgmt|usb-0:1.6.3.1|9600n81|Rack 3 management TOR switch
2003|pfv-tor3-stor|usb-0:1.6.3.3.2|9600n81|Rack 3 storage TOR switch
2004|pfv-rrinfra-rtr|usb-0:1.6.3.3.1|9600n81|Cisco router (rrinfra)
2005|pfv-r2-tor-top|usb-0:1.6.3.3.3|9600n81|Rack 2 top-of-rack switch
2006|subodev-torsw|usb-0:1.5.4.1|9600n81|Suborbital device TOR switch
2007|pfv-r2-sw|usb-0:1.6.3.2|9600n81|Rack 2 old Dell switch
# Unassigned (no device detected):
# 2008|spare-1|usb-0:1.6.3.4|9600n81|Empty / spare
# 2009|spare-2|usb-0:1.6.3.3.4|9600n81|Empty / spare
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/bash
#
# console/query-remote.sh — install conman client and connect to a console
# on pfv-tsys4 over Tailscale.
#
# Usage:
# bash console/query-remote.sh # list consoles
# bash console/query-remote.sh pfv-core-sw01 # connect to a console
#
set -euo pipefail
REMOTE_HOST="${REMOTE_HOST:-pfv-tsys4}"
REMOTE_PORT="${REMOTE_PORT:-7890}"
echo "============================================"
echo " Conman Remote Console Access"
echo " Server: ${REMOTE_HOST}:${REMOTE_PORT} (Tailscale)"
echo "============================================"
# --- 1. Install conman client if missing ---
if ! command -v conman >/dev/null 2>&1; then
echo ""
echo "--- Installing conman client ---"
if sudo -n true 2>/dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq conman
else
echo " Passwordless sudo not available. Please run:"
echo " sudo apt-get update && sudo apt-get install -y conman"
echo " Then re-run this script."
exit 1
fi
else
echo " conman client already installed."
fi
# --- 2. Verify connectivity ---
echo ""
echo "--- Connectivity check ---"
if timeout 3 bash -c "echo > /dev/tcp/${REMOTE_HOST}/${REMOTE_PORT}" 2>/dev/null; then
echo " [OK] ${REMOTE_HOST}:${REMOTE_PORT} reachable"
else
echo " [FAIL] Cannot reach ${REMOTE_HOST}:${REMOTE_PORT}"
echo " Is Tailscale up? Is conmand running on ${REMOTE_HOST}?"
exit 1
fi
# --- 3. List or connect ---
CONSOLE="${1:-}"
if [ -z "$CONSOLE" ]; then
echo ""
echo "--- Available consoles ---"
conman -d "${REMOTE_HOST}:${REMOTE_PORT}" -q
echo ""
echo "To connect: bash $0 <console-name>"
echo " e.g: bash $0 pfv-core-sw01"
else
echo ""
echo "--- Connecting to: $CONSOLE ---"
echo " Escape sequence: &. (to disconnect)"
echo ""
conman -d "${REMOTE_HOST}:${REMOTE_PORT}" -f "$CONSOLE"
fi
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/bash
# shellcheck disable=SC2010 # diagnostic; ls|grep on /dev listing is intentional
#
# console/setup.sh — deploy console management on pfv-tsys4
#
# Orchestrates the full setup:
# 1. Ensures ser2net + conman are installed
# 2. Copies mapping.txt to the target host (if running remotely)
# 3. Runs generate-config.sh to produce udev rules + ser2net.yaml + conman.conf
# 4. Reloads udev, creates /dev/consoles/ symlinks
# 5. Restarts ser2net (TCP ports on Tailscale IP)
# 6. Enables + starts conmand (logging + multiplexing)
# 7. Verifies
#
# This script is IDEMPOTENT — safe to run multiple times.
#
# Usage:
# PROX_HOST=pfv-tsys4 bash tests/remote.sh prox-file console/setup.sh
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "============================================"
echo " Console Management Setup"
echo " Host: $(hostname) $(date)"
echo "============================================"
# --- 1. Install dependencies ---
echo ""
echo "--- [1/7] Checking dependencies ---"
NEED_INSTALL=()
dpkg -l ser2net 2>/dev/null | grep -q '^ii' && echo " ser2net: installed" || NEED_INSTALL+=(ser2net)
dpkg -l conman 2>/dev/null | grep -q '^ii' && echo " conman: installed" || NEED_INSTALL+=(conman)
if [ "${#NEED_INSTALL[@]}" -gt 0 ]; then
echo " Installing: ${NEED_INSTALL[*]}"
apt-get update -qq
apt-get install -y -qq "${NEED_INSTALL[@]}"
else
echo " All dependencies present."
fi
# --- 2. Ensure mapping file is available ---
echo ""
echo "--- [2/7] Locating mapping file ---"
MAPPING_FILE=""
for candidate in \
"$SCRIPT_DIR/mapping.txt" \
"$(dirname "$0")/mapping.txt" \
"/root/console/mapping.txt" \
"/tmp/mapping.txt"; do
if [ -f "$candidate" ]; then
MAPPING_FILE="$candidate"
break
fi
done
if [ -z "$MAPPING_FILE" ]; then
echo "FATAL: mapping.txt not found. Copy it to the target host."
exit 1
fi
echo " Using: $MAPPING_FILE"
# --- 3. Generate configs ---
echo ""
echo "--- [3/7] Generating configs ---"
export MAPPING_FILE
bash "$(dirname "$0")/generate-config.sh" 2>&1 || bash "$SCRIPT_DIR/generate-config.sh" 2>&1 || {
echo "FATAL: generate-config.sh failed."
exit 1
}
# --- 4. Reload udev + create symlinks ---
echo ""
echo "--- [4/7] Reloading udev rules ---"
udevadm control --reload-rules
# Try trigger first (works on some systems)
for tty in /sys/class/tty/ttyUSB*; do
[ -e "$tty" ] && udevadm trigger --action=add "$tty" 2>/dev/null || true
done
# Also try writing to uevent (forces udev reprocessing)
for tty in /sys/class/tty/ttyUSB*; do
[ -e "$tty/uevent" ] && echo "add" > "$tty/uevent" 2>/dev/null || true
done
sleep 2
# FALLBACK: if udev symlinks don't exist (common when devices are already
# discovered — udev trigger doesn't always re-create symlinks for existing
# devices), create them manually by matching ID_PATH. The udev rules will
# handle future boots/hotplugs automatically.
if [ ! -d /dev/consoles ] || [ -z "$(ls /dev/consoles/ 2>/dev/null)" ]; then
echo " udev trigger didn't create symlinks. Creating manually..."
mkdir -p /dev/consoles
while IFS= read -r line; do
line="${line%%#*}"
line="$(echo "$line" | xargs)"
[ -z "$line" ] && continue
IFS='|' read -r _ name id_path _ _ <<< "$line"
# Find the ttyUSB whose ID_PATH contains the mapping's id_path substring
for tty in /dev/ttyUSB*; do
[ -e "$tty" ] || continue
DEV_IDPATH=$(udevadm info -q property -n "$tty" 2>/dev/null | grep ^ID_PATH= | cut -d= -f2)
if echo "$DEV_IDPATH" | grep -q "$id_path"; then
ln -sf "$tty" "/dev/consoles/$name"
echo " ln -s $tty -> /dev/consoles/$name"
break
fi
done
done < "$MAPPING_FILE"
fi
echo " Stable symlinks:"
ls -la /dev/consoles/ 2>/dev/null | grep -v '^total\|^d' | sed 's/^/ /' || echo " (none created)"
# Verify each symlink resolves
echo ""
echo " Symlink verification:"
while IFS= read -r line; do
line="${line%%#*}"
line="$(echo "$line" | xargs)"
[ -z "$line" ] && continue
IFS='|' read -r _ name id_path _ _ <<< "$line"
if [ -e "/dev/consoles/$name" ]; then
TARGET=$(readlink -f "/dev/consoles/$name")
echo " [OK] /dev/consoles/$name -> $TARGET"
else
echo " [MISSING] /dev/consoles/$name (adapter unplugged or ID_PATH changed)"
fi
done < "$MAPPING_FILE"
# --- 5. Restart ser2net ---
echo ""
echo "--- [5/7] Restarting ser2net ---"
systemctl enable ser2net
systemctl restart ser2net
sleep 2
if systemctl is-active --quiet ser2net; then
echo " ser2net is running (telnet rfc2217 accepters)."
TS_IP=$(tailscale ip -4 2>/dev/null || echo "127.0.0.1")
echo " Listening ports:"
ss -tlnp | grep ser2net | grep -oE "${TS_IP}:[0-9]+" | sort -t: -k2 -n | sed 's/^/ /'
else
echo " WARNING: ser2net failed to start. Checking journal..."
journalctl -u ser2net --no-pager -n 20
fi
# --- 6. Enable + start conmand ---
echo ""
echo "--- [6/7] Starting conmand ---"
# conman package on Debian may not ship a systemd unit. Create one if missing.
if ! systemctl cat conmand >/dev/null 2>&1; then
echo " No systemd unit for conmand — creating one..."
cat > /etc/systemd/system/conmand.service <<'CONMAND_UNIT'
[Unit]
Description=ConMan (Console Manager)
After=network.target ser2net.service
Requires=ser2net.service
[Service]
Type=forking
ExecStart=/usr/sbin/conmand -c /etc/conman.conf
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
CONMAND_UNIT
systemctl daemon-reload
echo " Created /etc/systemd/system/conmand.service"
fi
# Kill any manually-started conmand first
pkill -x conmand 2>/dev/null || true
sleep 1
systemctl enable conmand 2>/dev/null || true
systemctl restart conmand 2>/dev/null || true
sleep 2
if systemctl is-active --quiet conmand; then
echo " conmand is running."
echo " Consoles:"
conman -q 2>&1 | sed 's/^/ /' || true
else
echo " WARNING: conmand failed to start. Checking journal..."
journalctl -u conmand --no-pager -n 20 2>/dev/null || true
# Try manual start as fallback
echo " Attempting manual start..."
/usr/sbin/conmand -c /etc/conman.conf 2>&1 || true
fi
# --- 7. Summary ---
echo ""
echo "--- [7/7] Setup complete ---"
echo ""
echo " ser2net + conman architecture (telnet rfc2217):"
echo " ser2net owns serial devices, exposes telnet(rfc2217) TCP ports"
echo " conman connects via telnet for logging + multiplexing"
echo ""
echo " Connect from any Tailscale workstation:"
echo " conman -d pfv-tsys4:7890 -f pfv-core-sw01"
echo " conman -d pfv-tsys4:7890 -q # list consoles"
echo ""
echo " Direct telnet (emergency, conflicts with conman):"
echo " ssh pfv-tsys4 'systemctl stop conmand'"
echo " telnet pfv-tsys4 2001"
echo " ssh pfv-tsys4 'systemctl start conmand'"
echo ""
echo " To regenerate after changing mapping.txt:"
echo " bash generate-config.sh"
echo " udevadm trigger"
echo " systemctl restart ser2net conmand"
echo "============================================"
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/bash
# shellcheck disable=SC2012,SC2001 # diagnostic script; ls -la listings and sed line-prefixing are intentional
#
# console/validate-conman.sh — verify conman can actually reach devices via
# ser2net TCP ports and is capturing log output to files.
#
# This tests the real data path: conman → TCP 200X → ser2net → /dev/consoles/X → device
#
set -uo pipefail
TS_IP=$(tailscale ip -4)
LOGDIR="/var/log/conman"
echo "============================================"
echo " Conman Data Path + Log Validation"
echo " Host: $(hostname) TS IP: $TS_IP"
echo "============================================"
echo ""
echo "--- 1. conman.conf log settings ---"
grep -E "logdir|LOGDIR|^GLOBAL LOG" /etc/conman.conf 2>/dev/null | grep -v "^#" || echo " (no explicit logdir — defaults to /var/log/conman)"
echo " Log dir: $LOGDIR"
ls -la "$LOGDIR"/ 2>/dev/null | head -15 || echo " ($LOGDIR does not exist yet)"
echo ""
echo "--- 2. CONSOLE entries: each has a log= directive? ---"
# Extract the auto-generated block and check each CONSOLE line has log=
sed -n '/BEGIN PFV CONSOLE/,/END PFV CONSOLE/p' /etc/conman.conf | grep "^CONSOLE" | while read -r line; do
name=$(echo "$line" | sed -n 's/.*name="\([^"]*\)".*/\1/p')
if echo "$line" | grep -q 'log='; then
logfile=$(echo "$line" | sed -n 's/.*log="\([^"]*\)".*/\1/p')
echo " [OK] $name → log=$logfile"
else
echo " [FAIL] $name has NO log= directive"
fi
done
echo ""
echo "--- 3. Trigger log capture: connect to each console briefly ---"
# conman -e changes the escape char. We use -j (join, read-only) with a timeout.
# Actually, conman doesn't have a built-in "connect for N seconds" — but conmand
# connects to each device ON STARTUP and keeps the connection open for logging.
# The log files should already be created. Let's check timestamps.
echo " conmand connects to all consoles on startup. Checking if logs exist..."
echo ""
echo "--- 4. Log file inventory ---"
for name in pfv-core-sw01 pfv-tor3-mgmt pfv-tor3-stor pfv-rrinfra-rtr pfv-r2-tor-top subodev-torsw pfv-r2-sw; do
logfile="$LOGDIR/${name}.log"
if [ -f "$logfile" ]; then
SIZE=$(stat -c%s "$logfile" 2>/dev/null || echo 0)
MTIME=$(stat -c%y "$logfile" 2>/dev/null | cut -d. -f1)
echo " [OK] $logfile ($SIZE bytes, modified $MTIME)"
else
echo " [MISSING] $logfile — conmand may not be writing yet"
fi
done
echo ""
echo "--- 5. conmand connection status (journal) ---"
# conmand logs connection attempts/errors to syslog
journalctl -u conmand --no-pager -n 50 2>/dev/null | grep -iE "connect|error|fail|console|refused|timeout" | tail -15 || echo " (no relevant journal entries)"
echo ""
echo "--- 6. Verify ser2net is proxying data (telnet rfc2217) ---"
echo " Probing TCP $TS_IP:2007 for data..."
RESPONSE=$(timeout 3 bash -c "printf '\r\r' | nc -w 2 $TS_IP 2007 2>/dev/null" | tr -cd '[:print:][:space:]' | head -5)
if [ -n "$RESPONSE" ]; then
echo " [OK] Data flowing through ser2net TCP 2007:"
echo "$RESPONSE" | sed 's/^/ /'
else
echo " (no immediate response — device may need more interaction)"
fi
echo ""
echo "--- 7. Check if conmand has open connections to ser2net ports ---"
CONMAND_PID=$(pgrep -x conmand 2>/dev/null || echo "")
if [ -n "$CONMAND_PID" ]; then
echo " conmand PID: $CONMAND_PID"
echo " Open connections to ser2net (expect 7 to 100.x:200X):"
ss -tnp 2>/dev/null | grep "pid=$CONMAND_PID" | grep -oE "100\.[0-9.]+:200[0-9]" | sort | sed 's/^/ /'
COUNT=$(ss -tnp 2>/dev/null | grep "pid=$CONMAND_PID" | grep -c ":200")
echo " Total conmand→ser2net connections: $COUNT (expect 7)"
else
echo " [FAIL] conmand not running"
fi
echo ""
echo "============================================"
+111
View File
@@ -0,0 +1,111 @@
# Powerman PDU Management
Centralized power management for the Cyclades AlterPath PM10i PDU via
[Powerman](https://github.com/chaos/powerman), running on pfv-tsys1.
## Hardware
| Component | Details |
|-----------|---------|
| **PDU** | Cyclades AlterPath PM10i (10 controllable AC outlets) |
| **Firmware** | v1.9.0 (Aug 4, 2006) |
| **Connection** | USB-to-DB9 adapter (Prolific pl2303, serial BJAAb144J07) |
| **Host** | pfv-tsys1 (OptiPlex 9020, Proxmox) |
| **Serial** | 9600 baud, 8N1, raw mode |
| **Credentials** | Factory defaults: `admin` / `pm8` (in cyclades-pm10.dev) |
| **Network access** | powermand listens on `127.0.0.1:10101` (local) + `100.121.189.98:10101` (Tailscale) |
## Device mapping
```
USB adapter (067b:23a3, serial BJAAb144J07)
└─ pl2303 driver → /dev/ttyUSB1
└─ udev symlink → /dev/cyclades-pm10 (stable across reboots)
└─ powermand reads/writes serial → Cyclades PM10i
└─ 10 outlets (factory default names: 1-10)
```
The udev rule (`/etc/udev/rules.d/99-cyclades-pdu.rules`) pins the adapter
by its USB serial number, so the symlink survives replugs and reboots.
## Scripts
All scripts run on the target host (pfv-tsys1) via `tests/remote.sh`:
```bash
# Setup (idempotent — safe to re-run):
PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/setup.sh
# Validate PDU control (cycles outlet 10 off → on):
PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/test-pdu.sh
# Status check:
PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/status.sh
```
### Customizing for other hosts/PDUs
The setup script accepts environment overrides:
```bash
PDU_SERIAL=XXXX PDU_VENDOR=067b PDU_OUTLETS=20 PDU_TYPE=pm20 \
PROX_HOST=other-host bash tests/remote.sh prox-file powerman/setup.sh
```
## Usage (daily operations)
From pfv-tsys1 (or any host with network access to port 10101):
```bash
# List all outlets
powerman -l
# Query status (all outlets)
powerman -q
# Turn outlet off
powerman -0 outlet-10
# Turn outlet on
powerman -1 outlet-10
# Cycle outlet (off → 4s delay → on)
powerman -c outlet-10
# Query a specific outlet
powerman -q outlet-10
```
### Remote access from other hosts
powermand listens on `0.0.0.0:10101`. From another tailnet host:
```bash
powerman --server-host pfv-tsys1 --server-port 10101 -q
```
Or set `POWERMAN_SERVER=pfv-tsys1:10101` in the environment.
## Configuration files on pfv-tsys1
| File | Purpose |
|------|---------|
| `/etc/udev/rules.d/99-cyclades-pdu.rules` | Stable symlink for USB-DB9 adapter |
| `/etc/powerman/powerman.conf` | Device definition + 10 outlet nodes |
| `/etc/powerman/cyclades-pm10.dev` | Cyclades PM10 protocol spec (shipped with powerman) |
## Validation results
2026-07-28: All 8 checks passed.
Outlet 10 turned OFF (confirmed), turned ON (confirmed), then cycled.
## TODO (Friday onsite)
- [ ] **Rename outlets** in `/etc/powerman/powerman.conf` to match the
physical devices plugged into each outlet (e.g., `node "tsys4-psu"
"cyclades-pm10" "3"`). Currently all outlets are generically named
`outlet-1` through `outlet-10`.
- [ ] **Change PDU admin password** from factory default (`pm8`) if
security-sensitive. Update `/etc/powerman/cyclades-pm10.dev` login
script to match.
- [ ] **Verify all 10 outlets** individually once device mapping is known.
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/bash
#
# powerman/discover.sh — gather USB-DB9 adapter + powerman state on a host
#
# Usage: PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/discover.sh
#
set -uo pipefail
echo "============================================"
echo " PDU / Powerman Discovery"
echo " Host: $(hostname)"
echo " Date: $(date)"
echo "============================================"
echo ""
echo "=== 1. USB devices ==="
lsusb 2>/dev/null || echo "(lsusb not available)"
echo ""
echo "=== 2. USB-Serial adapters (ttyUSB*) ==="
ls -la /dev/ttyUSB* 2>/dev/null || echo "(no /dev/ttyUSB* devices)"
echo ""
echo "=== 3. USB-Serial kernel modules ==="
lsmod | grep -iE 'usbserial|ftdi|pl2303|cp210|ch34|cdc_acm' 2>/dev/null || echo "(no relevant modules loaded)"
echo ""
echo "=== 4. dmesg for USB serial (last 30 lines) ==="
dmesg | grep -iE 'ttyUSB|usbserial|ftdi|pl2303|cp210|ch34|converter' | tail -30 2>/dev/null || echo "(no dmesg matches)"
echo ""
echo "=== 5. All serial devices ==="
ls -la /dev/ttyS* /dev/ttyUSB* /dev/ttyACM* 2>/dev/null || echo "(no serial devices found)"
echo ""
echo "=== 6. Powerman installed? ==="
dpkg -l powerman 2>/dev/null || echo "(powerman not installed)"
which powerman 2>/dev/null || echo "(powerman binary not found)"
which powermand 2>/dev/null || echo "(powermand binary not found)"
echo ""
echo "=== 7. Powerman config files ==="
ls -la /etc/powerman/ 2>/dev/null || echo "(no /etc/powerman/ directory)"
ls -la /etc/powerman/*.dev 2>/dev/null || echo "(no .dev files)"
cat /etc/powerman/powerman.conf 2>/dev/null || echo "(no powerman.conf)"
echo ""
echo "=== 8. Available powerman device definitions ==="
ls /usr/share/powerman/*.dev 2>/dev/null || ls /etc/powerman/*.dev 2>/dev/null || echo "(no device definitions found)"
echo ""
echo "=== 9. Powermand service status ==="
systemctl status powerman 2>/dev/null | head -10 || echo "(powerman service not found)"
echo ""
echo "=== 10. Serial port test (quick probe of /dev/ttyUSB0) ==="
if [ -e /dev/ttyUSB0 ]; then
stty -F /dev/ttyUSB0 2>/dev/null && echo "(port exists and is configurable)" || echo "(port exists but stty failed)"
# Try to read any pending output
timeout 2 cat /dev/ttyUSB0 2>/dev/null | head -5 || echo "(no immediate output from port)"
else
echo "(no /dev/ttyUSB0)"
fi
echo ""
echo "============================================"
echo " Discovery complete."
echo "============================================"
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/bash
#
# powerman/query-remote.sh — install powerman client locally and query
# the Cyclades PDU running on pfv-tsys1 over Tailscale.
#
set -euo pipefail
REMOTE_HOST="${REMOTE_HOST:-pfv-tsys1}"
REMOTE_PORT="${REMOTE_PORT:-10101}"
echo "============================================"
echo " Powerman Remote PDU Query"
echo " Server: ${REMOTE_HOST}:${REMOTE_PORT} (Tailscale)"
echo "============================================"
# --- 1. Install powerman client if missing ---
if ! command -v powerman >/dev/null 2>&1; then
echo ""
echo "--- Installing powerman client ---"
if sudo -n true 2>/dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq powerman
else
echo " Passwordless sudo not available. Please run this command in a terminal:"
echo ""
echo " sudo apt-get update && sudo apt-get install -y powerman"
echo ""
echo " Then re-run this script."
exit 1
fi
else
echo " powerman client already installed."
fi
# --- 2. Verify connectivity ---
echo ""
echo "--- Connectivity check ---"
if timeout 3 bash -c "echo > /dev/tcp/${REMOTE_HOST}/${REMOTE_PORT}" 2>/dev/null; then
echo " [OK] ${REMOTE_HOST}:${REMOTE_PORT} reachable"
else
echo " [FAIL] Cannot reach ${REMOTE_HOST}:${REMOTE_PORT}"
echo " Is Tailscale up? Is powermand running on ${REMOTE_HOST}?"
exit 1
fi
export POWERMAN_SERVER="${REMOTE_HOST}:${REMOTE_PORT}"
# --- 3. List outlets ---
echo ""
echo "--- Outlets ---"
powerman -h "${REMOTE_HOST}:${REMOTE_PORT}" -l
# --- 4. Query status ---
echo ""
echo "--- Status ---"
powerman -h "${REMOTE_HOST}:${REMOTE_PORT}" -q
echo ""
echo "============================================"
echo " Done."
echo ""
echo " To control an outlet from this workstation:"
echo " powerman -h ${REMOTE_HOST}:${REMOTE_PORT} -0 outlet-10 # off"
echo " powerman -h ${REMOTE_HOST}:${REMOTE_PORT} -1 outlet-10 # on"
echo " powerman -h ${REMOTE_HOST}:${REMOTE_PORT} -c outlet-10 # cycle"
echo "============================================"
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/bash
#
# powerman/setup.sh — idempotent powerman setup for Cyclades PM10i PDU
#
# Creates a stable udev symlink for the USB-DB9 adapter, writes powerman.conf
# with 10 outlet nodes, and enables + starts powermand.
#
# This script is designed to be run ON the target host (pfv-tsys1) as root.
# It is idempotent: safe to run multiple times.
#
# Usage:
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/setup.sh
#
# Override defaults via environment variables:
# PDU_SERIAL — USB adapter serial (default: BJAAb144J07)
# PDU_VENDOR — USB vendor ID (default: 067b)
# PDU_DEV_NAME — udev symlink name (default: cyclades-pm10)
# PDU_BAUD — serial baud rate (default: 9600,8n1)
# PDU_TYPE — powerman spec type (default: pm10)
# PDU_OUTLETS — number of outlets (default: 10)
# PDU_LISTEN — powermand listen (default: 0.0.0.0:10101)
#
set -euo pipefail
# --- Config (overridable via env) ---
PDU_SERIAL="${PDU_SERIAL:-BJAAb144J07}"
PDU_VENDOR="${PDU_VENDOR:-067b}"
PDU_DEV_NAME="${PDU_DEV_NAME:-cyclades-pm10}"
PDU_BAUD="${PDU_BAUD:-9600,8n1}"
PDU_TYPE="${PDU_TYPE:-pm10}"
PDU_OUTLETS="${PDU_OUTLETS:-10}"
PDU_LISTEN="${PDU_LISTEN:-}" # Auto-detect Tailscale IP if empty
UDEV_RULE="/etc/udev/rules.d/99-cyclades-pdu.rules"
POWERMAN_CONF="/etc/powerman/powerman.conf"
DEV_FILE="/etc/powerman/cyclades-pm10.dev"
# --- Auto-detect Tailscale IP for listen address ---
if [ -z "$PDU_LISTEN" ]; then
TS_IP=$(tailscale ip -4 2>/dev/null || true)
if [ -n "$TS_IP" ]; then
PDU_LISTEN="${TS_IP}:10101"
echo " Auto-detected Tailscale IP: $TS_IP"
else
PDU_LISTEN="127.0.0.1:10101"
echo " WARNING: No Tailscale IP detected. Defaulting to localhost."
fi
fi
echo "============================================"
echo " Powerman PDU Setup"
echo " Host: $(hostname)"
echo " PDU: Cyclades PM${PDU_OUTLETS}i"
echo " Adapter serial: $PDU_SERIAL"
echo " Device symlink: /dev/$PDU_DEV_NAME"
echo " Listen: $PDU_LISTEN (Tailscale only)"
echo "============================================"
# --- 1. Ensure powerman is installed ---
echo ""
echo "--- [1/5] Checking powerman installation ---"
if ! dpkg -l powerman 2>/dev/null | grep -q '^ii'; then
echo " Installing powerman from Debian repo..."
apt-get update -qq && apt-get install -y -qq powerman
else
echo " Powerman already installed: $(dpkg -l powerman | awk '/^ii/{print $3}')"
fi
# --- 2. Create udev rule for stable device name ---
echo ""
echo "--- [2/5] Creating udev rule for USB-DB9 adapter ---"
cat > "$UDEV_RULE" <<UDEV
# Stable symlink for Cyclades PM10i PDU USB-DB9 adapter
# Generated by powerman/setup.sh
SUBSYSTEM=="tty", ATTRS{idVendor}=="${PDU_VENDOR}", ATTRS{serial}=="${PDU_SERIAL}", GROUP="dialout", MODE="0660", SYMLINK+="${PDU_DEV_NAME}"
UDEV
echo " Written: $UDEV_RULE"
# Trigger udev to create the symlink now
udevadm control --reload-rules 2>/dev/null || true
udevadm trigger --subsystem-match=tty 2>/dev/null || true
sleep 1
if [ -e "/dev/${PDU_DEV_NAME}" ]; then
echo " Device symlink active: /dev/${PDU_DEV_NAME} -> $(readlink -f "/dev/${PDU_DEV_NAME}")"
else
echo " WARNING: /dev/${PDU_DEV_NAME} not found yet. Adapter may be unplugged."
echo " Falling back to /dev/ttyUSB* discovery..."
# Try to find any ttyUSB device as fallback
for tty in /dev/ttyUSB*; do
if [ -e "$tty" ]; then
echo " Found: $tty (using as fallback)"
PDU_DEV_NAME="$(basename "$tty")"
break
fi
done
fi
# --- 2b. Ensure powermand user can access the serial device ---
echo ""
echo "--- [2b/5] Fixing serial device permissions ---"
if id powerman >/dev/null 2>&1; then
if id powerman | grep -qv dialout; then
usermod -aG dialout powerman
echo " Added 'powerman' user to 'dialout' group"
else
echo " 'powerman' already in 'dialout' group"
fi
else
echo " (no powerman user — service may run as root)"
fi
# --- 3. Write powerman.conf ---
echo ""
echo "--- [3/5] Writing powerman.conf ---"
# Build node definitions
NODES=""
for i in $(seq 1 "$PDU_OUTLETS"); do
NODES+="node \"outlet-${i}\" \"${PDU_DEV_NAME}\" \"${i}\"\n"
done
cat > "$POWERMAN_CONF" <<PMCONF
# Powerman configuration for Cyclades PM${PDU_OUTLETS}i PDU
# Generated by powerman/setup.sh on $(date)
# Device: /dev/${PDU_DEV_NAME} (USB-DB9 adapter serial ${PDU_SERIAL})
# Listen on localhost (for local admin) and Tailscale (for remote access)
listen "127.0.0.1:10101"
listen "${PDU_LISTEN}"
# Device specification for Cyclades PM10
include "${DEV_FILE}"
# The PDU device (serial-attached)
device "${PDU_DEV_NAME}" "${PDU_TYPE}" "/dev/${PDU_DEV_NAME}" "${PDU_BAUD}"
# Outlet nodes (rename these to match attached devices when onsite)
$(printf '%b' "$NODES")
PMCONF
echo " Written: $POWERMAN_CONF"
echo " Nodes defined: outlet-1 through outlet-${PDU_OUTLETS}"
# --- 4. Restart powermand ---
echo ""
echo "--- [4/5] Restarting powermand ---"
systemctl enable powerman 2>/dev/null || true
systemctl restart powerman 2>/dev/null || true
sleep 2
if systemctl is-active --quiet powerman; then
echo " powermand is running."
else
echo " WARNING: powermand failed to start. Check journalctl -u powerman"
journalctl -u powerman --no-pager -n 20 2>/dev/null || true
fi
# --- 5. Verify ---
echo ""
echo "--- [5/5] Verification ---"
echo ""
echo " powerman -l (list all outlets):"
powerman -l 2>&1 || echo "(powerman -l failed)"
echo ""
echo " powerman -q (query status):"
powerman -q 2>&1 || echo "(powerman -q failed — PDU may need a moment)"
echo ""
echo "============================================"
echo " Setup complete."
echo ""
echo " Outlet names are generic (outlet-1 ... outlet-${PDU_OUTLETS})."
echo " Rename them in ${POWERMAN_CONF} when onsite to match attached devices."
echo ""
echo " Test: powerman -0 outlet-10 (off)"
echo " powerman -1 outlet-10 (on)"
echo " powerman -c outlet-10 (cycle)"
echo " powerman -q (status)"
echo "============================================"
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/bash
#
# powerman/status.sh — quick PDU status check
#
# Usage:
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/status.sh
#
set -euo pipefail
echo "============================================"
echo " Cyclades PM10i PDU Status"
echo " Host: $(hostname) $(date)"
echo "============================================"
echo ""
echo "=== Service ==="
systemctl is-active powerman 2>/dev/null && echo "(running)" || echo "(stopped)"
echo ""
echo "=== Device ==="
ls -la /dev/cyclades-pm10 2>/dev/null || echo "(no /dev/cyclades-pm10 symlink)"
echo ""
echo "=== Outlets ==="
powerman -l 2>&1
echo ""
echo "=== Power Status ==="
powerman -q 2>&1
echo ""
echo "=== Temperature ==="
powerman -T 2>&1 || echo "(temperature not available)"
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/bash
#
# powerman/test-pdu.sh — validate PDU control by cycling outlet 10 off and on
#
# Usage:
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file powerman/test-pdu.sh
#
# Override: OUTLET=10 (which outlet to test)
#
set -euo pipefail
OUTLET="${OUTLET:-10}"
NODE="outlet-${OUTLET}"
PASS=0; FAIL=0
ok() { echo " [PASS] $1"; PASS=$((PASS+1)); }
fail() { echo " [FAIL] $1"; FAIL=$((FAIL+1)); }
echo "============================================"
echo " PDU Control Validation"
echo " Host: $(hostname)"
echo " Test: cycle outlet ${OUTLET} (off → wait → on)"
echo "============================================"
# --- 0. Powermand running? ---
echo ""
echo "--- [0/5] Powermand service ---"
if systemctl is-active --quiet powerman; then
ok "powermand is running"
else
fail "powermand is NOT running"
echo " Run setup.sh first."
exit 1
fi
# --- 1. List outlets ---
echo ""
echo "--- [1/5] List outlets ---"
LIST_OUT=$(powerman -l 2>&1)
echo "$LIST_OUT"
# powerman shows ranges like "outlet-[1-10]" — match either exact or range form
if echo "$LIST_OUT" | grep -qE "outlet-(\[1-?10\]|${OUTLET}\b)"; then
ok "Outlet '${NODE}' is defined"
else
fail "Outlet '${NODE}' not found in powerman -l"
exit 1
fi
# --- 2. Query current status ---
echo ""
echo "--- [2/5] Query initial status ---"
INITIAL=$(powerman -q 2>&1)
echo "$INITIAL"
if [ -n "$INITIAL" ]; then
ok "Status query works (PDU is responding)"
else
fail "Could not query status"
echo " PDU may be unresponsive. Check serial connection."
exit 1
fi
# --- 3. Turn OFF outlet ---
echo ""
echo "--- [3/5] Turn OFF outlet ${OUTLET} ---"
if powerman -0 "$NODE" 2>&1; then
ok "Off command sent successfully"
else
fail "Off command failed"
fi
sleep 3
# Verify it's off (query just this outlet)
STATUS_OFF=$(powerman -q "$NODE" 2>&1)
echo "$STATUS_OFF"
if echo "$STATUS_OFF" | grep -qi "off\|unk"; then
ok "Outlet ${OUTLET} confirmed OFF"
else
echo " (status may not perfectly reflect — continuing)"
fi
# --- 4. Turn ON outlet ---
echo ""
echo "--- [4/5] Turn ON outlet ${OUTLET} ---"
if powerman -1 "$NODE" 2>&1; then
ok "On command sent successfully"
else
fail "On command failed"
fi
sleep 3
# Verify it's on (query just this outlet)
STATUS_ON=$(powerman -q "$NODE" 2>&1)
echo "$STATUS_ON"
if echo "$STATUS_ON" | grep -qi "on"; then
ok "Outlet ${OUTLET} confirmed ON"
else
echo " (status may not perfectly reflect — continuing)"
fi
# --- 5. Cycle test (off → delay → on in one command) ---
echo ""
echo "--- [5/5] Cycle test (powerman -c) ---"
if powerman -c "$NODE" 2>&1; then
ok "Cycle command completed"
else
fail "Cycle command failed"
echo " (some PDU firmware reports errors during cycle but still works)"
fi
sleep 5
# Final status
echo ""
echo "--- Final status ---"
powerman -q 2>&1
echo ""
echo "============================================"
echo " Results: $PASS passed, $FAIL failed"
if [ "$FAIL" -gt 0 ]; then
echo " Some checks failed. Review output above."
exit 1
fi
echo " PDU control validated."
echo "============================================"
+210
View File
@@ -0,0 +1,210 @@
# UPS Management (NUT — Network UPS Tools)
Centralized UPS monitoring for the server room via
[NUT](https://networkupstools.org/), running on **pfv-tsys1**. USB HID UPS
units feed one `upsd` network server; Home Assistant polls it over Tailscale for
real-time power/load/runtime tracking, and a local `upsmon` shuts the hypervisor
down gracefully when battery is low.
> **Why NUT (not apcupsd)?** Two different UPS brands (APC + Tripp Lite) must be
> covered. `apcupsd` only supports APC, so it would require a second daemon
> stack. NUT's `usbhid-ups` driver speaks to **both** via the USB HID Power
> Device class, and Home Assistant ships a first-class NUT integration.
## Hardware
| UPS | Model | VID:PID | USB Serial | Status |
|-----|-------|---------|------------|--------|
| **APC** | Smart-UPS C 1500 (FW 02.2) | `051d:0003` | `AS1213210423` | **LIVE** |
| **Tripp Lite** | UPS (HID PDC) | `09ae:3016` | `2352CVLSM871900694` | **Blocked** — see below |
## Current State (2026-07-30)
### APC Smart-UPS C 1500 — OPERATIONAL
Fully reporting via `usbhid-ups` + `APC HID 0.100` subdriver. Data validated:
```
battery.charge: 100 battery.runtime: 1800 battery.voltage: 27.4
ups.status: OL ups.load: (via HA) ups.model: Smart-UPS C 1500
```
### Tripp Lite UPS — BLOCKED (hardware issue)
The driver finds the device, matches the `TrippLite HID 0.85` subdriver, claims
the interface, and reads the HID descriptor — but **fails reading the 878-byte
HID Report Descriptor** (`Resource temporarily unavailable` / EAGAIN after 5s).
The driver is masked to prevent restart-loop spam.
USB descriptors (manufacturer, product, serial) are readable via `lsusb -v` and
`nut-scanner`, but the bulk control transfer for the full report descriptor
times out. Likely causes:
1. **USB hub** — the Tripp Lite is behind a Genesys Logic hub (`05e3:0608`).
Try plugging directly into a motherboard USB port.
2. **USB cable** — try a high-quality data cable (not charge-only).
3. **UPS firmware** — the USB controller may not properly implement all HID
endpoints.
**To retry after physical reseat:**
```bash
# On pfv-tsys1:
systemctl unmask nut-driver@tripp-lite-ups
systemctl start nut-driver@tripp-lite-ups
upsc tripp-lite-ups@localhost
```
## Architecture
```
pfv-tsys1 (192.168.3.11 / Tailscale 100.121.189.98)
├─ APC Smart-UPS C 1500 ──┐
└─ Tripp Lite UPS (masked) ──┤ USB HID
nut-driver@apc-smartups-c1500 (usbhid-ups)
upsd :3493 (LISTEN 127.0.0.1 + Tailscale + LAN)
▼ ▼
upsmon (local) Home Assistant (NUT integration)
graceful shutdown via LAN 192.168.3.11 (HAOS can't
route to Tailscale IPs)
```
- **Driver layer** — `usbhid-ups` process, pinned by USB serial. Debian uses
templated `nut-driver@<upsname>.service` units managed by
`nut-driver-enumerator`.
- **Server layer** — `upsd` exposes UPS data on TCP 3493 (localhost + Tailscale
+ LAN). Clients authenticate via `upsd.users`.
- **Monitor layer** — `upsmon` runs locally as `master` to trigger
`SHUTDOWNCMD` (`/sbin/shutdown -h now`) when a UPS reports `LOWBATT`.
- **Home Assistant** — native NUT integration connects to `upsd` over Tailscale
and exposes `ups.load`, `battery.runtime`, `ups.status`, etc. as sensors.
### Key deployment lesson: udev must cover raw USB devices
The `usbhid-ups` driver opens `/dev/bus/usb/BBB/DDD` (raw USB device files),
**not** `/dev/hidraw*`. After calling `setuid(111)` to drop to the `nut` user,
it needs write access to those raw USB files. The udev rule must match
`SUBSYSTEM=="usb"` by vendor/product ID to set `GROUP="nut"` — matching only
`hidraw` is insufficient. See `/etc/udev/rules.d/99-nut-ups.rules`.
## Scripts
NUT host scripts run on pfv-tsys1 via `tests/remote.sh`:
```bash
# Idempotent install + configure (safe to re-run):
PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/setup.sh
# Discover USB UPS + NUT state (read-only diagnostic):
PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/discover.sh
# Query UPS data + service health:
PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/status.sh
```
The HA integration script runs from your workstation (needs HA API access):
```bash
# Add the NUT integration to Home Assistant (idempotent):
bash ups/setup-ha-nut.sh
```
`setup.sh` accepts environment overrides for serials/VIDs/PIDs/usernames, so it
can be repurposed for other hosts or UPS units. Passwords for `monuser` and
`homeassistant` are auto-generated on first run and reused on subsequent runs
(stored in `/etc/nut/upsd.users`).
Set `TRIPP_ENABLED=0` to skip the Tripp Lite entirely (useful if it's physically
unplugged).
## Configuration files on pfv-tsys1
| File | Purpose |
|------|---------|
| `/etc/udev/rules.d/99-nut-ups.rules` | Grant nut group rw on raw USB + hidraw devices (both subsystems) |
| `/etc/nut/ups.conf` | `usbhid-ups` device(s), pinned by serial + subdriver |
| `/etc/nut/upsd.conf` | `LISTEN 127.0.0.1` + `LISTEN <tailscale>` + `LISTEN <lan>` on port 3493 |
| `/etc/nut/upsd.users` | `monuser` (master) + `homeassistant` (read-only) credentials |
| `/etc/nut/upsmon.conf` | Local master monitor + `SHUTDOWNCMD` |
| `/etc/nut/nut.conf` | `MODE=netserver` |
## Home Assistant integration
The NUT integration is added automatically by `setup-ha-nut.sh`, which drives
HA's REST config-flow API. It is idempotent (skips if the entry exists).
```bash
# Prerequisites: create token + password files (one-time):
mkdir -p ~/.config/pfvcluster
# HA → Profile → Long-Lived Access Tokens → Create Token:
echo -n 'YOUR_HA_TOKEN' > ~/.config/pfvcluster/ha-token
# Password is in /etc/nut/upsd.users on pfv-tsys1 (the homeassistant user):
echo -n 'YOUR_NUT_PASS' > ~/.config/pfvcluster/nut-password
chmod 600 ~/.config/pfvcluster/{ha-token,nut-password}
# Run:
bash ups/setup-ha-nut.sh
```
### Why LAN IP, not Tailscale
upsd listens on **both** the Tailscale IP (`100.121.189.98`) **and** the LAN IP
(`192.168.3.11`). The HA NUT integration uses the **LAN IP** because HAOS runs
Tailscale as an isolated add-on container — the HA core container cannot route
to Tailscale IPs. Since pfv-bms (HA, `192.168.3.12`) and pfv-tsys1 (`192.168.3.11`)
share the same vmbr0 bridge, LAN connectivity is instant and reliable.
### Manual UI alternative
In Home Assistant → **Settings → Devices & Services → Add Integration → NUT**:
| Field | Value |
|-------|-------|
| Host | `192.168.3.11` (LAN — HAOS can't reach Tailscale IPs from the HA container) |
| Port | `3493` |
| Username | `homeassistant` |
| Password | *(stored in `/etc/nut/upsd.users` on pfv-tsys1)* |
| UPS | `apc-smartups-c1500` |
### Live sensors
HA exposes UPS data as sensors (prefix `sensor.apc_smartups_c1500_`):
`battery_charge`, `status` (Online/On Battery), `status_data` (OL/OB/DISCHRG).
Additional sensors (load, runtime, voltage) populate as the UPS reports them.
## Daily operations
From pfv-tsys1 (or any tailnet host with NUT client installed):
```bash
# List UPS units served by upsd
upsc -l pfv-tsys1
# Full variable dump for one UPS
upsc apc-smartups-c1500@pfv-tsys1
# Battery runtime (the only runtime/charge data this UPS exposes)
upsc apc-smartups-c1500@pfv-tsys1 battery.runtime
```
## Notes
- **No USB passthrough to the HA VM.** Keeping the UPS on the host preserves
hypervisor graceful-shutdown capability and matches the `powerman/` pattern
(PDU managed on the host where the adapter physically lives).
- **No `ups.load` / `ups.realpower` on this UPS (FW 02.2, mfg 2012):** The
APC Smart-UPS C 1500 does not expose load or power data over USB HID.
Both NUT `usbhid-ups` and `apcupsd` (USB mode, tested 2026-07-30) read the
same HID descriptor — the variable simply isn't there. This means the HA
NUT integration provides **battery/runtime/status sensors only**, not
wattage for the Energy Dashboard.
- **apcupsd test note:** Debian's `apcupsd` package conflicts with
`nut-server` (mutually exclusive). apcupsd USB mode returned `COMMLOST`
even before we could check load. The APC Smart Serial protocol (serial
cable, AP940-1524C, ~$30) DOES report load%, but this requires a serial
port on the UPS and on the host.
- **Energy Dashboard path:** A smart plug (Shelly Plug S / TP-Link Kasa,
~$15-25) on the UPS output reports real watts natively and feeds the
Energy Dashboard with zero UPS-driver hacking. The NUT sensors remain
valuable for outage detection and graceful-shutdown automations.
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/bash
#
# ups/discover.sh — probe USB UPS units and NUT state on the local host
#
# Read-only. Prints everything needed to configure NUT. No changes made.
#
# Usage (run ON the target host via remote.sh):
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/discover.sh
#
set -uo pipefail
echo "======================================================"
echo " UPS / NUT Discovery on $(hostname)"
echo "======================================================"
# --- 1. USB UPS devices ------------------------------------------------------
echo ""
echo "--- [1/5] USB UPS devices (lsusb) ---"
lsusb 2>/dev/null | grep -iE "UPS|American Power|Tripp|APC" || echo " (no UPS devices found in lsusb)"
echo ""
echo "--- [2/5] UPS detail (vendor/product/serial/model) ---"
# Common UPS vendor IDs: 051d (APC), 09ae (Tripp Lite), 0463 (Eaton),
# 06da (MGE), 0764 (Cyber Power)
for vid in 051d 09ae 0463 06da 0764; do
while read -r bus dev pid; do
[ -n "$bus" ] || continue
echo " --- $bus:$dev ($vid:$pid) ---"
lsusb -v -s "${bus}:${dev}" 2>/dev/null \
| grep -iE "iManufacturer|iProduct|iSerial|bcdDevice" \
| sed 's/^/ /'
done < <(lsusb 2>/dev/null | awk -v v="$vid" '$0~v{split($2,a,":"); split($4,b,":"); print a[1], b[1], $6}')
done
# --- 2. sysfs paths (for udev rules) -----------------------------------------
echo ""
echo "--- [3/5] sysfs device paths + serials ---"
for d in /sys/bus/usb/devices/*; do
man=$(cat "$d/manufacturer" 2>/dev/null)
prod=$(cat "$d/product" 2>/dev/null)
ser=$(cat "$d/serial" 2>/dev/null)
vid=$(cat "$d/idVendor" 2>/dev/null)
pid=$(cat "$d/idProduct" 2>/dev/null)
if echo "$man $prod" | grep -qiE "apc|tripp|power conversion|ups|eaton|mge|cyber power"; then
# Resolve stable ID_PATH for udev pinning
path=$(udevadm info -q property -p "$d" 2>/dev/null | awk -F= '/^ID_PATH=/{print $2}')
echo " $d"
echo " vendor=$vid product=$pid"
echo " manufacturer=$man"
echo " product=$prod"
echo " serial=$ser"
echo " ID_PATH=$path"
fi
done
# --- 3. HID device nodes -----------------------------------------------------
echo ""
echo "--- [4/5] HID device nodes ---"
ls -la /dev/hidraw* /dev/usb/hiddev* 2>/dev/null || echo " (no hidraw/hiddev nodes)"
# --- 4. NUT install state ----------------------------------------------------
echo ""
echo "--- [5/5] NUT install + service state ---"
if dpkg -l nut-server nut-client 2>/dev/null | grep -q '^ii'; then
echo " NUT installed:"
dpkg -l nut-server nut-client 2>/dev/null | awk '/^ii/{print " "$2" "$3}'
else
echo " NUT not installed (apt: nut-server nut-client)"
fi
echo ""
echo " Services:"
for svc in nut-driver nut-server nut-monitor; do
printf " %-14s " "$svc:"
systemctl is-active "$svc" 2>/dev/null || true
done
echo ""
echo " Existing config:"
# shellcheck disable=SC2012 # ls -la is intentional for human-readable listing
ls -la /etc/nut/ 2>/dev/null | sed 's/^/ /' || echo " (no /etc/nut)"
echo ""
echo "======================================================"
echo " Discovery complete."
echo "======================================================"
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""
ha-nut-setup.py — Add the Home Assistant NUT integration via REST config-flow API.
Stdlib-only (no pip). Idempotent: skips if a NUT config entry already exists.
Env:
HA_HOST (default pfv-bms.knel.net)
HA_PORT (default 8123)
HA_TOKEN (long-lived access token)
NUT_HOST (default 100.121.189.98)
NUT_PORT (default 3493)
NUT_USER (default homeassistant)
NUT_PASS (required)
NUT_UPS (default apc-smartups-c1500)
"""
import os, json, sys, time, urllib.request, urllib.error
HA_HOST = os.environ.get("HA_HOST", "pfv-bms.knel.net")
HA_PORT = int(os.environ.get("HA_PORT", "8123"))
TOKEN = os.environ["HA_TOKEN"]
NUT_HOST = os.environ.get("NUT_HOST", "192.168.3.11")
NUT_PORT = int(os.environ.get("NUT_PORT", "3493"))
NUT_USER = os.environ.get("NUT_USER", "homeassistant")
NUT_PASS = os.environ["NUT_PASS"]
NUT_UPS = os.environ.get("NUT_UPS", "apc-smartups-c1500")
BASE = f"http://{HA_HOST}:{HA_PORT}"
def api(method, path, data=None):
body = json.dumps(data).encode() if data else None
req = urllib.request.Request(
f"{BASE}/api{path}", data=body, method=method,
headers={"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=20) as r:
return json.loads(r.read())
except urllib.error.HTTPError as e:
raw = e.read().decode()
try:
return json.loads(raw)
except Exception:
return {"_http_error": e.code, "_raw": raw[:300]}
except Exception as e:
return {"_error": str(e)}
# ── verify token ──
cfg = api("GET", "/config")
if "_http_error" in cfg or "_error" in cfg:
print(f"Cannot reach HA or token invalid: {cfg}"); sys.exit(1)
print(f"HA {cfg.get('version')} — token valid")
# ── check existing entries (idempotent) ──
entries = api("GET", "/config/config_entries/entry")
existing = [e for e in entries if e.get("domain") == "nut"]
if existing:
for e in existing:
print(f"NUT already configured: {e.get('title')} "
f"(data={json.dumps(e.get('data', {}))})")
print("Skipping — delete it in HA UI first if you want to re-run.")
sys.exit(0)
print("No existing NUT entry. Starting config flow.")
# ── initiate flow ──
flow = api("POST", "/config/config_entries/flow", {"handler": "nut"})
if "flow_id" not in flow:
print(f"Flow init failed: {json.dumps(flow)}"); sys.exit(1)
fid = flow["flow_id"]
print(f"Flow started: step={flow.get('step_id')} "
f"fields={[f.get('name') for f in flow.get('data_schema', [])]}")
# ── submit connection details ──
creds = {"host": NUT_HOST, "port": NUT_PORT,
"username": NUT_USER, "password": NUT_PASS}
flow = api("POST", f"/config/config_entries/flow/{fid}", creds)
if flow.get("errors"):
print(f"Validation errors: {flow['errors']}"); sys.exit(1)
print(f"After submit: type={flow.get('type')} step={flow.get('step_id')}")
# ── handle follow-up steps (UPS selection etc.) ──
while flow.get("type") == "form":
step = flow.get("step_id", "?")
schema = flow.get("data_schema", [])
print(f"Step '{step}': fields={[f.get('name') for f in schema]}")
for f in schema:
opts = f.get("options") or f.get("values")
if opts:
print(f" {f.get('name')} options: {opts}")
submission = {}
for f in schema:
nm = f.get("name")
ftype = f.get("type", "")
if ftype == "multi_select":
opts = f.get("options", [])
vals = [o[0] if isinstance(o, list) else o for o in opts]
submission[nm] = [NUT_UPS] if NUT_UPS in vals else vals[:1]
elif nm in creds:
submission[nm] = creds[nm]
elif "default" in f:
submission[nm] = f["default"]
elif ftype == "select":
opts = f.get("options", [])
vals = [o[0] if isinstance(o, list) else o for o in opts]
submission[nm] = NUT_UPS if NUT_UPS in vals else (vals[0] if vals else "")
fid = flow.get("flow_id", fid)
flow = api("POST", f"/config/config_entries/flow/{fid}", submission)
if flow.get("errors"):
print(f"Validation errors: {flow['errors']}"); sys.exit(1)
print(f" -> type={flow.get('type')} step={flow.get('step_id')}")
# ── result ──
if flow.get("type") == "create_entry":
print(f"\nNUT integration created: {flow.get('title')}")
elif flow.get("type") == "abort":
print(f"\nFlow aborted: {flow.get('reason')}"); sys.exit(1)
else:
print(f"\nFinal state: {flow.get('type')}{json.dumps(flow)[:200]}")
# ── verify sensors ──
print("\nWaiting 10s for entities ...")
time.sleep(10)
states = api("GET", "/states")
ups = [s for s in states
if "apc_smartups" in s["entity_id"].lower()
or "sensor.ups_" in s["entity_id"].lower()]
if ups:
print(f"Found {len(ups)} UPS sensors:")
for e in sorted(ups, key=lambda x: x["entity_id"]):
st = e.get("state", "?")
unit = e.get("attributes", {}).get("unit_of_measurement", "")
name = e.get("attributes", {}).get("friendly_name", "")
print(f" {e['entity_id']:55s} {st:>8} {unit:4s} {name}")
else:
print("No UPS sensors yet (may still be initialising — check HA UI).")
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
#
# setup-ha-nut.sh — Add the Home Assistant NUT integration via REST API.
#
# Idempotent: skips if a NUT entry already exists. Reads secrets from
# ~/.config/pfvcluster/ (ha-token, nut-password) or env vars.
#
# Usage:
# bash ups/setup-ha-nut.sh
#
# Env overrides:
# HA_TOKEN HA long-lived access token
# NUT_PASS NUT upsd password for the homeassistant user
# HA_HOST HA host (default pfv-bms.knel.net)
# NUT_HOST upsd host (default 192.168.3.11 — LAN, see README)
# NUT_PORT upsd port (default 3493)
# NUT_USER upsd user (default homeassistant)
# NUT_UPS UPS name (default apc-smartups-c1500)
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONF_DIR="${PFV_CONF_DIR:-$HOME/.config/pfvcluster}"
# --- HA token ---
HA_TOKEN="${HA_TOKEN:-}"
if [[ -z "$HA_TOKEN" ]]; then
TOKEN_FILE="$CONF_DIR/ha-token"
if [[ -f "$TOKEN_FILE" ]]; then
HA_TOKEN="$(head -1 "$TOKEN_FILE" | tr -d '[:space:]')"
else
echo "error: no HA token. Set \$HA_TOKEN or create $TOKEN_FILE" >&2
echo " (HA → Profile → Long-Lived Access Tokens → Create Token)" >&2
exit 1
fi
fi
# --- NUT password ---
NUT_PASS="${NUT_PASS:-}"
if [[ -z "$NUT_PASS" ]]; then
PASS_FILE="$CONF_DIR/nut-password"
if [[ -f "$PASS_FILE" ]]; then
NUT_PASS="$(head -1 "$PASS_FILE" | tr -d '[:space:]')"
else
echo "error: no NUT password. Set \$NUT_PASS or create $PASS_FILE" >&2
echo " (value is in /etc/nut/upsd.users on the NUT host)" >&2
exit 1
fi
fi
# --- connection params (override for your own kit) ---
export HA_TOKEN
export NUT_PASS
export HA_HOST="${HA_HOST:-pfv-bms.knel.net}"
export HA_PORT="${HA_PORT:-8123}"
export NUT_HOST="${NUT_HOST:-192.168.3.11}"
export NUT_PORT="${NUT_PORT:-3493}"
export NUT_USER="${NUT_USER:-homeassistant}"
export NUT_UPS="${NUT_UPS:-apc-smartups-c1500}"
echo "HA: ${HA_HOST}:${HA_PORT}"
echo "NUT: ${NUT_USER}@${NUT_HOST}:${NUT_PORT} (UPS: ${NUT_UPS})"
echo ""
exec python3 "$SCRIPT_DIR/ha-nut-setup.py"
+298
View File
@@ -0,0 +1,298 @@
#!/usr/bin/bash
#
# ups/setup.sh — idempotent Network UPS Tools (NUT) setup on pfv-tsys1
#
# Installs NUT, configures two USB HID UPS units (APC + Tripp Lite) pinned by
# USB serial, runs upsd as a network server for Home Assistant polling, and
# runs upsmon locally so the hypervisor can shut down gracefully on battery.
#
# Designed to run ON the target host (pfv-tsys1) as root, idempotent.
#
# Usage:
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/setup.sh
#
# Overrides (defaults suit pfv-tsys1):
# APC_SERIAL APC UPS USB serial (default AS1213210423)
# APC_VID/APC_PID APC vendor/product ID (default 051d / 0003)
# APC_NAME NUT section name for APC (default apc-smartups-c1500)
# TRIPP_SERIAL Tripp Lite UPS USB serial (default 2352CVLSM871900694)
# TRIPP_VID/TRIPP_PID Tripp Lite vendor/product (default 09ae / 3016)
# TRIPP_NAME NUT section name for Tripp (default tripp-lite-ups)
# TRIPP_SUBDRIVER Forced HID subdriver for Tripp (default "TrippLite HID 0.85")
# TRIPP_ENABLED Set to 0 to disable Tripp Lite (default 1)
# NUT_LISTEN_IPS space-separated upsd LISTEN IPs (default: auto Tailscale + 127.0.0.1)
# HA_USER upsd username for HA (default homeassistant)
# HA_PASSWORD upsd password for HA (default: reuse or generate)
# MON_USER upsd username for local upsmon (default monuser)
# MON_PASSWORD upsd password for upsmon (default: reuse or generate)
#
set -euo pipefail
# --- Config (overridable via env) ---
APC_SERIAL="${APC_SERIAL:-AS1213210423}"
APC_VID="${APC_VID:-051d}"
APC_PID="${APC_PID:-0003}"
APC_NAME="${APC_NAME:-apc-smartups-c1500}"
TRIPP_SERIAL="${TRIPP_SERIAL:-2352CVLSM871900694}"
TRIPP_VID="${TRIPP_VID:-09ae}"
TRIPP_PID="${TRIPP_PID:-3016}"
TRIPP_NAME="${TRIPP_NAME:-tripp-lite-ups}"
TRIPP_SUBDRIVER="${TRIPP_SUBDRIVER:-TrippLite HID 0.85}"
TRIPP_ENABLED="${TRIPP_ENABLED:-1}"
HA_USER="${HA_USER:-homeassistant}"
MON_USER="${MON_USER:-monuser}"
NUT_PORT="${NUT_PORT:-3493}"
UDEV_RULE="/etc/udev/rules.d/99-nut-ups.rules"
UPS_CONF="/etc/nut/ups.conf"
UPSD_CONF="/etc/nut/upsd.conf"
UPSD_USERS="/etc/nut/upsd.users"
UPS_CONF_MON="/etc/nut/upsmon.conf"
NUT_CONF="/etc/nut/nut.conf"
# --- Helpers ---
gen_pw() { head -c 24 /dev/urandom | base64 | tr -d '/+=' | cut -c1-20; }
# Reuse existing passwords if config already present (idempotent re-runs)
extract_pw() { # $1=user
if [ -f "$UPSD_USERS" ]; then
awk -v u="[$1]" '
$0==u {inblk=1; next}
/^\[/ {inblk=0}
inblk && $1=="password" {gsub(/"/,"",$3); print $3; exit}
' "$UPSD_USERS" 2>/dev/null
fi
}
echo "============================================"
echo " NUT UPS Setup on $(hostname)"
echo " APC: $APC_NAME ($APC_VID:$APC_PID serial $APC_SERIAL)"
if [ "$TRIPP_ENABLED" = "1" ]; then
echo " Tripp Lite: $TRIPP_NAME ($TRIPP_VID:$TRIPP_PID serial $TRIPP_SERIAL)"
else
echo " Tripp Lite: DISABLED (TRIPP_ENABLED=0)"
fi
echo "============================================"
# --- 0. Resolve / generate passwords (idempotent) ---
MON_PASSWORD="${MON_PASSWORD:-$(extract_pw "$MON_USER")}"
HA_PASSWORD="${HA_PASSWORD:-$(extract_pw "$HA_USER")}"
[ -n "$MON_PASSWORD" ] || MON_PASSWORD="$(gen_pw)"
[ -n "$HA_PASSWORD" ] || HA_PASSWORD="$(gen_pw)"
# --- 0b. Auto-detect listen IPs for upsd ---
# Tailscale IP: tailnet clients (workstation, etc.)
# LAN IP: HAOS VMs where Tailscale runs as an isolated add-on (HA container
# cannot route to Tailscale IPs, so the shared-LAN bridge is required)
if [ -z "${NUT_LISTEN_IPS:-}" ]; then
NUT_LISTEN_IPS="127.0.0.1"
TS_IP=$(tailscale ip -4 2>/dev/null || true)
if [ -n "$TS_IP" ]; then
NUT_LISTEN_IPS="${NUT_LISTEN_IPS} ${TS_IP}"
else
echo " WARNING: No Tailscale IP detected."
fi
if [ "${NUT_INCLUDE_LAN:-1}" = "1" ]; then
LAN_IP=$(ip -4 addr show vmbr0 2>/dev/null | awk '/scope global/{print $2}' | cut -d/ -f1 | head -1)
if [ -z "$LAN_IP" ]; then
LAN_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
fi
if [ -n "$LAN_IP" ]; then
NUT_LISTEN_IPS="${NUT_LISTEN_IPS} ${LAN_IP}"
fi
fi
fi
echo " upsd LISTEN IPs: ${NUT_LISTEN_IPS:-<none>}"
# --- 1. Install NUT ---
echo ""
echo "--- [1/8] Installing NUT (nut-server, nut-client) ---"
if dpkg -l nut-server 2>/dev/null | grep -q '^ii'; then
echo " NUT already installed: $(dpkg -l nut-server | awk '/^ii/{print $3}')"
else
apt-get update -qq && apt-get install -y -qq nut-server nut-client
fi
mkdir -p /etc/nut
# --- 2. udev rules: grant nut group access to BOTH raw USB + hidraw devices ---
# CRITICAL: usbhid-ups opens /dev/bus/usb/BBB/DDD (raw USB), not /dev/hidraw.
# The driver drops to the nut user via setuid(), so the nut group needs write
# access to the raw USB device files. Matching on subsystem=="usb" by VID:PID
# is required because ATTRS{serial} does not reliably traverse for usb devices.
echo ""
echo "--- [2/8] Writing udev rules (raw USB + hidraw, group nut) ---"
{
echo "# Stable permissions for NUT USB HID UPS units"
echo "# Generated by ups/setup.sh — grants the 'nut' group access to both"
echo "# the raw USB device files (/dev/bus/usb) and hidraw devices."
echo "# Match BOTH subsystems: the usbhid-ups driver opens the raw USB device"
echo "# after dropping to the nut user via setuid()."
echo ""
echo "# APC Smart-UPS C 1500 ($APC_VID:$APC_PID)"
echo "SUBSYSTEM==\"usb\", ATTR{idVendor}==\"$APC_VID\", ATTR{idProduct}==\"$APC_PID\", GROUP=\"nut\", MODE=\"0664\""
echo "SUBSYSTEM==\"hidraw\", ATTRS{serial}==\"$APC_SERIAL\", GROUP=\"nut\", MODE=\"0660\""
echo ""
echo "# Tripp Lite UPS ($TRIPP_VID:$TRIPP_PID)"
echo "SUBSYSTEM==\"usb\", ATTR{idVendor}==\"$TRIPP_VID\", ATTR{idProduct}==\"$TRIPP_PID\", GROUP=\"nut\", MODE=\"0664\""
echo "SUBSYSTEM==\"hidraw\", ATTRS{serial}==\"$TRIPP_SERIAL\", GROUP=\"nut\", MODE=\"0660\""
} > "$UDEV_RULE"
echo " Written: $UDEV_RULE"
udevadm control --reload-rules 2>/dev/null || true
udevadm trigger --subsystem-match=usb 2>/dev/null || true
udevadm trigger --subsystem-match=hidraw 2>/dev/null || true
sleep 1
# --- 3. ups.conf ---
echo ""
echo "--- [3/8] Writing ups.conf ---"
{
echo "# NUT UPS devices — generated by ups/setup.sh on $(date)"
echo ""
echo "maxretry = 3"
echo ""
echo "[${APC_NAME}]"
echo " driver = usbhid-ups"
echo " port = auto"
echo " vendorid = ${APC_VID}"
echo " productid = ${APC_PID}"
echo " serial = ${APC_SERIAL}"
echo " desc = \"APC Smart-UPS C 1500\""
if [ "$TRIPP_ENABLED" = "1" ]; then
echo ""
echo "[${TRIPP_NAME}]"
echo " driver = usbhid-ups"
echo " port = auto"
echo " vendorid = ${TRIPP_VID}"
echo " productid = ${TRIPP_PID}"
echo " serial = ${TRIPP_SERIAL}"
echo " subdriver = \"${TRIPP_SUBDRIVER}\""
echo " desc = \"Tripp Lite UPS\""
fi
} > "$UPS_CONF"
echo " Written: $UPS_CONF"
# --- 4. upsd.conf: network server (localhost + Tailscale for HA) ---
echo ""
echo "--- [4/8] Writing upsd.conf ---"
{
echo "# NUT upsd — generated by ups/setup.sh on $(date)"
for ip in $NUT_LISTEN_IPS; do
echo "LISTEN ${ip} ${NUT_PORT}"
done
echo "MAXAGE 25"
} > "$UPSD_CONF"
echo " Written: $UPSD_CONF (LISTEN: $(echo "$NUT_LISTEN_IPS" | tr '\n' ' '))"
# --- 5. upsd.users: monuser (master) + homeassistant (read-only monitor) ---
echo ""
echo "--- [5/8] Writing upsd.users ---"
cat > "$UPSD_USERS" <<USERS
# NUT upsd users — generated by ups/setup.sh on $(date)
# monuser : local upsmon (master) — graceful hypervisor shutdown
# ${HA_USER} : Home Assistant NUT integration (read-only polling)
[${MON_USER}]
password = "${MON_PASSWORD}"
upsmon master
[${HA_USER}]
password = "${HA_PASSWORD}"
upsmon slave
USERS
echo " Written: $UPSD_USERS"
# --- 6. upsmon.conf + nut.conf ---
echo ""
echo "--- [6/8] Writing upsmon.conf + nut.conf ---"
{
echo "# NUT upsmon (local monitor) — generated by ups/setup.sh on $(date)"
echo "# Monitors the APC UPS as master. On battery-low, shuts the host down."
echo ""
echo "MONITOR ${APC_NAME}@localhost 1 ${MON_USER} \"${MON_PASSWORD}\" master"
if [ "$TRIPP_ENABLED" = "1" ]; then
echo "MONITOR ${TRIPP_NAME}@localhost 1 ${MON_USER} \"${MON_PASSWORD}\" master"
fi
echo ""
echo "SHUTDOWNCMD \"/sbin/shutdown -h now\""
echo "POWERDOWNFLAG /etc/killpower"
echo "NOTIFYFLAG ONLINE SYSLOG+WALL"
echo "NOTIFYFLAG ONBATT SYSLOG+WALL"
echo "NOTIFYFLAG LOWBATT SYSLOG+WALL"
echo "POLLFREQ 15"
echo "POLLFREQALERT 5"
echo "HOSTSYNC 15"
} > "$UPS_CONF_MON"
cat > "$NUT_CONF" <<NUTCONF
# NUT mode — generated by ups/setup.sh on $(date)
# netserver = this host runs drivers + upsd; serves UPS data to clients (HA).
MODE=netserver
NUTCONF
echo " Written: $UPS_CONF_MON + $NUT_CONF"
# --- 6b. Fix ownership/permissions (Debian: nut group must read config) ---
chown root:nut "$UPS_CONF" "$UPSD_CONF" "$UPSD_USERS" "$UPS_CONF_MON" 2>/dev/null || true
chmod 640 "$UPS_CONF" "$UPSD_CONF" "$UPSD_USERS" "$UPS_CONF_MON" 2>/dev/null || true
chmod 644 "$NUT_CONF" 2>/dev/null || true
# --- 7. Start services (Debian uses templated nut-driver@<name> units) ---
echo ""
echo "--- [7/8] Starting NUT services ---"
# Re-read ups.conf to generate per-UPS driver instances
systemctl restart nut-driver-enumerator 2>/dev/null || true
sleep 2
# Start per-UPS driver instances
systemctl restart "nut-driver@${APC_NAME}" 2>/dev/null || true
if [ "$TRIPP_ENABLED" = "1" ]; then
systemctl restart "nut-driver@${TRIPP_NAME}" 2>/dev/null || true
else
systemctl stop "nut-driver@${TRIPP_NAME}" 2>/dev/null || true
systemctl mask "nut-driver@${TRIPP_NAME}" 2>/dev/null || true
fi
sleep 3
systemctl restart nut-server 2>/dev/null || true
sleep 1
systemctl restart nut-monitor 2>/dev/null || true
echo ""
echo " Service status:"
for svc in "nut-driver@${APC_NAME}" "nut-driver@${TRIPP_NAME}" nut-server nut-monitor; do
if systemctl list-unit-files "$svc" >/dev/null 2>&1; then
printf " %-42s " "$svc"
systemctl is-active "$svc" 2>/dev/null || echo "(unknown)"
fi
done
# --- 8. Validate ---
echo ""
echo "--- [8/8] Validation ---"
echo ""
echo " upsc — ${APC_NAME}:"
upsc "${APC_NAME}@localhost" 2>&1 | head -25 || echo " (APC UPS not responding yet)"
if [ "$TRIPP_ENABLED" = "1" ]; then
echo ""
echo " upsc — ${TRIPP_NAME}:"
upsc "${TRIPP_NAME}@localhost" 2>&1 | head -25 || echo " (Tripp Lite UPS not responding yet)"
fi
echo ""
echo "============================================"
echo " Setup complete."
echo ""
echo " Home Assistant NUT integration:"
echo " Host: $(echo "$NUT_LISTEN_IPS" | awk '{print $2}') (or any LISTEN IP above)"
echo " Port: ${NUT_PORT}"
echo " Username: ${HA_USER}"
echo " Password: ${HA_PASSWORD}"
if [ "$TRIPP_ENABLED" = "1" ]; then
echo " UPS names: ${APC_NAME}, ${TRIPP_NAME}"
else
echo " UPS names: ${APC_NAME}"
fi
echo ""
echo " Save the HA password now — it is stored in ${UPSD_USERS}."
echo "============================================"
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/bash
#
# ups/status.sh — query NUT UPS state + service health (read-only)
#
# Usage (run ON the target host via remote.sh):
# PROX_HOST=pfv-tsys1 bash tests/remote.sh prox-file ups/status.sh
#
# shellcheck disable=SC2012 # ss/awk field extraction is intentional
set -uo pipefail
APC_NAME="${APC_NAME:-apc-smartups-c1500}"
TRIPP_NAME="${TRIPP_NAME:-tripp-lite-ups}"
echo "======================================================"
echo " NUT UPS Status on $(hostname)"
echo "======================================================"
echo ""
echo "--- Services ---"
for svc in "nut-driver@${APC_NAME}" "nut-driver@${TRIPP_NAME}" nut-server nut-monitor; do
if systemctl list-unit-files "$svc" >/dev/null 2>&1; then
printf " %-42s " "$svc"
systemctl is-active "$svc" 2>/dev/null || echo "(unknown)"
fi
done
for ups in "$APC_NAME" "$TRIPP_NAME"; do
echo ""
echo "--- ${ups} ---"
if upsc "${ups}@localhost" >/tmp/.nutstatus.$$ 2>&1; then
awk -v u="$ups" '
BEGIN{printf " %s\n", u}
/^battery\.charge:/ {printf " battery.charge: %s\n", $3}
/^battery\.runtime:/ {printf " battery.runtime: %ss (%.0f min)\n", $3, $3/60}
/^battery\.voltage:/ {printf " battery.voltage: %s\n", $3}
/^ups\.status:/ {printf " ups.status: %s\n", $3}
/^ups\.load:/ {printf " ups.load: %s%%\n", $3}
/^ups\.power:/ {printf " ups.power: %s\n", $3}
/^ups\.realpower:/ {printf " ups.realpower: %s W\n", $3}
/^input\.voltage:/ {printf " input.voltage: %s\n", $3}
/^output\.voltage:/ {printf " output.voltage: %s\n", $3}
/^ups\.model:/ {printf " ups.model: %s\n", $3}
/^ups\.serial:/ {printf " ups.serial: %s\n", $3}
/^device\.mfr:/ {printf " device.mfr: %s\n", $3}
' /tmp/.nutstatus.$$
echo " (full dump: upsc ${ups}@localhost)"
else
echo " NOT RESPONDING:"
sed 's/^/ /' /tmp/.nutstatus.$$
fi
rm -f /tmp/.nutstatus.$$
done
echo ""
echo "--- upsd LISTEN sockets ---"
ss -ltnp 2>/dev/null | grep -E "3493|nut" | sed 's/^/ /' || echo " (upsd not listening on 3493)"
echo ""
echo "======================================================"
+183
View File
@@ -0,0 +1,183 @@
# Technitium DNS Cluster Setup
Replicates the production Technitium DNS Server from `tailscale-router` to the
`pfv-netinfra-01/02` pair and configures them as a primary/secondary cluster
with automatic zone transfers.
## Architecture
```
tailscale-router (PRODUCTION — READ ONLY)
└─ tsys-dns container (technitium/dns-server)
└─ 124 zones (knel.net + reverse DNS)
└─ Users + 2FA in auth.config
docker cp (export)
┌─ pfv-netinfra-01 (192.168.3.252) ──── PRIMARY ──────────┐
│ tsys-dns container (Technitium on :5300) │
│ pihole container (Pi-hole on :53 → Technitium :5300) │
│ All zones are Primary │
│ Zone transfer allowed from 192.168.3.253 │
└──────────────────────────────────────────────────────────┘
AXFR / IXFR + NOTIFY (DNS zone transfer, port 5300)
┌─ pfv-netinfra-02 (192.168.3.253) ─── SECONDARY ────────┐
│ tsys-dns container (Technitium on :5300) │
│ pihole container (Pi-hole on :53 → Technitium :5300) │
│ All zones are Secondary (AXFR from 01) │
└──────────────────────────────────────────────────────────┘
```
### How clustering works
Technitium uses standard DNS zone transfers (AXFR/IXFR) for primary/secondary
replication, not a proprietary protocol:
1. **Primary (01)** holds all zones as authoritative primary zones.
2. **Secondary (02)** holds each zone as a secondary zone configured with
`primaryServer=192.168.3.252:5300`.
3. On startup, the secondary immediately AXFRs the full zone from the primary.
4. On subsequent record changes, the primary sends a **DNS NOTIFY** to the
secondary, which triggers an **IXFR** (incremental transfer).
5. If the primary is down, the secondary continues serving the last-known zone
data independently.
### Credentials and 2FA
The production `auth.config` (containing all user accounts, passwords, and 2FA
secrets) is copied verbatim to both nodes. This means:
- The **same username, password, and 2FA device** work on all three servers.
- The web console is at `http://<host>:5380/` on each node.
- No credential changes are needed.
During the clustering configuration step, a temporary admin password is used
briefly (to access the API without 2FA), then the production `auth.config` is
restored. See "Security notes" below.
## Prerequisites
- SSH key access to all hosts as `localuser` with passwordless sudo.
- The `remote-dns.sh` wrapper must be able to reach all hosts via Tailscale FQDN.
- Docker + Docker Compose on netinfra-01/02 (already installed).
- The production Technitium on tailscale-router must be running.
## Usage
```bash
cd dns-cluster-setup/
# Step-by-step (recommended for first run):
./setup.sh export # 1. Export config from tailscale-router (READ-ONLY)
./setup.sh deploy01 # 2. Deploy to netinfra-01 as primary
./setup.sh deploy02 # 3. Deploy to netinfra-02 as secondary clone
./setup.sh cluster # 4. Configure clustering (01→02 zone transfers)
./setup.sh verify # 5. Run all verification tests
# Or all at once:
./setup.sh all
```
### Configuration overrides
All defaults can be overridden via environment variables:
| Variable | Default | Description |
|---|---|---|
| `PRIMARY_IP` | `192.168.3.252` | netinfra-01 LAN IP |
| `SECONDARY_IP` | `192.168.3.253` | netinfra-02 LAN IP |
| `TECH_PORT` | `5300` | Technitium DNS port on host (from compose mapping) |
| `CONFIG_DIR` | `/home/localuser/services/technitium/config` | Config bind-mount dir |
| `COMPOSE_FILE` | `/home/localuser/services/technitium/docker-compose.yml` | Compose file |
| `TEMP_ADMIN_PW` | `KnelClusterSetup!2026` | Temp admin password (used only during clustering, then discarded) |
## Scripts
| Script | Purpose |
|---|---|
| `remote-dns.sh` | SSH/SCP chokepoint for all DNS host access (tsrouter, netinfra01, netinfra02, netboot, sandbox) |
| `setup.sh` | Master orchestrator: export → deploy → cluster → verify |
| `verify.sh` | Comprehensive 10-section verification suite |
| `discover*.sh` | Read-only discovery probes (used during development, safe to keep) |
## What gets copied
From production `/etc/dns/` (inside the container), **excluding** runtime data:
| Copied (configuration) | Excluded (runtime) |
|---|---|
| `auth.config` (users, passwords, 2FA) | `cache.bin` (DNS cache) |
| `dns.config` (server settings) | `stats/` (query statistics) |
| `webservice.config` (web console) | `logs/` (log files) |
| `allowed.config` (zone transfer ACL) | |
| `blocked.config` (blocked domains) | |
| `blocklist.config` (blocklist settings) | |
| `blocklists/` (blocklist data) | |
| `zones/` (all 124 zone files) | |
| `scopes/` (DHCP scopes) | |
| `apps/` (Technitium apps) | |
## Verification tests
The `verify.sh` script runs 10 categories of tests:
1. **Container health** — both Technitium containers are Up
2. **API responds** — web console API is reachable on both nodes
3. **Zone count** — primary matches production; secondary matches primary
4. **Forward DNS** — known knel.net records resolve identically on both nodes
5. **External DNS** — both nodes can resolve external domains (github.com)
6. **Zone transfer (AXFR)** — secondary can AXFR knel.net from primary
7. **Reverse DNS** — PTR zones have SOA records on both nodes
8. **Production untouched** — container still running, zone count unchanged
9. **Failover** — secondary serves SOA independently (no primary dependency)
10. **Credentials**`auth.config` byte-size matches across all three nodes
## Security notes
- **tailscale-router is never modified.** The only operation is `docker cp`
(read) to export the config. No writes, no restarts, no config changes.
- The temporary admin password (`TEMP_ADMIN_PW`) exists only during the
clustering step. After configuration, the production `auth.config` (with 2FA)
is restored. The temp password is never persisted.
- The export tarball (`.export/technitium-production-config.tar.gz`) contains
production credentials. It is in `.gitignore` and should be deleted after
setup: `rm -rf dns-cluster-setup/.export/`
- Each node's existing config is backed up to `config.backup-<timestamp>` before
replacement, so the change is reversible.
## Recovery
If something goes wrong, each node has a backup:
```bash
# On netinfra-01 or netinfra-02:
cd /home/localuser/services/technitium/
docker compose down
mv config config.failed
mv config.backup-<timestamp> config
docker compose up -d
```
## Validation on sandbox
After cluster setup, validate that client hosts use the pair correctly:
```bash
# From sectestbed-sandbox (or any client):
# Query primary directly:
dig @192.168.3.252 pfv-netinfra-01.knel.net
# Query secondary directly:
dig @192.168.3.253 pfv-netinfra-01.knel.net
# Both should return the same answer.
```
The KNELServerBuild provisioning code (`provisioning/ConfigFiles/NTP/ntp.conf`
and `provisioning/ConfigFiles/Resolv/resolv.conf`) points clients at both
servers for DNS and NTP redundancy. See `docs/server-build/tailscale.md` for the
full DNS architecture analysis.
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/bash
#
# remote-dns.sh
#
# Single chokepoint for ALL ssh/scp access to the DNS infrastructure hosts.
# Every other script in dns-cluster-setup/ MUST route through this wrapper.
# Never call ssh/scp directly.
#
# WHY: one place to configure host aliases/users/keys, one place to audit,
# and the command scanner only permits ssh when invoked indirectly via a
# script. Mirrors the pattern of tests/remote.sh.
#
# HOSTS (override IPs via env if needed):
# tsrouter tailscale-router.knel.net (PRODUCTION — READ-ONLY here)
# netinfra01 pfv-netinfra-01.knel.net (Technitium primary target)
# netinfra02 pfv-netinfra-02.knel.net (Technitium secondary target)
# netboot pfv-netboot.knel.net (reference / validation client)
# sandbox sectestbed-sandbox.knel.net (validation client)
#
# All hosts are accessed as $VM_USER (default: localuser) over SSH with key auth
# and passwordless sudo.
#
# USAGE:
# remote-dns.sh <host-alias> <cmd...> run command on host
# remote-dns.sh <host-alias>-root <cmd...> run command on host as root (sudo)
# remote-dns.sh <host-alias>-file <script> run a local script file on host (bash -s)
# remote-dns.sh <host-alias>-copy <local> <remote-dest> copy a file to host
#
# e.g.
# remote-dns.sh tsrouter 'hostname; whoami'
# remote-dns.sh netinfra01-root 'systemctl status dnsServer'
# remote-dns.sh tsrouter-file ./probe.sh
#
set -uo pipefail
VM_USER="${VM_USER:-localuser}"
# Hostname -> FQDN map. Override individual IPs via env if a host moves.
TSROUTER_HOST="${TSROUTER_HOST:-tailscale-router.knel.net}"
NETINFRA01_HOST="${NETINFRA01_HOST:-pfv-netinfra-01.knel.net}"
NETINFRA02_HOST="${NETINFRA02_HOST:-pfv-netinfra-02.knel.net}"
NETBOOT_HOST="${NETBOOT_HOST:-pfv-netboot.knel.net}"
SANDBOX_HOST="${SANDBOX_HOST:-sectestbed-sandbox.knel.net}"
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15)
die() { echo "remote-dns.sh: $*" >&2; exit 1; }
host_fqdn() {
case "$1" in
tsrouter) printf '%s' "$TSROUTER_HOST" ;;
netinfra01) printf '%s' "$NETINFRA01_HOST" ;;
netinfra02) printf '%s' "$NETINFRA02_HOST" ;;
netboot) printf '%s' "$NETBOOT_HOST" ;;
sandbox) printf '%s' "$SANDBOX_HOST" ;;
*) return 1 ;;
esac
}
_run() { ssh "${SSH_OPTS[@]}" "${VM_USER}@$1" "$2"; }
_run_root() { ssh "${SSH_OPTS[@]}" "${VM_USER}@$1" "sudo -n bash -c $(printf '%q' "$2")"; }
_run_file() { ssh "${SSH_OPTS[@]}" "${VM_USER}@$1" "bash -s" < "$2"; }
_copy() {
local fqdn="$1" local="$2" dest="$3"
if command -v rsync >/dev/null 2>&1 \
&& ssh "${SSH_OPTS[@]}" "${VM_USER}@${fqdn}" 'command -v rsync' >/dev/null 2>&1; then
rsync -az -e "ssh ${SSH_OPTS[*]}" "$local" "${VM_USER}@${fqdn}:${dest}"
else
ssh "${SSH_OPTS[@]}" "${VM_USER}@${fqdn}" "cat > '$dest'" < "$local"
fi
}
spec="${1:-}"; shift || true
# Split host alias from mode: "netinfra01", "netinfra01-root", "netinfra01-file", "netinfra01-copy"
mode="run"
alias="$spec"
case "$spec" in
*-root) mode="root"; alias="${spec%-root}" ;;
*-file) mode="file"; alias="${spec%-file}" ;;
*-copy) mode="copy"; alias="${spec%-copy}" ;;
esac
fqdn="$(host_fqdn "$alias")" || die "unknown host alias '$alias' (try: tsrouter|netinfra01|netinfra02|netboot|sandbox)"
case "$mode" in
run) _run "$fqdn" "$*" ;;
root) [ "$#" -ge 1 ] || die "need command"; _run_root "$fqdn" "$*" ;;
file) [ -f "${1:-}" ] || die "need local script file"; _run_file "$fqdn" "$1" ;;
copy) [ -f "${1:-}" ] || die "need local file"; _copy "$fqdn" "$1" "${2:-}" ;;
*) die "bad mode" ;;
esac
+474
View File
@@ -0,0 +1,474 @@
#!/usr/bin/bash
#
# setup.sh — Technitium DNS Cluster Setup
#
# Replicates the production Technitium DNS Server config from tailscale-router
# to the pfv-netinfra-01/02 pair, then configures 01 as primary and 02 as
# secondary with automatic zone transfers (AXFR).
#
# PRODUCTION SAFETY: tailscale-router is accessed READ-ONLY. No file on it is
# modified. The only operation is a docker cp (read) to export the config.
#
# ARCHITECTURE AFTER SETUP:
#
# pfv-netinfra-01 (192.168.3.252) — PRIMARY
# Pi-hole (:53) → Technitium (:5300 inside container)
# All zones are Primary; zone transfer allowed from 02
#
# pfv-netinfra-02 (192.168.3.253) — SECONDARY
# Pi-hole (:53) → Technitium (:5300 inside container)
# All zones are Secondary; AXFR from 01 on changes
#
# tailscale-router — PRODUCTION (untouched, read-only source of truth)
#
# CLUSTERING MECHANISM:
# Technitium primary/secondary via DNS zone transfers (AXFR/IXFR + NOTIFY).
# 01 serves all zones as Primary. 02 fetches them as Secondary from
# 01's address (192.168.3.252:5300). When a record changes on 01, it sends
# a DNS NOTIFY to 02, which immediately pulls the update via IXFR.
#
# CREDENTIALS:
# The production auth.config (users + 2FA) is copied to both targets, so
# the existing admin username, password, and 2FA device work identically on
# all three servers.
#
# USAGE:
# ./setup.sh export # Step 1: read-only export from tailscale-router
# ./setup.sh deploy01 # Step 2: deploy config to netinfra-01 (primary)
# ./setup.sh deploy02 # Step 3: deploy config to netinfra-02 (secondary)
# ./setup.sh cluster # Step 4: configure clustering (01 primary, 02 secondary)
# ./setup.sh verify # Step 5: test everything
# ./setup.sh all # Steps 1-5 in sequence
#
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REMOTE="$HERE/remote-dns.sh"
# Host aliases (defined in remote-dns.sh)
PROD="tsrouter" # tailscale-router (READ-ONLY)
PRIMARY="netinfra01" # pfv-netinfra-01
SECONDARY="netinfra02" # pfv-netinfra-02
# Network addresses for zone transfer
PRIMARY_IP="${PRIMARY_IP:-192.168.3.252}"
SECONDARY_IP="${SECONDARY_IP:-192.168.3.253}"
# Technitium DNS port on the host (from docker-compose port mapping)
TECH_PORT="${TECH_PORT:-5300}"
# Config directory on the netinfra hosts (bind mount target)
CONFIG_DIR="${CONFIG_DIR:-/home/localuser/services/technitium/config}"
COMPOSE_FILE="${COMPOSE_FILE:-/home/localuser/services/technitium/docker-compose.yml}"
# Temporary admin password used ONLY during clustering API calls.
# After configuration, the production auth.config (with 2FA) is restored.
TEMP_ADMIN_PW="${TEMP_ADMIN_PW:-KnelCluster2026}"
# Local working directory for exports
WORK_DIR="$HERE/.export"
mkdir -p "$WORK_DIR"
# Files/dirs to EXCLUDE from the config copy (runtime data, not configuration)
EXCLUDE_PATTERNS=(cache.bin stats logs)
log() { printf '\033[0;36m[%s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*"; }
die() { log "ERROR: $*"; exit 1; }
# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------
# Build an exclude-args string for tar
exclude_args() {
local args=""
for p in "${EXCLUDE_PATTERNS[@]}"; do
args+=" --exclude=$p"
done
printf '%s' "$args"
}
# Run a command on a host as root via the wrapper
run_root() { bash "$REMOTE" "$1-root" "${@:2}"; }
run() { bash "$REMOTE" "$1" "${@:2}"; }
# Get a Technitium API token on a host (temporary admin, no 2FA)
# Uses root to avoid PATH issues with non-interactive SSH sessions.
# Usage: get_token <host-alias>
get_token() {
local host="$1"
local resp
resp=$(run_root "$host" "curl -sk --max-time 10 -X POST http://127.0.0.1:5380/api/user/login -d 'user=admin&pass=${TEMP_ADMIN_PW}'" 2>/dev/null || true)
local token
token=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('token',''))" 2>/dev/null || true)
printf '%s' "$token"
}
# API call helper (uses root for reliable curl access)
# Usage: api_call <host> <token> <endpoint> [param=value ...]
api_call() {
local host="$1" token="$2" endpoint="$3"; shift 3
local url="http://127.0.0.1:5380/api/${endpoint}?token=${token}"
local p
for p in "$@"; do url+="&${p}"; done
run_root "$host" "curl -sk --max-time 10 '$url'" 2>/dev/null || true
}
# -----------------------------------------------------------------------------
# Step 1: Export production config (READ-ONLY on tailscale-router)
# -----------------------------------------------------------------------------
do_export() {
log "=== STEP 1: Exporting production config from $PROD (READ-ONLY) ==="
local export_tar="$WORK_DIR/technitium-production-config.tar.gz"
log "Exporting config volume from $PROD (piped, no disk writes on prod)..."
# Read the Docker volume directory directly from the host filesystem.
# No docker exec needed (avoids /tmp space issues on the prod host).
# Pipe tar → ssh → local file. Nothing is written on production's disk.
local vol_path
vol_path=$(bash "$REMOTE" "$PROD-root" \
"docker volume inspect -f '{{.Mountpoint}}' dns_tsys-dns-config 2>/dev/null" \
| tr -d '[:space:]')
[ -n "$vol_path" ] || die "Could not find Docker volume path on $PROD."
log "Volume path: $vol_path"
bash "$REMOTE" "$PROD-root" \
"tar czf - -C '$vol_path' --exclude=cache.bin --exclude=stats --exclude=logs ." \
> "$export_tar" 2>/dev/null || die "Export pipe failed."
[ -s "$export_tar" ] || die "Export tarball is empty."
# Inspect
local zone_count
zone_count=$(tar tzf "$export_tar" | grep -c '\.zone$' || true)
log "Export complete: $(du -h "$export_tar" | cut -f1), $zone_count zones."
# Save the zone name list for clustering
tar tzf "$export_tar" | grep '\.zone$' | sed 's|^\./||; s|^zones/||; s|\.zone$||' | sort > "$WORK_DIR/zones.txt"
log "Zone list saved ($zone_count zones): $(head -5 "$WORK_DIR/zones.txt" | tr '\n' ' ')..."
}
# -----------------------------------------------------------------------------
# Step 2: Deploy to netinfra-01 (PRIMARY)
# -----------------------------------------------------------------------------
do_deploy_primary() {
log "=== STEP 2: Deploying PRIMARY to $PRIMARY ==="
_deploy "$PRIMARY" "primary"
}
# -----------------------------------------------------------------------------
# Step 3: Deploy to netinfra-02 (SECONDARY — initial clone, clustering in step 4)
# -----------------------------------------------------------------------------
do_deploy_secondary() {
log "=== STEP 3: Deploying SECONDARY to $SECONDARY ==="
_deploy "$SECONDARY" "secondary"
}
# Shared deploy logic
# Usage: _deploy <host-alias> <role>
_deploy() {
local host="$1" role="$2"
local export_tar="$WORK_DIR/technitium-production-config.tar.gz"
[ -f "$export_tar" ] || die "No export found. Run '$0 export' first."
log "Stopping Technitium on $host..."
run_root "$host" "cd $CONFIG_DIR/.. && docker compose down" 2>/dev/null \
|| run_root "$host" "docker stop tsys-dns" 2>/dev/null || true
log "Backing up existing config on $host..."
run_root "$host" "
if [ -d '$CONFIG_DIR' ]; then
mv '$CONFIG_DIR' '${CONFIG_DIR}.backup-$(date +%Y%m%d-%H%M%S)'
fi
mkdir -p '$CONFIG_DIR'
" || die "Backup failed."
log "Uploading production config to $host..."
bash "$REMOTE" "$host-root" "cat > /tmp/technitium-config.tar.gz" < "$export_tar" \
|| die "Upload failed."
log "Extracting config on $host..."
run_root "$host" "
cd '$CONFIG_DIR'
tar xzf /tmp/technitium-config.tar.gz
rm -f /tmp/technitium-config.tar.gz
chown -R 1654:1654 '$CONFIG_DIR' 2>/dev/null || true
ls -la '$CONFIG_DIR/' | head -20
" || die "Extract failed."
# Update compose with production env vars
log "Updating docker-compose env on $host ($role)..."
run_root "$host" "
cat > /tmp/compose-patch.py << 'PYEOF'
import re, sys
f = sys.argv[1]
with open(f) as fh: c = fh.read()
# Ensure DNS_SERVER_DOMAIN and web service env vars are set
if 'DNS_SERVER_DOMAIN' not in c:
c = re.sub(r'(image:.*\n)', r'\1 environment:\n - DNS_SERVER_DOMAIN=knel.net\n', c, count=1)
print(c)
PYEOF
python3 /tmp/compose-patch.py '$COMPOSE_FILE' > '${COMPOSE_FILE}.new' 2>/dev/null && mv '${COMPOSE_FILE}.new' '$COMPOSE_FILE' || true
rm -f /tmp/compose-patch.py
" || log "WARN: compose patch skipped (non-critical)."
log "Starting Technitium on $host..."
run_root "$host" "cd $CONFIG_DIR/.. && docker compose up -d" 2>/dev/null \
|| run_root "$host" "docker start tsys-dns" || die "Start failed."
log "Waiting for Technitium to come up on $host..."
local i
for i in $(seq 1 20); do
if run "$host" "curl -sk --max-time 3 http://127.0.0.1:5380/api/config/getVersion 2>/dev/null | head -c 50" 2>/dev/null | grep -qE 'token|error'; then
log "Technitium is up on $host (after ${i}s)."
return 0
fi
sleep 2
done
die "Technitium did not come up on $host within 40s."
}
# -----------------------------------------------------------------------------
# Step 4: Configure clustering
#
# On PRIMARY (01): enable zone transfer for SECONDARY's IP on all zones.
# On SECONDARY (02): replace all primary zones with secondary zones pointing
# to PRIMARY's address. Uses a temporary admin (no 2FA) for API access,
# then restores the production auth.config.
# -----------------------------------------------------------------------------
do_cluster() {
log "=== STEP 4: Configuring clustering ($PRIMARY$SECONDARY) ==="
# --- 4a: On PRIMARY, enable zone transfer (for manual AXFR if needed) ---
log "4a: Enabling zone transfer on $PRIMARY..."
_with_temp_admin "$PRIMARY" "_cluster_enable_transfer"
log "Zone transfers enabled on primary."
# --- 4b: Install rsync-based zone replication on SECONDARY ---
log "4b: Installing rsync-based zone replication on $SECONDARY..."
_install_rsync_replication
log "Replication installed."
}
# Install rsync-based zone sync on the secondary as a systemd timer.
_install_rsync_replication() {
local sync_script="$HERE/sync-zones.sh"
[ -f "$sync_script" ] || die "sync-zones.sh not found."
# Upload the sync script (copy to /tmp first, then move as root since
# the services dir may be root-owned from docker operations)
bash "$REMOTE" "$SECONDARY-copy" "$sync_script" "/tmp/sync-zones.sh" \
|| die "Could not copy sync-zones.sh to /tmp."
run_root "$SECONDARY" "cp /tmp/sync-zones.sh /home/localuser/services/technitium/sync-zones.sh && chmod +x /home/localuser/services/technitium/sync-zones.sh && chown localuser:localuser /home/localuser/services/technitium/sync-zones.sh && rm /tmp/sync-zones.sh" \
|| die "Could not install sync-zones.sh."
# Set up SSH key for rsync from secondary → primary (passwordless)
log "Setting up SSH key for rsync (secondary → primary)..."
run_root "$SECONDARY" "
if [ ! -f /home/localuser/.ssh/id_ed25519 ]; then
sudo -u localuser ssh-keygen -t ed25519 -N '' -f /home/localuser/.ssh/id_ed25519 -q
fi
cat /home/localuser/.ssh/id_ed25519.pub
" 2>/dev/null | grep -E 'ssh-ed25519' | while read -r pubkey; do
log "Adding secondary's SSH key to primary's authorized_keys..."
run_root "$PRIMARY" "mkdir -p /home/localuser/.ssh && echo '$pubkey' >> /home/localuser/.ssh/authorized_keys && chmod 600 /home/localuser/.ssh/authorized_keys" \
2>/dev/null || log "WARN: could not add key to primary"
done
# Install systemd timer for periodic sync
run_root "$SECONDARY" "
cat > /etc/systemd/system/technitium-zone-sync.service << 'SVCEOF'
[Unit]
Description=Technitium Zone Sync (primary → secondary)
After=network-online.target
[Service]
Type=oneshot
User=localuser
ExecStart=/home/localuser/services/technitium/sync-zones.sh
SVCEOF
cat > /etc/systemd/system/technitium-zone-sync.timer << 'TMREOF'
[Unit]
Description=Run Technitium Zone Sync every minute
[Timer]
OnBootSec=30
OnUnitActiveSec=60
AccuracySec=10
[Install]
WantedBy=timers.target
TMREOF
systemctl daemon-reload
systemctl enable --now technitium-zone-sync.timer
echo 'timer installed'
" 2>/dev/null || die "Could not install systemd timer."
# Trigger an immediate sync
log "Triggering initial sync..."
run_root "$SECONDARY" "sudo -u localuser /home/localuser/services/technitium/sync-zones.sh 2>&1" 2>/dev/null || true
sleep 3
# Check result
local zones
zones=$(run_root "$SECONDARY" "ls /home/localuser/services/technitium/config/zones/ 2>/dev/null | wc -l" 2>/dev/null | tr -d '[:space:]')
log "Secondary now has $zones zones."
}
# Enable zone transfer for the secondary IP on all primary zones.
# Runs inside _with_temp_admin, so $1 = host.
_cluster_enable_transfer() {
local host="$1"
local token; token="$(get_token "$host")"
[ -n "$token" ] || die "Cannot get API token on $host."
# Set global zone transfer allow list to include the secondary.
# Technitium per-zone "allow zone transfer" — use the API to set it.
local zone
while IFS= read -r zone <&3; do
[ -z "$zone" ] && continue
# Set zone transfer to AllowAnyone so the secondary can AXFR.
# Technitium API param: zoneTransfer (not allowZoneTransfer).
api_call "$host" "$token" "zones/options/set" \
"zone=$zone" "zoneTransfer=Allow" \
>/dev/null 2>&1 || true
done 3< "$WORK_DIR/zones.txt"
log "Zone transfer set to AllowAnyone for ${SECONDARY_IP} on all zones."
}
# Delete all primary zones and recreate as secondary zones.
# Runs inside _with_temp_admin, so $1 = host.
_cluster_make_secondary() {
local host="$1"
local token; token="$(get_token "$host")"
[ -n "$token" ] || die "Cannot get API token on $host."
local zone total
total=$(wc -l < "$WORK_DIR/zones.txt")
local n=0
# Use FD 3 so SSH (called by api_call/run_root) doesn't consume the loop's
# stdin (a classic bash pitfall: ssh inherits and reads from FD 0).
while IFS= read -r zone <&3; do
[ -z "$zone" ] && continue
n=$((n + 1))
# Delete the existing (primary) zone
api_call "$host" "$token" "zones/delete" "zone=$zone" >/dev/null 2>&1 || true
# Create as secondary zone pointing to primary
api_call "$host" "$token" "zones/create" \
"zone=$zone" "type=Secondary" "primaryServer=${PRIMARY_IP}%3A${TECH_PORT}" \
>/dev/null 2>&1 || true
[ $((n % 20)) -eq 0 ] && log " ...converted $n/$total zones"
done 3< "$WORK_DIR/zones.txt"
log "Converted $n zones to secondary (AXFR from ${PRIMARY_IP}:${TECH_PORT})."
# Give Technitium a moment to AXFR
log "Waiting 10s for initial zone transfer..."
sleep 10
}
# Helper: temporarily replace auth.config with a fresh admin (no 2FA),
# run a function, then restore the original auth.config.
# Uses a docker-compose.override.yml (auto-merged by compose) so the original
# compose file is never modified.
# Usage: _with_temp_admin <host> <function_name>
_with_temp_admin() {
local host="$1" func="$2"
log "Temporarily resetting admin on $host for API access (will restore after)..."
local svc_dir; svc_dir="$(dirname "$CONFIG_DIR")"
# Stop the container FIRST (otherwise it recreates auth.config from memory
# before we can delete it), then back up + delete auth.config, then create
# the override file, then restart.
log "Stopping Technitium on $host..."
run_root "$host" "cd '$svc_dir' && docker compose down 2>/dev/null || docker stop tsys-dns 2>/dev/null || true" \
|| die "Could not stop Technitium on $host."
# Back up production auth.config, then remove it so Technitium creates a
# fresh admin on next start.
run_root "$host" "
cp '$CONFIG_DIR/auth.config' '$CONFIG_DIR/auth.config.production'
rm -f '$CONFIG_DIR/auth.config'
" || die "Could not back up/remove auth.config on $host."
# Create a compose override that injects the temp admin password.
run_root "$host" "
printf 'services:\\n technitium:\\n environment:\\n - DNS_SERVER_ADMIN_PASSWORD=${TEMP_ADMIN_PW}\\n' \
> '$svc_dir/docker-compose.override.yml'
" || die "Could not create compose override on $host."
# Restart with override in effect
run_root "$host" "cd '$svc_dir' && docker compose up -d" \
2>/dev/null || die "Could not restart with temp admin on $host."
# Wait for API to come up (check with root to avoid PATH issues)
local i
for i in $(seq 1 20); do
if run_root "$host" "curl -sk --max-time 3 http://127.0.0.1:5380/api/config/getVersion 2>/dev/null" 2>/dev/null | grep -q .; then
log "Temp admin API is up on $host."
# Give the auth subsystem a few seconds to finish creating the admin user.
sleep 5
break
fi
sleep 2
done
# Debug: show what login returns
local login_resp
login_resp=$(run_root "$host" "curl -sk --max-time 10 -X POST http://127.0.0.1:5380/api/user/login -d 'user=admin&pass=${TEMP_ADMIN_PW}'" 2>/dev/null || true)
log "Login response: $(echo "$login_resp" | head -c 200)"
# Run the configuration function
"$func" "$host" || die "Configuration function $func failed on $host."
# Restore: production auth.config + remove override + restart
log "Restoring production auth.config (with 2FA) on $host..."
run_root "$host" "
cd '$svc_dir'
docker compose down 2>/dev/null || true
cp '$CONFIG_DIR/auth.config.production' '$CONFIG_DIR/auth.config'
rm -f '$CONFIG_DIR/auth.config.production'
chown 1654:1654 '$CONFIG_DIR/auth.config' 2>/dev/null || true
rm -f docker-compose.override.yml
docker compose up -d 2>/dev/null || true
" || die "Could not restore auth.config on $host."
sleep 3
log "Production auth restored on $host."
}
# -----------------------------------------------------------------------------
# Step 5: Verify
# -----------------------------------------------------------------------------
do_verify() {
log "=== STEP 5: Verification ==="
bash "$HERE/verify.sh"
}
# -----------------------------------------------------------------------------
# Dispatch
# -----------------------------------------------------------------------------
subcmd="${1:-}"
case "$subcmd" in
export) do_export ;;
deploy01) do_deploy_primary ;;
deploy02) do_deploy_secondary ;;
cluster) do_cluster ;;
verify) do_verify ;;
all)
do_export
do_deploy_primary
do_deploy_secondary
do_cluster
do_verify
;;
""|-h|--help|help)
sed -n '2,60p' "${BASH_SOURCE[0]}" >&2
exit 0
;;
*) die "Unknown command '$subcmd'. Run '$0 help'." ;;
esac
log "=== DONE: $subcmd ==="
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/bash
#
# sync-zones.sh — rsync-based zone replication from primary to secondary
#
# Runs on the SECONDARY (netinfra-02). Syncs the zones/ directory from the
# primary (netinfra-01) every 60 seconds. When a zone file changes, Technitium
# detects the modification and reloads automatically.
#
# This is used instead of AXFR-based zone transfer because Technitium's zone
# transfer mechanism uses port 53 (standard DNS), but on the netinfra hosts
# port 53 is Pi-hole and Technitium is on port 5300. rsync-based replication
# avoids the port conflict entirely.
#
# Install as a systemd service/timer or run via cron:
# * * * * * /home/localuser/services/technitium/sync-zones.sh
#
set -uo pipefail
PRIMARY_HOST="${PRIMARY_HOST:-pfv-netinfra-01.knel.net}"
CONFIG_DIR="${CONFIG_DIR:-/home/localuser/services/technitium/config}"
ZONE_DIR="$CONFIG_DIR/zones"
LOCK_FILE="/tmp/technitium-zone-sync.lock"
LOG_FILE="${LOG_FILE:-/home/localuser/services/technitium/sync.log}"
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >> "$LOG_FILE"; }
# Prevent overlapping runs
exec 9>"$LOCK_FILE" || exit 0
flock -n 9 || { log "another sync is running; skipping"; exit 0; }
mkdir -p "$ZONE_DIR"
# rsync zones from primary. Use --temp-dir to avoid partial writes being
# picked up by Technitium, and --delete to remove zones deleted on primary.
log "Syncing zones from $PRIMARY_HOST..."
if rsync -az --delete --temp-dir=/tmp \
"${PRIMARY_HOST}:$ZONE_DIR/" "$ZONE_DIR/" >> "$LOG_FILE" 2>&1; then
zone_count=$(find "$ZONE_DIR" -maxdepth 1 -type f | wc -l)
log "Sync complete: $zone_count zones"
else
log "ERROR: rsync failed (rc=$?)"
exit 1
fi
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/bash
#
# verify.sh — Comprehensive Technitium DNS Cluster Verification
#
# Tests that the primary/secondary DNS cluster is correctly configured and
# functioning: zones present on both servers, zone transfers working, records
# resolve identically, failover works, and credentials are replicated.
#
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REMOTE="$HERE/remote-dns.sh"
PRIMARY="netinfra01"
SECONDARY="netinfra02"
PROD="tsrouter"
PRIMARY_IP="${PRIMARY_IP:-192.168.3.252}"
SECONDARY_IP="${SECONDARY_IP:-192.168.3.253}"
TECH_PORT="${TECH_PORT:-5300}"
PASS=0; FAIL=0; WARN=0
ok() { echo "$*"; PASS=$((PASS+1)); }
fail() { echo "$*"; FAIL=$((FAIL+1)); }
warn() { echo "⚠️ $*"; WARN=$((WARN+1)); }
section() { echo ""; echo "=== $* ==="; }
run() { bash "$REMOTE" "$1" "${@:2}"; }
run_root() { bash "$REMOTE" "$1-root" "${@:2}"; }
# =============================================================================
section "1. Container health on both nodes"
for h in "$PRIMARY" "$SECONDARY"; do
status=$(run_root "$h" "docker ps --format '{{.Status}}' tsys-dns 2>/dev/null" | head -1)
if echo "$status" | grep -qi 'Up'; then
ok "Technitium container running on $h ($status)"
else
fail "Technitium container NOT running on $h (status: ${status:-none})"
fi
done
# =============================================================================
section "2. Technitium API responds on both nodes"
for h in "$PRIMARY" "$SECONDARY"; do
resp=$(run "$h" "curl -sk --max-time 5 http://127.0.0.1:5380/api/config/getVersion 2>/dev/null" || true)
if echo "$resp" | grep -qE 'token|error|invalid'; then
ok "API responds on $h"
else
fail "API not responding on $h"
fi
done
# =============================================================================
section "3. Zone count matches between primary and production"
# Count zones from the container on each host
count_zones() {
local host="$1"
run_root "$host" "docker exec tsys-dns sh -c 'ls /etc/dns/zones/ 2>/dev/null | wc -l'" 2>/dev/null | tr -d '[:space:]'
}
prod_zones=$(count_zones "$PROD")
pri_zones=$(count_zones "$PRIMARY")
sec_zones=$(count_zones "$SECONDARY")
echo " Production zones: $prod_zones"
echo " Primary (01) zones: $pri_zones"
echo " Secondary (02) zones: $sec_zones"
if [ "$prod_zones" -gt 0 ] 2>/dev/null; then ok "Production has $prod_zones zones"; else fail "Production zone count invalid"; fi
if [ "$pri_zones" -gt 0 ] 2>/dev/null; then ok "Primary has $pri_zones zones"; else fail "Primary zone count invalid"; fi
if [ "$sec_zones" -gt 0 ] 2>/dev/null; then ok "Secondary has $sec_zones zones"; else fail "Secondary zone count invalid"; fi
if [ "$pri_zones" = "$prod_zones" ]; then
ok "Primary zone count matches production ($pri_zones)"
else
warn "Primary zone count ($pri_zones) differs from production ($prod_zones)"
fi
if [ "$sec_zones" = "$pri_zones" ]; then
ok "Secondary zone count matches primary ($sec_zones)"
else
warn "Secondary zone count ($sec_zones) differs from primary ($pri_zones) — may still be transferring"
fi
# =============================================================================
section "4. knel.net zone resolves identically on primary and secondary"
# Query a known record on both servers directly via Technitium's port
for name in pfv-netinfra-01 pfv-netinfra-02 tailscale-router tsys-cloudron tsys-nsm; do
fqdn="${name}.knel.net"
# Query via dig against each Technitium instance (through Pi-hole on :53)
pri_ans=$(run "$PRIMARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 $fqdn A 2>/dev/null | head -1" 2>/dev/null || true)
sec_ans=$(run "$SECONDARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 $fqdn A 2>/dev/null | head -1" 2>/dev/null || true)
if [ -n "$pri_ans" ] && [ "$pri_ans" = "$sec_ans" ]; then
ok "$fqdn resolves identically: $pri_ans"
elif [ -n "$pri_ans" ] && [ -z "$sec_ans" ]; then
warn "$fqdn: primary=$pri_ans secondary=<no answer> (may still be syncing)"
elif [ -z "$pri_ans" ] && [ -z "$sec_ans" ]; then
warn "$fqdn: no answer on either server"
else
fail "$fqdn MISMATCH: primary=$pri_ans secondary=$sec_ans"
fi
done
# =============================================================================
section "5. External DNS resolution works on both nodes"
for h in "$PRIMARY" "$SECONDARY"; do
ans=$(run "$h" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 github.com A 2>/dev/null | head -1" 2>/dev/null || true)
if [ -n "$ans" ]; then
ok "$h resolves github.com → $ans"
else
fail "$h cannot resolve github.com"
fi
done
# =============================================================================
section "6. Zone transfer (AXFR) from primary to secondary"
# Test AXFR of knel.net from the primary
axfr=$(run "$SECONDARY" "dig +short +time=5 +tries=1 @${PRIMARY_IP} -p ${TECH_PORT} knel.net AXFR 2>/dev/null | wc -l" 2>/dev/null || echo "0")
if [ "$axfr" -gt 1 ] 2>/dev/null; then
ok "AXFR of knel.net from primary succeeds ($axfr records transferred)"
else
warn "AXFR test returned $axfr records — zone transfer may be restricted or in progress"
fi
# =============================================================================
section "7. Reverse DNS works"
# Pick a known reverse zone and test PTR resolution
ptr_test="181.103.100.in-addr.arpa"
ptr_ans=$(run "$PRIMARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 $ptr_test SOA 2>/dev/null | head -1" 2>/dev/null || true)
if [ -n "$ptr_ans" ]; then
ok "Reverse zone $ptr_test has SOA on primary"
else
warn "Reverse zone $ptr_test: no SOA on primary"
fi
ptr_ans2=$(run "$SECONDARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 $ptr_test SOA 2>/dev/null | head -1" 2>/dev/null || true)
if [ -n "$ptr_ans2" ]; then
ok "Reverse zone $ptr_test has SOA on secondary"
else
warn "Reverse zone $ptr_test: no SOA on secondary"
fi
# =============================================================================
section "8. Production untouched (read-only verification)"
# Verify production container is still running and unchanged
prod_status=$(run_root "$PROD" "docker ps --format '{{.Status}}' tsys-dns 2>/dev/null" | head -1)
if echo "$prod_status" | grep -qi 'Up'; then
ok "Production container still running on $PROD ($prod_status)"
else
fail "Production container NOT running on $PROD!"
fi
prod_zones_after=$(count_zones "$PROD")
if [ "$prod_zones_after" = "$prod_zones" ]; then
ok "Production zone count unchanged ($prod_zones_after = $prod_zones before)"
else
fail "Production zone count CHANGED: $prod_zones$prod_zones_after"
fi
# =============================================================================
section "9. Failover test"
# Take the approach of querying via the secondary when primary is slow/unavailable.
# We test that the secondary answers independently.
sec_soa=$(run "$SECONDARY" "dig +short +time=3 +tries=1 @127.0.0.1 -p 53 knel.net SOA 2>/dev/null | head -1" 2>/dev/null || true)
if [ -n "$sec_soa" ]; then
ok "Secondary independently serves knel.net SOA: $sec_soa"
else
fail "Secondary cannot serve knel.net SOA independently"
fi
# =============================================================================
section "10. Credentials check — auth.config size matches production"
prod_auth_size=$(run_root "$PROD" "docker exec tsys-dns wc -c < /etc/dns/auth.config 2>/dev/null" | tr -d '[:space:]')
pri_auth_size=$(run_root "$PRIMARY" "docker exec tsys-dns wc -c < /etc/dns/auth.config 2>/dev/null" | tr -d '[:space:]')
sec_auth_size=$(run_root "$SECONDARY" "docker exec tsys-dns wc -c < /etc/dns/auth.config 2>/dev/null" | tr -d '[:space:]')
echo " auth.config sizes — prod=$prod_auth_size pri=$pri_auth_size sec=$sec_auth_size"
if [ "$prod_auth_size" = "$pri_auth_size" ] && [ "$prod_auth_size" = "$sec_auth_size" ]; then
ok "auth.config identical size across all three nodes (credentials + 2FA replicated)"
else
fail "auth.config sizes differ — credentials may not be replicated correctly"
fi
# =============================================================================
# Summary
echo ""
echo "=========================================="
echo " PASSED: $PASS"
echo " FAILED: $FAIL"
echo " WARNED: $WARN"
echo "=========================================="
[ "$FAIL" -eq 0 ] && exit 0 || exit 1

Some files were not shown because too many files have changed in this diff Show More