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