prep for next ai session
This commit is contained in:
@@ -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
|
||||||
@@ -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'
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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]}"
|
||||||
|
}
|
||||||
@@ -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 "
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -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 "============================================"
|
||||||
@@ -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 "============================================"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 "============================================"
|
||||||
@@ -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 "============================================"
|
||||||
@@ -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.
|
||||||
@@ -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 "============================================"
|
||||||
@@ -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 "============================================"
|
||||||
@@ -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 "============================================"
|
||||||
@@ -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)"
|
||||||
@@ -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 "============================================"
|
||||||
@@ -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.
|
||||||
@@ -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 "======================================================"
|
||||||
@@ -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).")
|
||||||
@@ -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"
|
||||||
@@ -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 "============================================"
|
||||||
@@ -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 "======================================================"
|
||||||
@@ -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.
|
||||||
Executable
+90
@@ -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
|
||||||
Executable
+474
@@ -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 ==="
|
||||||
Executable
+43
@@ -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
|
||||||
Executable
+204
@@ -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
Reference in New Issue
Block a user