This guide explains how to connect a CyberPower CST1500UC2 UPS to a Linux VM running on VMware ESXi, monitor it with NUT in Docker, send Telegram alerts, and automatically shut down ESXi virtual machines safely during a power outage.
- Create a VM in ESXi
Create a small Linux VM on ESXi.
Recommended VM resources:
1 vCPU
2 GB RAM
20 GB disk
Ubuntu or Debian
After creating the VM:
Open VM settings.
Add USB Controller.
Add USB Device.
Select the CyberPower UPS USB device.
Start the VM.
Check if the UPS is detected:
lsusb
- Install Docker
Install Docker and Docker Compose plugin:
sudo apt update
sudo apt install docker.io docker-compose-plugin -y
Add your user to the Docker group:
sudo usermod -aG docker $USER
newgrp docker
Check Docker:
docker version
- Create NUT Docker Directory
Create the working directory:
sudo mkdir -p /opt/ups-nut
cd /opt/ups-nut
Create the Docker Compose file:
nano docker-compose.yml
Copy this code:
services: nut: image: instantlinux/nut-upsd:latest container_name: nut-ups restart: unless-stopped privileged: true environment: - API_USER=upsmon - API_PASSWORD=StrongPassword123 - DRIVER=usbhid-ups - NAME=cyberpower - PORT=auto - DESCRIPTION=CyberPower CST1500UC2 ports: - "3493:3493" devices: - /dev/bus/usb:/dev/bus/usb volumes: - /dev/bus/usb:/dev/bus/usb nut-web: image: ghcr.io/superioone/nut_webgui:latest container_name: nut-web restart: unless-stopped ports: - "9000:9000" environment: - UPSD_ADDR=nut - UPSD_PORT=3493 - UPSD_USER=upsmon - UPSD_PASS=StrongPassword123 depends_on: - nutStart the container:
docker compose up -dhttp://<your IP srv>:9000/Check container logs:
docker logs -f nut-ups
- Check UPS Status
Show all UPS information:
docker exec -it nut-ups upsc cyberpower@localhost
Check only UPS status:
docker exec -it nut-ups upsc cyberpower@localhost ups.status
Common status values:
OL = On Line / utility power is available
OB = On Battery / power is lost
LB = Low Battery
- Create Telegram Notification Script
Create the script:
sudo nano /usr/local/bin/ups-telegram.sh
Copy this code:
#!/bin/bash
BOT_TOKEN=“YOUR_BOT_TOKEN”
CHAT_ID=“YOUR_CHAT_ID”
STATUS=$(docker exec nut-ups upsc cyberpower@localhost ups.status 2>/dev/null)
BATTERY=$(docker exec nut-ups upsc cyberpower@localhost battery.charge 2>/dev/null)
RUNTIME=$(docker exec nut-ups upsc cyberpower@localhost battery.runtime 2>/dev/null)
LOAD=$(docker exec nut-ups upsc cyberpower@localhost ups.load 2>/dev/null)
INPUT=$(docker exec nut-ups upsc cyberpower@localhost input.voltage 2>/dev/null)
MESSAGE=”$1
Host: $(hostname)
UPS status: $STATUS
Battery: $BATTERY%
Runtime: $RUNTIME sec
Load: $LOAD%
Input voltage: $INPUT V”curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \ -d chat_id="${CHAT_ID}" \ -d text="$MESSAGE" > /dev/nullMake it executable:
sudo chmod +x /usr/local/bin/ups-telegram.sh
Test Telegram message:
/usr/local/bin/ups-telegram.sh “✅ UPS Telegram test”
- Create UPS Monitor Script
This script checks UPS status every 30 seconds. If power is lost, it sends a Telegram alert. If battery or runtime becomes low, it connects to ESXi by SSH and starts the shutdown script.
Create the script:
sudo nano /usr/local/bin/ups-monitor.sh
Copy this code:
#!/bin/bash
UPS="cyberpower@localhost"
STATE_FILE="/tmp/ups_last_state"
SHUTDOWN_FILE="/tmp/ups_shutdown_started"
ESXI_HOST="<esxi ip>"
ESXI_USER="root"
BATTERY_LIMIT=30
RUNTIME_LIMIT=600
STATUS=$(docker exec nut-ups upsc "$UPS" ups.status 2>/dev/null)
BATTERY=$(docker exec nut-ups upsc "$UPS" battery.charge 2>/dev/null)
RUNTIME=$(docker exec nut-ups upsc "$UPS" battery.runtime 2>/dev/null)
LAST_STATE=$(cat "$STATE_FILE" 2>/dev/null || echo "UNKNOWN")
if [[ "$STATUS" == *"OB"* && "$LAST_STATE" != "OB" ]]; then
/usr/local/bin/ups-telegram.sh \
"⚡ Power lost. UPS is on battery. Battery: ${BATTERY}%. Runtime: ${RUNTIME}s."
echo "OB" > "$STATE_FILE"
fi
if [[ "$STATUS" == *"OL"* && "$LAST_STATE" != "OL" ]]; then
/usr/local/bin/ups-telegram.sh \
"✅ Power restored. UPS is back online. Battery: ${BATTERY}%."
echo "OL" > "$STATE_FILE"
rm -f "$SHUTDOWN_FILE"
fi
if [[ "$STATUS" == *"OB"* ]]; then
if [[ "$BATTERY" =~ ^[0-9]+$ && "$RUNTIME" =~ ^[0-9]+$ ]]; then
if (( BATTERY <= BATTERY_LIMIT || RUNTIME <= RUNTIME_LIMIT )); then
if [[ ! -f "$SHUTDOWN_FILE" ]]; then
touch "$SHUTDOWN_FILE"
/usr/local/bin/ups-telegram.sh \
"🚨 UPS battery low. Battery: ${BATTERY}%. Runtime: ${RUNTIME}s. Starting graceful shutdown of ESXi and VMs."
if ssh \
-o BatchMode=yes \
-o ConnectTimeout=10 \
"${ESXI_USER}@${ESXI_HOST}" \
"nohup /bin/sh /vmfs/volumes/VMwarw-HHD-2TB/scripts/esxi-ups-shutdown.sh >>/vmfs/volumes/VMwarw-HHD-2TB/scripts/ups-shutdown.log 2>&1 </dev/null &"
then
/usr/local/bin/ups-telegram.sh \
"🖥️ ESXi shutdown command was successfully submitted."
else
rm -f "$SHUTDOWN_FILE"
/usr/local/bin/ups-telegram.sh \
"❌ Failed to start ESXi shutdown. Check SSH access and the remote script path."
fi
fi
fi
else
/usr/local/bin/ups-telegram.sh \
"⚠️ Invalid UPS data received. Battery: '${BATTERY}', runtime: '${RUNTIME}', status: '${STATUS}'."
fi
fiMake it executable:
sudo chmod +x /usr/local/bin/ups-monitor.sh
- Create systemd Service
Create the service file:
sudo nano /etc/systemd/system/ups-monitor.service
Copy this code:
[Unit]
Description=UPS Monitor Telegram Alerts
After=docker.service
Requires=docker.service
[Service]
Type=simple
ExecStart=/bin/bash -c ‘while true; do /usr/local/bin/ups-monitor.sh; sleep 30; done’
Restart=always
[Install]
WantedBy=multi-user.targetReload systemd:
sudo systemctl daemon-reload
Enable the service:
sudo systemctl enable ups-monitor
Start the service:
sudo systemctl start ups-monitor
Check service status:
sudo systemctl status ups-monitor
- Configure SSH Access to ESXi
Generate SSH key on the Linux UPS VM:
ssh-keygen -t ed25519
Copy the key to ESXi:
ssh-copy-id root@<your ip>
Test SSH login:
ssh root@<your ip> hostname- Enable SSH on ESXi
In ESXi UI:
Go to Host.
Open Manage.
Open Services.
Start TSM-SSH.
Check authorized keys on ESXi:
cat /etc/ssh/keys-root/authorized_keys- Create ESXi Shutdown Script
On ESXi, create the scripts directory:
mkdir -p /vmfs/volumes/VMwarw-HHD-2TB/scripts/
Create the shutdown script:
vi /vmfs/volumes/VMwarw-HHD-2TB/scripts/esxi-ups-shutdown.sh
Copy this code:
#!/bin/sh
LOG="/vmfs/volumes/VMwarw-HHD-2TB/scripts/ups-shutdown.log"
UPS_VM_NAME="heimdall"
VM_SHUTDOWN_TIMEOUT=180
UPS_VM_SHUTDOWN_TIMEOUT=60
CHECK_INTERVAL=10
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG"
}
get_vm_name() {
vim-cmd vmsvc/get.summary "$1" 2>/dev/null |
grep "name =" |
head -1 |
sed 's/.*name = "\(.*\)",/\1/'
}
get_vm_state() {
vim-cmd vmsvc/power.getstate "$1" 2>/dev/null |
tail -1
}
is_ups_vm() {
VM_NAME_TO_CHECK="$1"
[ "$VM_NAME_TO_CHECK" = "$UPS_VM_NAME" ]
}
shutdown_vm() {
VM_ID="$1"
VM_NAME="$2"
log_message "Requesting graceful shutdown: VM $VM_ID - $VM_NAME"
vim-cmd vmsvc/power.shutdown "$VM_ID" >> "$LOG" 2>&1
if [ $? -ne 0 ]; then
log_message "WARNING: Graceful shutdown request failed for VM $VM_ID - $VM_NAME"
fi
}
force_power_off_vm() {
VM_ID="$1"
VM_NAME="$2"
log_message "Force powering off: VM $VM_ID - $VM_NAME"
vim-cmd vmsvc/power.off "$VM_ID" >> "$LOG" 2>&1
if [ $? -ne 0 ]; then
log_message "ERROR: Force power-off failed for VM $VM_ID - $VM_NAME"
fi
}
wait_for_regular_vms() {
ELAPSED=0
while [ "$ELAPSED" -lt "$VM_SHUTDOWN_TIMEOUT" ]; do
POWERED_ON_COUNT=0
for VM_ID in $(vim-cmd vmsvc/getallvms | awk 'NR > 1 && $1 ~ /^[0-9]+$/ {print $1}'); do
VM_NAME=$(get_vm_name "$VM_ID")
VM_STATE=$(get_vm_state "$VM_ID")
if is_ups_vm "$VM_NAME"; then
continue
fi
if [ "$VM_STATE" = "Powered on" ]; then
POWERED_ON_COUNT=$((POWERED_ON_COUNT + 1))
fi
done
if [ "$POWERED_ON_COUNT" -eq 0 ]; then
log_message "All regular VMs are powered off."
return 0
fi
log_message "Waiting for $POWERED_ON_COUNT VM(s) to shut down. Elapsed: ${ELAPSED}s."
sleep "$CHECK_INTERVAL"
ELAPSED=$((ELAPSED + CHECK_INTERVAL))
done
log_message "Regular VM shutdown timeout reached after ${VM_SHUTDOWN_TIMEOUT}s."
return 1
}
wait_for_vm_shutdown() {
VM_ID="$1"
VM_NAME="$2"
TIMEOUT="$3"
ELAPSED=0
while [ "$ELAPSED" -lt "$TIMEOUT" ]; do
VM_STATE=$(get_vm_state "$VM_ID")
if [ "$VM_STATE" != "Powered on" ]; then
log_message "VM $VM_ID - $VM_NAME is powered off."
return 0
fi
sleep "$CHECK_INTERVAL"
ELAPSED=$((ELAPSED + CHECK_INTERVAL))
done
return 1
}
log_message "============================================================"
log_message "UPS-triggered ESXi shutdown started."
log_message "UPS monitor VM configured as: $UPS_VM_NAME"
# Request graceful shutdown for all regular VMs.
for VM_ID in $(vim-cmd vmsvc/getallvms | awk 'NR > 1 && $1 ~ /^[0-9]+$/ {print $1}'); do
VM_NAME=$(get_vm_name "$VM_ID")
VM_STATE=$(get_vm_state "$VM_ID")
log_message "Detected VM $VM_ID - $VM_NAME - $VM_STATE"
if is_ups_vm "$VM_NAME"; then
log_message "Skipping UPS monitor VM until last: $VM_NAME"
continue
fi
if [ "$VM_STATE" = "Powered on" ]; then
shutdown_vm "$VM_ID" "$VM_NAME"
else
log_message "VM already powered off: $VM_ID - $VM_NAME"
fi
done
# Wait for regular VMs to shut down gracefully.
wait_for_regular_vms
# Force power off any regular VM that is still running.
for VM_ID in $(vim-cmd vmsvc/getallvms | awk 'NR > 1 && $1 ~ /^[0-9]+$/ {print $1}'); do
VM_NAME=$(get_vm_name "$VM_ID")
VM_STATE=$(get_vm_state "$VM_ID")
if is_ups_vm "$VM_NAME"; then
continue
fi
if [ "$VM_STATE" = "Powered on" ]; then
force_power_off_vm "$VM_ID" "$VM_NAME"
fi
done
# Shut down the UPS monitoring VM last.
UPS_VM_FOUND=0
for VM_ID in $(vim-cmd vmsvc/getallvms | awk 'NR > 1 && $1 ~ /^[0-9]+$/ {print $1}'); do
VM_NAME=$(get_vm_name "$VM_ID")
if ! is_ups_vm "$VM_NAME"; then
continue
fi
UPS_VM_FOUND=1
VM_STATE=$(get_vm_state "$VM_ID")
log_message "UPS monitor VM found: VM $VM_ID - $VM_NAME - $VM_STATE"
if [ "$VM_STATE" = "Powered on" ]; then
shutdown_vm "$VM_ID" "$VM_NAME"
if wait_for_vm_shutdown "$VM_ID" "$VM_NAME" "$UPS_VM_SHUTDOWN_TIMEOUT"; then
log_message "UPS monitor VM shut down gracefully."
else
log_message "UPS monitor VM did not shut down within ${UPS_VM_SHUTDOWN_TIMEOUT}s."
force_power_off_vm "$VM_ID" "$VM_NAME"
fi
else
log_message "UPS monitor VM is already powered off."
fi
break
done
if [ "$UPS_VM_FOUND" -eq 0 ]; then
log_message "WARNING: UPS monitor VM '$UPS_VM_NAME' was not found."
fi
log_message "Entering ESXi maintenance mode."
vim-cmd hostsvc/maintenance_mode_enter >> "$LOG" 2>&1
if [ $? -ne 0 ]; then
log_message "WARNING: ESXi could not enter maintenance mode. Continuing with host shutdown."
else
log_message "ESXi entered maintenance mode."
fi
log_message "Powering off ESXi host in 10 seconds."
sync
sleep 10
log_message "Executing ESXi host poweroff."
sync
/sbin/poweroffMake the script executable:
chmod +x /vmfs/volumes/VMwarw-HHD-2TB/scripts/esxi-ups-shutdown.sh
- How the Shutdown Logic Works
When power is normal:
UPS status: OL
The system does nothing and continues monitoring.
When power is lost:
UPS status: OB
Telegram sends:
⚡ Power lost. UPS is on battery.
When battery is low or runtime is low:
Battery <= 30%
Runtime <= 600 seconds
Telegram sends:
🚨 UPS battery low. Starting graceful shutdown of ESXi and VMs.
Then the Linux VM connects to ESXi and runs:
/vmfs/volumes/VMwarw-HHD-2TB/scripts/esxi-ups-shutdown.shThe ESXi script:
Shuts down all VMs gracefully.
Skips the UPS monitor VM until the end.
Waits 180 seconds.
Forces off any VM still running.
Shuts down the UPS monitor VM last.
Powers off the ESXi host.
- Useful Commands
Check UPS status:
docker exec -it nut-ups upsc cyberpower@localhost ups.status
Show all UPS values:
docker exec -it nut-ups upsc cyberpower@localhost
Check NUT container logs:
docker logs -f nut-ups
Restart UPS monitor service:
sudo systemctl restart ups-monitor
Check UPS monitor service:
sudo systemctl status ups-monitor
Check ESXi shutdown log:
cat /vmfs/volumes/VMwarw-HHD-2TB/scripts/ups-shutdown.log
- Result
After this setup, the CyberPower CST1500UC2 UPS will automatically protect your ESXi environment.
You will have:
UPS monitoring through USB passthrough
NUT running inside Docker
Telegram alerts for power loss and power restore
Battery and runtime monitoring
Automatic ESXi shutdown
Graceful VM shutdown before host poweroff
UPS monitor VM shutdown last
#CyberPower CST1500UC2 #CyberPower UPS ESXi #NUT Docker
#VMware ESXi UPS #UPS Monitoring #Telegram UPS Alerts
#Automatic ESXi Shutdown #Network UPS Tools #USB Passthrough ESXi
#Docker UPS Monitoring