docs: Ledger Live integration, contract deploy learnings, NEXT_STEPS updates
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled

- ADD_CHAIN138_TO_LEDGER_LIVE: Ledger form done; public code review repo bis-innovations/LedgerLive; init/push commands
- CONTRACT_DEPLOYMENT_RUNBOOK: Chain 138 gas price 1 gwei, 36-addr check, TransactionMirror workaround
- CONTRACT_*: AddressMapper, MirrorManager deployed 2026-02-12; 36-address on-chain check
- NEXT_STEPS_FOR_YOU: Ledger done; steps completable now (no LAN); run-completable-tasks-from-anywhere
- MASTER_INDEX, OPERATOR_OPTIONAL, SMART_CONTRACTS_INVENTORY_SIMPLE: updates
- LEDGER_BLOCKCHAIN_INTEGRATION_COMPLETE: bis-innovations/LedgerLive reference

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-12 15:46:57 -08:00
parent cc8dcaf356
commit fbda1b4beb
5114 changed files with 498901 additions and 4567 deletions

View File

@@ -0,0 +1,27 @@
# Quick Start: Finding the Add Button
## Fastest Method
Run the visual analyzer and test buttons interactively:
```bash
cd /home/intlc/projects/proxmox
UNIFI_USERNAME=unifi_api UNIFI_PASSWORD='L@kers2010$$' \
node scripts/unifi/analyze-page-visually.js
```
1. Script will highlight all buttons in RED
2. Enter button number to test (1, 2, 3, etc.)
3. When form appears, script will show the selector
4. Update `configure-static-route-playwright.js` with that selector
## Alternative: Let Script Wait for You
```bash
cd /home/intlc/projects/proxmox
UNIFI_USERNAME=unifi_api UNIFI_PASSWORD='L@kers2010$$' \
HEADLESS=false PAUSE_MODE=true \
node scripts/unifi/configure-static-route-playwright.js
```
Script will pause at Static Routes page. Manually click Add, and script will continue automatically.

89
scripts/unifi/README.md Normal file
View File

@@ -0,0 +1,89 @@
# UniFi Utility Scripts
Utility scripts for common UniFi Controller operations.
## Available Scripts
### `list-sites.sh`
List all sites in the UniFi Controller.
**Usage:**
```bash
./scripts/unifi/list-sites.sh
```
### `check-health.sh`
Check UniFi controller health and connectivity.
**Usage:**
```bash
./scripts/unifi/check-health.sh
```
This script:
- Verifies configuration in `~/.env`
- Tests controller connectivity
- Tests API connection
- Displays site information
### `list-devices.sh`
Placeholder script for listing devices (currently not available in Official API).
**Usage:**
```bash
./scripts/unifi/list-devices.sh
```
### `test-integration.sh`
Test suite to verify UniFi integration components are properly set up.
**Usage:**
```bash
./scripts/unifi/test-integration.sh
```
This script verifies:
- Package builds
- Configuration
- Script executability
- Documentation
- API connection (if configured)
### `check-networks.sh`
Check UniFi networks/VLANs configuration (requires Private API mode).
**Usage:**
```bash
./scripts/unifi/check-networks.sh
```
**Note:** Requires Private API mode and a non-2FA account. If your account has 2FA/SSO enabled, this script will not work. Use the web interface instead.
**Requirements:**
- `UNIFI_API_MODE=private` in `~/.env`
- `UNIFI_USERNAME` and `UNIFI_PASSWORD` set
- Account must NOT have 2FA/SSO enabled
## Requirements
- UniFi configuration in `~/.env`
- Node.js and pnpm installed
- Python3 (for JSON formatting, optional)
## Configuration
Ensure your `~/.env` file contains:
```bash
UNIFI_UDM_URL=https://your-udm-ip
UNIFI_API_MODE=official
UNIFI_API_KEY=your-api-key
UNIFI_SITE_ID=default
UNIFI_VERIFY_SSL=false
```
## Notes
- Scripts use `NODE_TLS_REJECT_UNAUTHORIZED=0` for self-signed certificates
- For production, install proper SSL certificates and remove this workaround
- Some endpoints may only be available in Private API mode

View File

@@ -0,0 +1,125 @@
#!/bin/bash
set -euo pipefail
# Load IP configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
# Add VLAN 11 secondary IP address using ifupdown (Debian/Ubuntu traditional)
# This makes the configuration persistent across reboots
set -e
# Configuration
VLAN11_IP="${IP_SERVICE_23:-${IP_SERVICE_23:-192.168.11.23}}"
VLAN11_NETMASK="255.255.255.0"
VLAN11_GATEWAY="${NETWORK_GATEWAY:-192.168.11.1}"
PRIMARY_IF="eth0"
echo "🔧 Adding VLAN 11 Secondary IP Address (Persistent via ifupdown)"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ This script must be run with sudo"
echo " Usage: sudo $0"
exit 1
fi
# Check if /etc/network/interfaces exists
if [ ! -f /etc/network/interfaces ]; then
echo "❌ /etc/network/interfaces not found"
echo " This system may not use ifupdown"
exit 1
fi
CURRENT_IP=$(ip -4 addr show $PRIMARY_IF 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
CURRENT_GW=$(ip route show | grep default | awk '{print $3}' | head -1)
echo "📋 Configuration:"
echo " Primary Interface: $PRIMARY_IF"
echo " Current IP: $CURRENT_IP"
echo " Current Gateway: $CURRENT_GW"
echo " VLAN 11 IP: $VLAN11_IP"
echo " VLAN 11 Netmask: $VLAN11_NETMASK"
echo " VLAN 11 Gateway: $VLAN11_GATEWAY"
echo ""
# Backup
BACKUP_FILE="/etc/network/interfaces.backup.$(date +%Y%m%d_%H%M%S)"
cp /etc/network/interfaces "$BACKUP_FILE"
echo "✅ Backup created: $BACKUP_FILE"
echo ""
# Check if VLAN 11 IP already configured
if grep -q "$VLAN11_IP" /etc/network/interfaces; then
echo "✅ VLAN 11 IP ($VLAN11_IP) already configured"
echo " Applying configuration..."
ifdown $PRIMARY_IF 2>/dev/null || true
ifup $PRIMARY_IF
exit 0
fi
echo "🔄 Updating /etc/network/interfaces..."
echo ""
# Add secondary IP configuration
cat >> /etc/network/interfaces << EOF
# VLAN 11 secondary IP address (added automatically)
auto $PRIMARY_IF:11
iface $PRIMARY_IF:11 inet static
address $VLAN11_IP
netmask $VLAN11_NETMASK
gateway $VLAN11_GATEWAY
EOF
echo "✅ Configuration added to /etc/network/interfaces"
echo ""
# Apply configuration
echo "🔄 Applying network configuration..."
ifdown $PRIMARY_IF:11 2>/dev/null || true
ifup $PRIMARY_IF:11
if [ $? -eq 0 ]; then
echo "✅ Configuration applied successfully"
else
echo "⚠️ Configuration applied with warnings"
fi
# Wait for network
sleep 2
# Verify
echo ""
echo "🔍 Verifying configuration..."
NEW_IPS=$(ip -4 addr show $PRIMARY_IF | grep "inet " | awk '{print $2}')
echo " Current IP addresses on $PRIMARY_IF:"
echo "$NEW_IPS" | sed 's/^/ /'
if echo "$NEW_IPS" | grep -q "$VLAN11_IP"; then
echo " ✅ VLAN 11 IP ($VLAN11_IP) is configured"
else
echo " ⚠️ VLAN 11 IP not found (checking alias interface)..."
if ip addr show | grep -q "$VLAN11_IP"; then
echo " ✅ VLAN 11 IP found on alias interface"
fi
fi
# Test connectivity
echo ""
echo "🧪 Testing Connectivity..."
if ping -c 1 -W 2 $VLAN11_GATEWAY >/dev/null 2>&1; then
echo " ✅ VLAN 11 gateway ($VLAN11_GATEWAY) is reachable"
else
echo " ⚠️ VLAN 11 gateway ($VLAN11_GATEWAY) is not reachable"
fi
echo ""
echo "✅ Configuration complete and persistent!"
echo ""
echo "💡 The configuration will persist across reboots."
echo ""

View File

@@ -0,0 +1,119 @@
#!/bin/bash
set -euo pipefail
# Add VLAN 11 secondary IP address using ifupdown (Debian/Ubuntu traditional)
# This makes the configuration persistent across reboots
set -e
# Configuration
VLAN11_IP="192.168.11.23"
VLAN11_NETMASK="255.255.255.0"
VLAN11_GATEWAY="192.168.11.1"
PRIMARY_IF="eth0"
echo "🔧 Adding VLAN 11 Secondary IP Address (Persistent via ifupdown)"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ This script must be run with sudo"
echo " Usage: sudo $0"
exit 1
fi
# Check if /etc/network/interfaces exists
if [ ! -f /etc/network/interfaces ]; then
echo "❌ /etc/network/interfaces not found"
echo " This system may not use ifupdown"
exit 1
fi
CURRENT_IP=$(ip -4 addr show $PRIMARY_IF 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
CURRENT_GW=$(ip route show | grep default | awk '{print $3}' | head -1)
echo "📋 Configuration:"
echo " Primary Interface: $PRIMARY_IF"
echo " Current IP: $CURRENT_IP"
echo " Current Gateway: $CURRENT_GW"
echo " VLAN 11 IP: $VLAN11_IP"
echo " VLAN 11 Netmask: $VLAN11_NETMASK"
echo " VLAN 11 Gateway: $VLAN11_GATEWAY"
echo ""
# Backup
BACKUP_FILE="/etc/network/interfaces.backup.$(date +%Y%m%d_%H%M%S)"
cp /etc/network/interfaces "$BACKUP_FILE"
echo "✅ Backup created: $BACKUP_FILE"
echo ""
# Check if VLAN 11 IP already configured
if grep -q "$VLAN11_IP" /etc/network/interfaces; then
echo "✅ VLAN 11 IP ($VLAN11_IP) already configured"
echo " Applying configuration..."
ifdown $PRIMARY_IF 2>/dev/null || true
ifup $PRIMARY_IF
exit 0
fi
echo "🔄 Updating /etc/network/interfaces..."
echo ""
# Add secondary IP configuration
cat >> /etc/network/interfaces << EOF
# VLAN 11 secondary IP address (added automatically)
auto $PRIMARY_IF:11
iface $PRIMARY_IF:11 inet static
address $VLAN11_IP
netmask $VLAN11_NETMASK
gateway $VLAN11_GATEWAY
EOF
echo "✅ Configuration added to /etc/network/interfaces"
echo ""
# Apply configuration
echo "🔄 Applying network configuration..."
ifdown $PRIMARY_IF:11 2>/dev/null || true
ifup $PRIMARY_IF:11
if [ $? -eq 0 ]; then
echo "✅ Configuration applied successfully"
else
echo "⚠️ Configuration applied with warnings"
fi
# Wait for network
sleep 2
# Verify
echo ""
echo "🔍 Verifying configuration..."
NEW_IPS=$(ip -4 addr show $PRIMARY_IF | grep "inet " | awk '{print $2}')
echo " Current IP addresses on $PRIMARY_IF:"
echo "$NEW_IPS" | sed 's/^/ /'
if echo "$NEW_IPS" | grep -q "$VLAN11_IP"; then
echo " ✅ VLAN 11 IP ($VLAN11_IP) is configured"
else
echo " ⚠️ VLAN 11 IP not found (checking alias interface)..."
if ip addr show | grep -q "$VLAN11_IP"; then
echo " ✅ VLAN 11 IP found on alias interface"
fi
fi
# Test connectivity
echo ""
echo "🧪 Testing Connectivity..."
if ping -c 1 -W 2 $VLAN11_GATEWAY >/dev/null 2>&1; then
echo " ✅ VLAN 11 gateway ($VLAN11_GATEWAY) is reachable"
else
echo " ⚠️ VLAN 11 gateway ($VLAN11_GATEWAY) is not reachable"
fi
echo ""
echo "✅ Configuration complete and persistent!"
echo ""
echo "💡 The configuration will persist across reboots."
echo ""

View File

@@ -0,0 +1,199 @@
#!/bin/bash
set -euo pipefail
# Load IP configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
# Add VLAN 11 secondary IP address using netplan (persistent)
# This makes the configuration persistent across reboots
set -e
# Configuration
VLAN11_IP="${IP_SERVICE_23:-${IP_SERVICE_23:-192.168.11.23}}"
VLAN11_NETMASK="24"
VLAN11_GATEWAY="${NETWORK_GATEWAY:-192.168.11.1}"
echo "🔧 Adding VLAN 11 Secondary IP Address (Persistent via Netplan)"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ This script must be run with sudo"
echo " Usage: sudo $0"
exit 1
fi
# Detect primary interface
PRIMARY_IF=$(ip route show | grep default | awk '{print $5}' | head -1)
if [ -z "$PRIMARY_IF" ]; then
echo "❌ Could not detect primary network interface"
exit 1
fi
CURRENT_IP=$(ip -4 addr show $PRIMARY_IF 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
CURRENT_GW=$(ip route show | grep default | awk '{print $3}' | head -1)
echo "📋 Configuration:"
echo " Primary Interface: $PRIMARY_IF"
echo " Current IP: $CURRENT_IP"
echo " Current Gateway: $CURRENT_GW"
echo " VLAN 11 IP: $VLAN11_IP/$VLAN11_NETMASK"
echo " VLAN 11 Gateway: $VLAN11_GATEWAY"
echo ""
# Check if netplan is available
if ! command -v netplan &> /dev/null; then
echo "❌ netplan is not installed"
echo " Install with: sudo apt install netplan.io"
exit 1
fi
# Find netplan config file
NETPLAN_DIR="/etc/netplan"
NETPLAN_FILE=$(ls $NETPLAN_DIR/*.yaml 2>/dev/null | head -1)
if [ -z "$NETPLAN_FILE" ]; then
echo "❌ No netplan configuration file found in $NETPLAN_DIR"
exit 1
fi
echo "📋 Found netplan config: $NETPLAN_FILE"
echo ""
# Backup
BACKUP_FILE="${NETPLAN_FILE}.backup.$(date +%Y%m%d_%H%M%S)"
cp "$NETPLAN_FILE" "$BACKUP_FILE"
echo "✅ Backup created: $BACKUP_FILE"
echo ""
# Check if VLAN 11 IP already configured
if grep -q "$VLAN11_IP" "$NETPLAN_FILE"; then
echo "✅ VLAN 11 IP ($VLAN11_IP) already configured in netplan"
echo " Applying configuration..."
netplan apply
exit 0
fi
echo "🔄 Updating netplan configuration..."
echo ""
# Use Python to safely modify YAML
python3 << EOF
import yaml
import sys
# Read current config
with open("$NETPLAN_FILE", 'r') as f:
config = yaml.safe_load(f)
# Ensure network structure exists
if 'network' not in config:
config['network'] = {}
if 'ethernets' not in config['network']:
config['network']['ethernets'] = {}
if '$PRIMARY_IF' not in config['network']['ethernets']:
config['network']['ethernets']['$PRIMARY_IF'] = {}
# Get current addresses
interface = config['network']['ethernets']['$PRIMARY_IF']
current_addresses = interface.get('addresses', [])
# Add VLAN 11 IP if not present
vlan11_address = "$VLAN11_IP/$VLAN11_NETMASK"
if vlan11_address not in current_addresses:
if not isinstance(current_addresses, list):
current_addresses = [current_addresses] if current_addresses else []
current_addresses.append(vlan11_address)
interface['addresses'] = current_addresses
print(f"✅ Added VLAN 11 IP: {vlan11_address}")
else:
print(f"✅ VLAN 11 IP already present: {vlan11_address}")
# Add routes for VLAN 11 if needed
if 'routes' not in interface:
interface['routes'] = []
# Check if route to VLAN 11 network exists
vlan11_route = {
'to': '${NETWORK_192_168_11_0:-192.168.11.0}/24',
'via': '$VLAN11_GATEWAY'
}
route_exists = any(
r.get('to') == '${NETWORK_192_168_11_0:-192.168.11.0}/24'
for r in interface['routes']
)
if not route_exists:
interface['routes'].append(vlan11_route)
print(f"✅ Added route to VLAN 11 network")
else:
print(f"✅ Route to VLAN 11 network already exists")
# Write updated config
with open("$NETPLAN_FILE", 'w') as f:
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
print("✅ Netplan configuration updated")
EOF
if [ $? -ne 0 ]; then
echo "❌ Failed to update netplan configuration"
echo " Restoring backup..."
cp "$BACKUP_FILE" "$NETPLAN_FILE"
exit 1
fi
echo ""
echo "📋 Updated configuration:"
cat "$NETPLAN_FILE"
echo ""
# Apply netplan configuration
echo "🔄 Applying netplan configuration..."
if netplan try --timeout 5 >/dev/null 2>&1; then
netplan apply
echo "✅ Configuration applied successfully"
else
echo "⚠️ Validation failed. Restoring backup..."
cp "$BACKUP_FILE" "$NETPLAN_FILE"
netplan apply
exit 1
fi
# Wait for network
sleep 3
# Verify
echo ""
echo "🔍 Verifying configuration..."
NEW_IPS=$(ip -4 addr show $PRIMARY_IF | grep "inet " | awk '{print $2}')
echo " Current IP addresses on $PRIMARY_IF:"
echo "$NEW_IPS" | sed 's/^/ /'
if echo "$NEW_IPS" | grep -q "$VLAN11_IP"; then
echo " ✅ VLAN 11 IP ($VLAN11_IP) is configured"
else
echo " ⚠️ VLAN 11 IP not found (may need to check configuration)"
fi
# Test connectivity
echo ""
echo "🧪 Testing Connectivity..."
if ping -c 1 -W 2 $VLAN11_GATEWAY >/dev/null 2>&1; then
echo " ✅ VLAN 11 gateway ($VLAN11_GATEWAY) is reachable"
else
echo " ⚠️ VLAN 11 gateway ($VLAN11_GATEWAY) is not reachable"
fi
echo ""
echo "✅ Configuration complete and persistent!"
echo ""
echo "💡 The configuration will persist across reboots."
echo ""

View File

@@ -0,0 +1,193 @@
#!/bin/bash
set -euo pipefail
# Add VLAN 11 secondary IP address using netplan (persistent)
# This makes the configuration persistent across reboots
set -e
# Configuration
VLAN11_IP="192.168.11.23"
VLAN11_NETMASK="24"
VLAN11_GATEWAY="192.168.11.1"
echo "🔧 Adding VLAN 11 Secondary IP Address (Persistent via Netplan)"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ This script must be run with sudo"
echo " Usage: sudo $0"
exit 1
fi
# Detect primary interface
PRIMARY_IF=$(ip route show | grep default | awk '{print $5}' | head -1)
if [ -z "$PRIMARY_IF" ]; then
echo "❌ Could not detect primary network interface"
exit 1
fi
CURRENT_IP=$(ip -4 addr show $PRIMARY_IF 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
CURRENT_GW=$(ip route show | grep default | awk '{print $3}' | head -1)
echo "📋 Configuration:"
echo " Primary Interface: $PRIMARY_IF"
echo " Current IP: $CURRENT_IP"
echo " Current Gateway: $CURRENT_GW"
echo " VLAN 11 IP: $VLAN11_IP/$VLAN11_NETMASK"
echo " VLAN 11 Gateway: $VLAN11_GATEWAY"
echo ""
# Check if netplan is available
if ! command -v netplan &> /dev/null; then
echo "❌ netplan is not installed"
echo " Install with: sudo apt install netplan.io"
exit 1
fi
# Find netplan config file
NETPLAN_DIR="/etc/netplan"
NETPLAN_FILE=$(ls $NETPLAN_DIR/*.yaml 2>/dev/null | head -1)
if [ -z "$NETPLAN_FILE" ]; then
echo "❌ No netplan configuration file found in $NETPLAN_DIR"
exit 1
fi
echo "📋 Found netplan config: $NETPLAN_FILE"
echo ""
# Backup
BACKUP_FILE="${NETPLAN_FILE}.backup.$(date +%Y%m%d_%H%M%S)"
cp "$NETPLAN_FILE" "$BACKUP_FILE"
echo "✅ Backup created: $BACKUP_FILE"
echo ""
# Check if VLAN 11 IP already configured
if grep -q "$VLAN11_IP" "$NETPLAN_FILE"; then
echo "✅ VLAN 11 IP ($VLAN11_IP) already configured in netplan"
echo " Applying configuration..."
netplan apply
exit 0
fi
echo "🔄 Updating netplan configuration..."
echo ""
# Use Python to safely modify YAML
python3 << EOF
import yaml
import sys
# Read current config
with open("$NETPLAN_FILE", 'r') as f:
config = yaml.safe_load(f)
# Ensure network structure exists
if 'network' not in config:
config['network'] = {}
if 'ethernets' not in config['network']:
config['network']['ethernets'] = {}
if '$PRIMARY_IF' not in config['network']['ethernets']:
config['network']['ethernets']['$PRIMARY_IF'] = {}
# Get current addresses
interface = config['network']['ethernets']['$PRIMARY_IF']
current_addresses = interface.get('addresses', [])
# Add VLAN 11 IP if not present
vlan11_address = "$VLAN11_IP/$VLAN11_NETMASK"
if vlan11_address not in current_addresses:
if not isinstance(current_addresses, list):
current_addresses = [current_addresses] if current_addresses else []
current_addresses.append(vlan11_address)
interface['addresses'] = current_addresses
print(f"✅ Added VLAN 11 IP: {vlan11_address}")
else:
print(f"✅ VLAN 11 IP already present: {vlan11_address}")
# Add routes for VLAN 11 if needed
if 'routes' not in interface:
interface['routes'] = []
# Check if route to VLAN 11 network exists
vlan11_route = {
'to': '192.168.11.0/24',
'via': '$VLAN11_GATEWAY'
}
route_exists = any(
r.get('to') == '192.168.11.0/24'
for r in interface['routes']
)
if not route_exists:
interface['routes'].append(vlan11_route)
print(f"✅ Added route to VLAN 11 network")
else:
print(f"✅ Route to VLAN 11 network already exists")
# Write updated config
with open("$NETPLAN_FILE", 'w') as f:
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
print("✅ Netplan configuration updated")
EOF
if [ $? -ne 0 ]; then
echo "❌ Failed to update netplan configuration"
echo " Restoring backup..."
cp "$BACKUP_FILE" "$NETPLAN_FILE"
exit 1
fi
echo ""
echo "📋 Updated configuration:"
cat "$NETPLAN_FILE"
echo ""
# Apply netplan configuration
echo "🔄 Applying netplan configuration..."
if netplan try --timeout 5 >/dev/null 2>&1; then
netplan apply
echo "✅ Configuration applied successfully"
else
echo "⚠️ Validation failed. Restoring backup..."
cp "$BACKUP_FILE" "$NETPLAN_FILE"
netplan apply
exit 1
fi
# Wait for network
sleep 3
# Verify
echo ""
echo "🔍 Verifying configuration..."
NEW_IPS=$(ip -4 addr show $PRIMARY_IF | grep "inet " | awk '{print $2}')
echo " Current IP addresses on $PRIMARY_IF:"
echo "$NEW_IPS" | sed 's/^/ /'
if echo "$NEW_IPS" | grep -q "$VLAN11_IP"; then
echo " ✅ VLAN 11 IP ($VLAN11_IP) is configured"
else
echo " ⚠️ VLAN 11 IP not found (may need to check configuration)"
fi
# Test connectivity
echo ""
echo "🧪 Testing Connectivity..."
if ping -c 1 -W 2 $VLAN11_GATEWAY >/dev/null 2>&1; then
echo " ✅ VLAN 11 gateway ($VLAN11_GATEWAY) is reachable"
else
echo " ⚠️ VLAN 11 gateway ($VLAN11_GATEWAY) is not reachable"
fi
echo ""
echo "✅ Configuration complete and persistent!"
echo ""
echo "💡 The configuration will persist across reboots."
echo ""

View File

@@ -0,0 +1,97 @@
#!/bin/bash
set -euo pipefail
# Load IP configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
# Simple script to add VLAN 11 secondary IP (works on any Linux system)
# Uses direct ip commands - can be made persistent with systemd service
set -e
# Configuration
VLAN11_IP="${IP_SERVICE_23:-${IP_SERVICE_23:-192.168.11.23}}"
VLAN11_NETMASK="24"
VLAN11_GATEWAY="${NETWORK_GATEWAY:-192.168.11.1}"
PRIMARY_IF="eth0"
echo "🔧 Adding VLAN 11 Secondary IP Address"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ This script must be run with sudo"
echo " Usage: sudo $0"
exit 1
fi
CURRENT_IP=$(ip -4 addr show $PRIMARY_IF 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
echo "📋 Configuration:"
echo " Primary Interface: $PRIMARY_IF"
echo " Current IP: $CURRENT_IP"
echo " VLAN 11 IP: $VLAN11_IP/$VLAN11_NETMASK"
echo " VLAN 11 Gateway: $VLAN11_GATEWAY"
echo ""
# Check if VLAN 11 IP already exists
if ip addr show $PRIMARY_IF | grep -q "$VLAN11_IP"; then
echo "✅ VLAN 11 IP ($VLAN11_IP) already configured on $PRIMARY_IF"
exit 0
fi
# Add secondary IP address
echo " Adding secondary IP address $VLAN11_IP/$VLAN11_NETMASK to $PRIMARY_IF..."
ip addr add $VLAN11_IP/$VLAN11_NETMASK dev $PRIMARY_IF
if [ $? -eq 0 ]; then
echo " ✅ Secondary IP added successfully"
else
echo " ❌ Failed to add secondary IP"
exit 1
fi
# Add route to VLAN 11 network (if not already present)
if ! ip route show | grep -q "${NETWORK_192_168_11_0:-192.168.11.0}/24"; then
echo " Adding route to VLAN 11 network..."
ip route add ${NETWORK_192_168_11_0:-192.168.11.0}/24 dev $PRIMARY_IF src $VLAN11_IP
echo " ✅ Route added"
else
echo " ✅ Route to VLAN 11 already exists"
fi
# Test connectivity
echo ""
echo "🧪 Testing Connectivity..."
sleep 2
# Test VLAN 11 gateway
if ping -c 1 -W 2 $VLAN11_GATEWAY >/dev/null 2>&1; then
echo " ✅ VLAN 11 gateway ($VLAN11_GATEWAY) is reachable"
else
echo " ⚠️ VLAN 11 gateway ($VLAN11_GATEWAY) is not reachable"
echo " (This may be normal if gateway is not configured)"
fi
# Test Proxmox hosts
for host in "${PROXMOX_HOST_ML110:-192.168.11.10}:ml110" "${PROXMOX_HOST_R630_01:-192.168.11.11}:r630-01" "${PROXMOX_HOST_R630_02:-192.168.11.12}:r630-02"; do
IFS=':' read -r ip name <<< "$host"
if ping -c 1 -W 2 $ip >/dev/null 2>&1; then
echo "$name ($ip) is reachable"
else
echo " ⚠️ $name ($ip) is not reachable"
fi
done
echo ""
echo "✅ Configuration Complete!"
echo ""
echo "📋 Current IP Addresses:"
ip addr show $PRIMARY_IF | grep "inet " | sed 's/^/ /'
echo ""
echo "💡 Note: This configuration is temporary and will be lost on reboot."
echo " To make it persistent, see: docs/04-configuration/ADD_VLAN11_SECONDARY_IP_GUIDE.md"
echo ""

View File

@@ -0,0 +1,91 @@
#!/bin/bash
set -euo pipefail
# Simple script to add VLAN 11 secondary IP (works on any Linux system)
# Uses direct ip commands - can be made persistent with systemd service
set -e
# Configuration
VLAN11_IP="192.168.11.23"
VLAN11_NETMASK="24"
VLAN11_GATEWAY="192.168.11.1"
PRIMARY_IF="eth0"
echo "🔧 Adding VLAN 11 Secondary IP Address"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ This script must be run with sudo"
echo " Usage: sudo $0"
exit 1
fi
CURRENT_IP=$(ip -4 addr show $PRIMARY_IF 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
echo "📋 Configuration:"
echo " Primary Interface: $PRIMARY_IF"
echo " Current IP: $CURRENT_IP"
echo " VLAN 11 IP: $VLAN11_IP/$VLAN11_NETMASK"
echo " VLAN 11 Gateway: $VLAN11_GATEWAY"
echo ""
# Check if VLAN 11 IP already exists
if ip addr show $PRIMARY_IF | grep -q "$VLAN11_IP"; then
echo "✅ VLAN 11 IP ($VLAN11_IP) already configured on $PRIMARY_IF"
exit 0
fi
# Add secondary IP address
echo " Adding secondary IP address $VLAN11_IP/$VLAN11_NETMASK to $PRIMARY_IF..."
ip addr add $VLAN11_IP/$VLAN11_NETMASK dev $PRIMARY_IF
if [ $? -eq 0 ]; then
echo " ✅ Secondary IP added successfully"
else
echo " ❌ Failed to add secondary IP"
exit 1
fi
# Add route to VLAN 11 network (if not already present)
if ! ip route show | grep -q "192.168.11.0/24"; then
echo " Adding route to VLAN 11 network..."
ip route add 192.168.11.0/24 dev $PRIMARY_IF src $VLAN11_IP
echo " ✅ Route added"
else
echo " ✅ Route to VLAN 11 already exists"
fi
# Test connectivity
echo ""
echo "🧪 Testing Connectivity..."
sleep 2
# Test VLAN 11 gateway
if ping -c 1 -W 2 $VLAN11_GATEWAY >/dev/null 2>&1; then
echo " ✅ VLAN 11 gateway ($VLAN11_GATEWAY) is reachable"
else
echo " ⚠️ VLAN 11 gateway ($VLAN11_GATEWAY) is not reachable"
echo " (This may be normal if gateway is not configured)"
fi
# Test Proxmox hosts
for host in "192.168.11.10:ml110" "192.168.11.11:r630-01" "192.168.11.12:r630-02"; do
IFS=':' read -r ip name <<< "$host"
if ping -c 1 -W 2 $ip >/dev/null 2>&1; then
echo "$name ($ip) is reachable"
else
echo " ⚠️ $name ($ip) is not reachable"
fi
done
echo ""
echo "✅ Configuration Complete!"
echo ""
echo "📋 Current IP Addresses:"
ip addr show $PRIMARY_IF | grep "inet " | sed 's/^/ /'
echo ""
echo "💡 Note: This configuration is temporary and will be lost on reboot."
echo " To make it persistent, see: docs/04-configuration/ADD_VLAN11_SECONDARY_IP_GUIDE.md"
echo ""

View File

@@ -0,0 +1,152 @@
#!/bin/bash
set -euo pipefail
# Load IP configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
# Add VLAN 11 secondary IP with systemd service for persistence (WSL2 compatible)
# Creates a systemd service that runs on boot to add the IP
set -e
# Configuration
VLAN11_IP="${IP_SERVICE_23:-${IP_SERVICE_23:-192.168.11.23}}"
VLAN11_NETMASK="24"
VLAN11_GATEWAY="${NETWORK_GATEWAY:-192.168.11.1}"
PRIMARY_IF="eth0"
echo "🔧 Adding VLAN 11 Secondary IP Address (Persistent via systemd)"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ This script must be run with sudo"
echo " Usage: sudo $0"
exit 1
fi
CURRENT_IP=$(ip -4 addr show $PRIMARY_IF 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
echo "📋 Configuration:"
echo " Primary Interface: $PRIMARY_IF"
echo " Current IP: $CURRENT_IP"
echo " VLAN 11 IP: $VLAN11_IP/$VLAN11_NETMASK"
echo " VLAN 11 Gateway: $VLAN11_GATEWAY"
echo ""
# Add IP immediately
echo " Adding VLAN 11 IP address immediately..."
ip addr add $VLAN11_IP/$VLAN11_NETMASK dev $PRIMARY_IF 2>/dev/null || true
# Add route
if ! ip route show | grep -q "${NETWORK_192_168_11_0:-192.168.11.0}/24"; then
ip route add ${NETWORK_192_168_11_0:-192.168.11.0}/24 dev $PRIMARY_IF src $VLAN11_IP 2>/dev/null || true
fi
echo " ✅ IP added (temporary)"
echo ""
# Create systemd service for persistence
SERVICE_FILE="/etc/systemd/system/add-vlan11-ip.service"
SCRIPT_FILE="/usr/local/bin/add-vlan11-ip.sh"
echo "📝 Creating systemd service for persistence..."
echo ""
# Create script
cat > "$SCRIPT_FILE" << 'EOFSCRIPT'
#!/bin/bash
# Script to add VLAN 11 secondary IP
PRIMARY_IF="eth0"
VLAN11_IP="${IP_SERVICE_23:-${IP_SERVICE_23:-192.168.11.23}}"
VLAN11_NETMASK="24"
VLAN11_GATEWAY="${NETWORK_GATEWAY:-192.168.11.1}"
# Wait for interface to be up
sleep 2
# Add IP if not already present
if ! ip addr show $PRIMARY_IF | grep -q "$VLAN11_IP"; then
ip addr add $VLAN11_IP/$VLAN11_NETMASK dev $PRIMARY_IF
fi
# Add route if not present
if ! ip route show | grep -q "${NETWORK_192_168_11_0:-192.168.11.0}/24"; then
ip route add ${NETWORK_192_168_11_0:-192.168.11.0}/24 dev $PRIMARY_IF src $VLAN11_IP
fi
EOFSCRIPT
chmod +x "$SCRIPT_FILE"
echo " ✅ Script created: $SCRIPT_FILE"
# Create systemd service
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=Add VLAN 11 Secondary IP Address
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=$SCRIPT_FILE
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
echo " ✅ Service created: $SERVICE_FILE"
echo ""
# Enable and start service
echo "🔄 Enabling systemd service..."
systemctl daemon-reload
systemctl enable add-vlan11-ip.service
systemctl start add-vlan11-ip.service
if [ $? -eq 0 ]; then
echo " ✅ Service enabled and started"
else
echo " ⚠️ Service may not be available (WSL2 may not support systemd)"
echo " 💡 IP is added temporarily - will need to run script on each boot"
fi
# Verify
echo ""
echo "🔍 Verifying configuration..."
sleep 2
NEW_IPS=$(ip -4 addr show $PRIMARY_IF | grep "inet " | awk '{print $2}')
echo " Current IP addresses on $PRIMARY_IF:"
echo "$NEW_IPS" | sed 's/^/ /'
if echo "$NEW_IPS" | grep -q "$VLAN11_IP"; then
echo " ✅ VLAN 11 IP ($VLAN11_IP) is configured"
else
echo " ⚠️ VLAN 11 IP not found"
fi
# Test connectivity
echo ""
echo "🧪 Testing Connectivity..."
if ping -c 1 -W 2 $VLAN11_GATEWAY >/dev/null 2>&1; then
echo " ✅ VLAN 11 gateway ($VLAN11_GATEWAY) is reachable"
else
echo " ⚠️ VLAN 11 gateway ($VLAN11_GATEWAY) is not reachable"
fi
echo ""
echo "✅ Configuration complete!"
echo ""
if systemctl is-enabled add-vlan11-ip.service >/dev/null 2>&1; then
echo "💡 Service is enabled - IP will be added automatically on boot"
else
echo "💡 Note: On WSL2, you may need to run the script manually on each boot"
echo " Or add to ~/.bashrc or ~/.profile:"
echo " sudo ip addr add $VLAN11_IP/$VLAN11_NETMASK dev $PRIMARY_IF"
fi
echo ""

View File

@@ -0,0 +1,146 @@
#!/bin/bash
set -euo pipefail
# Add VLAN 11 secondary IP with systemd service for persistence (WSL2 compatible)
# Creates a systemd service that runs on boot to add the IP
set -e
# Configuration
VLAN11_IP="192.168.11.23"
VLAN11_NETMASK="24"
VLAN11_GATEWAY="192.168.11.1"
PRIMARY_IF="eth0"
echo "🔧 Adding VLAN 11 Secondary IP Address (Persistent via systemd)"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ This script must be run with sudo"
echo " Usage: sudo $0"
exit 1
fi
CURRENT_IP=$(ip -4 addr show $PRIMARY_IF 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
echo "📋 Configuration:"
echo " Primary Interface: $PRIMARY_IF"
echo " Current IP: $CURRENT_IP"
echo " VLAN 11 IP: $VLAN11_IP/$VLAN11_NETMASK"
echo " VLAN 11 Gateway: $VLAN11_GATEWAY"
echo ""
# Add IP immediately
echo " Adding VLAN 11 IP address immediately..."
ip addr add $VLAN11_IP/$VLAN11_NETMASK dev $PRIMARY_IF 2>/dev/null || true
# Add route
if ! ip route show | grep -q "192.168.11.0/24"; then
ip route add 192.168.11.0/24 dev $PRIMARY_IF src $VLAN11_IP 2>/dev/null || true
fi
echo " ✅ IP added (temporary)"
echo ""
# Create systemd service for persistence
SERVICE_FILE="/etc/systemd/system/add-vlan11-ip.service"
SCRIPT_FILE="/usr/local/bin/add-vlan11-ip.sh"
echo "📝 Creating systemd service for persistence..."
echo ""
# Create script
cat > "$SCRIPT_FILE" << 'EOFSCRIPT'
#!/bin/bash
# Script to add VLAN 11 secondary IP
PRIMARY_IF="eth0"
VLAN11_IP="192.168.11.23"
VLAN11_NETMASK="24"
VLAN11_GATEWAY="192.168.11.1"
# Wait for interface to be up
sleep 2
# Add IP if not already present
if ! ip addr show $PRIMARY_IF | grep -q "$VLAN11_IP"; then
ip addr add $VLAN11_IP/$VLAN11_NETMASK dev $PRIMARY_IF
fi
# Add route if not present
if ! ip route show | grep -q "192.168.11.0/24"; then
ip route add 192.168.11.0/24 dev $PRIMARY_IF src $VLAN11_IP
fi
EOFSCRIPT
chmod +x "$SCRIPT_FILE"
echo " ✅ Script created: $SCRIPT_FILE"
# Create systemd service
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=Add VLAN 11 Secondary IP Address
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=$SCRIPT_FILE
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
echo " ✅ Service created: $SERVICE_FILE"
echo ""
# Enable and start service
echo "🔄 Enabling systemd service..."
systemctl daemon-reload
systemctl enable add-vlan11-ip.service
systemctl start add-vlan11-ip.service
if [ $? -eq 0 ]; then
echo " ✅ Service enabled and started"
else
echo " ⚠️ Service may not be available (WSL2 may not support systemd)"
echo " 💡 IP is added temporarily - will need to run script on each boot"
fi
# Verify
echo ""
echo "🔍 Verifying configuration..."
sleep 2
NEW_IPS=$(ip -4 addr show $PRIMARY_IF | grep "inet " | awk '{print $2}')
echo " Current IP addresses on $PRIMARY_IF:"
echo "$NEW_IPS" | sed 's/^/ /'
if echo "$NEW_IPS" | grep -q "$VLAN11_IP"; then
echo " ✅ VLAN 11 IP ($VLAN11_IP) is configured"
else
echo " ⚠️ VLAN 11 IP not found"
fi
# Test connectivity
echo ""
echo "🧪 Testing Connectivity..."
if ping -c 1 -W 2 $VLAN11_GATEWAY >/dev/null 2>&1; then
echo " ✅ VLAN 11 gateway ($VLAN11_GATEWAY) is reachable"
else
echo " ⚠️ VLAN 11 gateway ($VLAN11_GATEWAY) is not reachable"
fi
echo ""
echo "✅ Configuration complete!"
echo ""
if systemctl is-enabled add-vlan11-ip.service >/dev/null 2>&1; then
echo "💡 Service is enabled - IP will be added automatically on boot"
else
echo "💡 Note: On WSL2, you may need to run the script manually on each boot"
echo " Or add to ~/.bashrc or ~/.profile:"
echo " sudo ip addr add $VLAN11_IP/$VLAN11_NETMASK dev $PRIMARY_IF"
fi
echo ""

View File

@@ -0,0 +1,104 @@
#!/bin/bash
set -euo pipefail
# Load IP configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
# Add VLAN 11 secondary IP address to current machine
# This allows the machine to have both its current IP and an IP on VLAN 11
set -e
# Configuration
VLAN11_IP="${IP_SERVICE_23:-${IP_SERVICE_23:-192.168.11.23}}"
VLAN11_NETMASK="24"
VLAN11_GATEWAY="${NETWORK_GATEWAY:-192.168.11.1}"
# Detect primary interface
PRIMARY_IF=$(ip route show | grep default | awk '{print $5}' | head -1)
if [ -z "$PRIMARY_IF" ]; then
echo "❌ Could not detect primary network interface"
exit 1
fi
CURRENT_IP=$(ip -4 addr show $PRIMARY_IF 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
CURRENT_GW=$(ip route show | grep default | awk '{print $3}' | head -1)
echo "🔧 Adding VLAN 11 Secondary IP Address"
echo ""
echo "📋 Configuration:"
echo " Primary Interface: $PRIMARY_IF"
echo " Current IP: $CURRENT_IP"
echo " Current Gateway: $CURRENT_GW"
echo " VLAN 11 IP: $VLAN11_IP/$VLAN11_NETMASK"
echo " VLAN 11 Gateway: $VLAN11_GATEWAY"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ This script must be run with sudo"
echo " Usage: sudo $0"
exit 1
fi
# Check if VLAN 11 IP already exists
if ip addr show $PRIMARY_IF | grep -q "$VLAN11_IP"; then
echo "✅ VLAN 11 IP ($VLAN11_IP) already configured on $PRIMARY_IF"
exit 0
fi
# Add secondary IP address
echo " Adding secondary IP address $VLAN11_IP/$VLAN11_NETMASK to $PRIMARY_IF..."
ip addr add $VLAN11_IP/$VLAN11_NETMASK dev $PRIMARY_IF
if [ $? -eq 0 ]; then
echo " ✅ Secondary IP added successfully"
else
echo " ❌ Failed to add secondary IP"
exit 1
fi
# Add route to VLAN 11 network (if not already present)
if ! ip route show | grep -q "${NETWORK_192_168_11_0:-192.168.11.0}/24"; then
echo " Adding route to VLAN 11 network..."
ip route add ${NETWORK_192_168_11_0:-192.168.11.0}/24 dev $PRIMARY_IF src $VLAN11_IP
echo " ✅ Route added"
else
echo " ✅ Route to VLAN 11 already exists"
fi
# Test connectivity
echo ""
echo "🧪 Testing Connectivity..."
sleep 2
# Test VLAN 11 gateway
if ping -c 1 -W 2 $VLAN11_GATEWAY >/dev/null 2>&1; then
echo " ✅ VLAN 11 gateway ($VLAN11_GATEWAY) is reachable"
else
echo " ⚠️ VLAN 11 gateway ($VLAN11_GATEWAY) is not reachable"
echo " (This may be normal if gateway is not configured)"
fi
# Test Proxmox hosts
for host in "${PROXMOX_HOST_ML110:-192.168.11.10}:ml110" "${PROXMOX_HOST_R630_01:-192.168.11.11}:r630-01" "${PROXMOX_HOST_R630_02:-192.168.11.12}:r630-02"; do
IFS=':' read -r ip name <<< "$host"
if ping -c 1 -W 2 $ip >/dev/null 2>&1; then
echo "$name ($ip) is reachable"
else
echo " ⚠️ $name ($ip) is not reachable"
fi
done
echo ""
echo "✅ Configuration Complete!"
echo ""
echo "📋 Current IP Addresses:"
ip addr show $PRIMARY_IF | grep "inet " | sed 's/^/ /'
echo ""
echo "💡 Note: This configuration is temporary and will be lost on reboot."
echo " To make it persistent, configure netplan or network manager."
echo ""

View File

@@ -0,0 +1,98 @@
#!/bin/bash
set -euo pipefail
# Add VLAN 11 secondary IP address to current machine
# This allows the machine to have both its current IP and an IP on VLAN 11
set -e
# Configuration
VLAN11_IP="192.168.11.23"
VLAN11_NETMASK="24"
VLAN11_GATEWAY="192.168.11.1"
# Detect primary interface
PRIMARY_IF=$(ip route show | grep default | awk '{print $5}' | head -1)
if [ -z "$PRIMARY_IF" ]; then
echo "❌ Could not detect primary network interface"
exit 1
fi
CURRENT_IP=$(ip -4 addr show $PRIMARY_IF 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
CURRENT_GW=$(ip route show | grep default | awk '{print $3}' | head -1)
echo "🔧 Adding VLAN 11 Secondary IP Address"
echo ""
echo "📋 Configuration:"
echo " Primary Interface: $PRIMARY_IF"
echo " Current IP: $CURRENT_IP"
echo " Current Gateway: $CURRENT_GW"
echo " VLAN 11 IP: $VLAN11_IP/$VLAN11_NETMASK"
echo " VLAN 11 Gateway: $VLAN11_GATEWAY"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ This script must be run with sudo"
echo " Usage: sudo $0"
exit 1
fi
# Check if VLAN 11 IP already exists
if ip addr show $PRIMARY_IF | grep -q "$VLAN11_IP"; then
echo "✅ VLAN 11 IP ($VLAN11_IP) already configured on $PRIMARY_IF"
exit 0
fi
# Add secondary IP address
echo " Adding secondary IP address $VLAN11_IP/$VLAN11_NETMASK to $PRIMARY_IF..."
ip addr add $VLAN11_IP/$VLAN11_NETMASK dev $PRIMARY_IF
if [ $? -eq 0 ]; then
echo " ✅ Secondary IP added successfully"
else
echo " ❌ Failed to add secondary IP"
exit 1
fi
# Add route to VLAN 11 network (if not already present)
if ! ip route show | grep -q "192.168.11.0/24"; then
echo " Adding route to VLAN 11 network..."
ip route add 192.168.11.0/24 dev $PRIMARY_IF src $VLAN11_IP
echo " ✅ Route added"
else
echo " ✅ Route to VLAN 11 already exists"
fi
# Test connectivity
echo ""
echo "🧪 Testing Connectivity..."
sleep 2
# Test VLAN 11 gateway
if ping -c 1 -W 2 $VLAN11_GATEWAY >/dev/null 2>&1; then
echo " ✅ VLAN 11 gateway ($VLAN11_GATEWAY) is reachable"
else
echo " ⚠️ VLAN 11 gateway ($VLAN11_GATEWAY) is not reachable"
echo " (This may be normal if gateway is not configured)"
fi
# Test Proxmox hosts
for host in "192.168.11.10:ml110" "192.168.11.11:r630-01" "192.168.11.12:r630-02"; do
IFS=':' read -r ip name <<< "$host"
if ping -c 1 -W 2 $ip >/dev/null 2>&1; then
echo "$name ($ip) is reachable"
else
echo " ⚠️ $name ($ip) is not reachable"
fi
done
echo ""
echo "✅ Configuration Complete!"
echo ""
echo "📋 Current IP Addresses:"
ip addr show $PRIMARY_IF | grep "inet " | sed 's/^/ /'
echo ""
echo "💡 Note: This configuration is temporary and will be lost on reboot."
echo " To make it persistent, configure netplan or network manager."
echo ""

View File

@@ -0,0 +1,193 @@
#!/usr/bin/env node
/**
* Create firewall rule to allow 192.168.0.0/24 (UDM Pro default network) to access VLAN 11
* This fixes the "Destination Host Unreachable" issue when pinging 192.168.11.10 from 192.168.0.23
*/
import { readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
// Load environment variables
const envPath = join(homedir(), '.env');
function loadEnvFile(filePath) {
try {
const envFile = readFileSync(filePath, 'utf8');
const envVars = envFile.split('\n').filter(
(line) => line.includes('=') && !line.trim().startsWith('#')
);
for (const line of envVars) {
const [key, ...values] = line.split('=');
if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) {
let value = values.join('=').trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key.trim()] = value;
}
}
return true;
} catch {
return false;
}
}
loadEnvFile(envPath);
const baseUrl = process.env.UNIFI_UDM_URL || 'https://192.168.0.1';
const apiKey = process.env.UNIFI_API_KEY;
const siteId = '88f7af54-98f8-306a-a1c7-c9349722b1f6';
if (!apiKey) {
console.error('❌ UNIFI_API_KEY not set in environment');
process.exit(1);
}
console.log('Creating Firewall Rule: Allow 192.168.0.0/24 → VLAN 11');
console.log('================================================================');
console.log('');
// Fetch networks
async function fetchNetworks() {
const response = await fetch(`${baseUrl}/proxy/network/integration/v1/sites/${siteId}/networks`, {
headers: {
'X-API-KEY': apiKey,
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch networks: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const networks = data.data || [];
const vlanMap = {};
for (const net of networks) {
const vlanId = net.vlanId;
if (vlanId && vlanId > 1) {
vlanMap[vlanId] = {
id: net.id,
name: net.name,
subnet: net.ipSubnet,
};
}
}
return vlanMap;
}
// Create ACL rule using IP address filter
async function createACLRule(ruleConfig) {
const { name, description, action, index, sourceIP, destNetworkId, protocolFilter } = ruleConfig;
const rule = {
type: 'IPV4',
enabled: true,
name,
description,
action,
index,
sourceFilter: sourceIP
? { type: 'IP_ADDRESSES_OR_SUBNETS', ipAddressesOrSubnets: [sourceIP] }
: null,
destinationFilter: destNetworkId
? { type: 'NETWORKS', networkIds: [destNetworkId] }
: null,
protocolFilter: protocolFilter || null,
enforcingDeviceFilter: null,
};
const response = await fetch(`${baseUrl}/proxy/network/integration/v1/sites/${siteId}/acl-rules`, {
method: 'POST',
headers: {
'X-API-KEY': apiKey,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(rule),
});
const responseText = await response.text();
let responseData;
try {
responseData = JSON.parse(responseText);
} catch {
responseData = responseText;
}
if (response.ok) {
return { success: true, data: responseData };
} else {
return { success: false, error: responseData, status: response.status };
}
}
// Main function
async function main() {
try {
console.log('Fetching network IDs...');
const vlanMap = await fetchNetworks();
const vlan11Network = vlanMap[11];
if (!vlan11Network) {
console.error('❌ VLAN 11 (MGMT-LAN) not found');
process.exit(1);
}
console.log(`✅ Found VLAN 11 (MGMT-LAN): ${vlan11Network.name}`);
console.log(` Network ID: ${vlan11Network.id}`);
console.log(` Subnet: ${vlan11Network.subnet}`);
console.log('');
// Create rule to allow 192.168.0.0/24 → VLAN 11
console.log('Creating firewall rule...');
console.log('');
const ruleConfig = {
name: 'Allow Default Network to Management VLAN',
description: 'Allow 192.168.0.0/24 (UDM Pro default network) to access VLAN 11 (MGMT-LAN)',
action: 'ALLOW',
index: 5, // Higher priority than service VLAN rules
sourceIP: '192.168.0.0/24',
destNetworkId: vlan11Network.id,
protocolFilter: null, // Allow all protocols (ICMP, TCP, UDP)
};
console.log(`Rule: ${ruleConfig.name}`);
console.log(`Source: ${ruleConfig.sourceIP}`);
console.log(`Destination: VLAN 11 (${vlan11Network.name})`);
console.log(`Protocol: All`);
console.log('');
const result = await createACLRule(ruleConfig);
if (result.success) {
console.log('✅ Rule created successfully!');
console.log('');
console.log('This rule allows devices on 192.168.0.0/24 to access VLAN 11 (192.168.11.0/24)');
console.log('You should now be able to ping 192.168.11.10 from 192.168.0.23');
} else {
console.log(`❌ Failed to create rule (HTTP ${result.status})`);
if (result.error && typeof result.error === 'object') {
console.log(`Error: ${JSON.stringify(result.error, null, 2)}`);
} else {
console.log(`Error: ${result.error}`);
}
process.exit(1);
}
} catch (error) {
console.error('❌ Error:', error.message);
if (error.stack) {
console.error(error.stack);
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,332 @@
#!/usr/bin/env node
/**
* Visual Page Analyzer
*
* This script opens the routing page in a visible browser and provides
* interactive analysis tools to identify the Add button location.
*/
import { chromium } from 'playwright';
import { readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import readline from 'readline';
// Load environment variables
const envPath = join(homedir(), '.env');
function loadEnvFile(filePath) {
try {
const envFile = readFileSync(filePath, 'utf8');
const envVars = envFile.split('\n').filter(
(line) => line.includes('=') && !line.trim().startsWith('#')
);
for (const line of envVars) {
const [key, ...values] = line.split('=');
if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) {
let value = values.join('=').trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key.trim()] = value;
}
}
return true;
} catch {
return false;
}
}
loadEnvFile(envPath);
const UDM_URL = process.env.UNIFI_UDM_URL || 'https://192.168.0.1';
const USERNAME = process.env.UNIFI_BROWSER_USERNAME || process.env.UNIFI_USERNAME || 'unifi_api';
const PASSWORD = process.env.UNIFI_BROWSER_PASSWORD || process.env.UNIFI_PASSWORD;
console.log('🔍 Visual Page Analyzer');
console.log('======================\n');
if (!PASSWORD) {
console.error('❌ UNIFI_PASSWORD must be set in ~/.env');
process.exit(1);
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function question(prompt) {
return new Promise((resolve) => {
rl.question(prompt, resolve);
});
}
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 100 });
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
try {
console.log('1. Logging in...');
await page.goto(UDM_URL, { waitUntil: 'networkidle' });
await page.waitForSelector('input[type="text"]');
await page.fill('input[type="text"]', USERNAME);
await page.fill('input[type="password"]', PASSWORD);
await page.click('button[type="submit"]');
await page.waitForTimeout(5000);
console.log('2. Navigating to Routing page...');
await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'networkidle' });
await page.waitForTimeout(10000);
// Wait for URL to be correct
await page.waitForURL('**/settings/routing**', { timeout: 20000 }).catch(() => {});
await page.waitForTimeout(5000);
console.log(` Current URL: ${page.url()}`);
// Wait for routes API
try {
await page.waitForResponse(response =>
response.url().includes('/rest/routing') || response.url().includes('/trafficroutes'),
{ timeout: 15000 }
);
console.log(' Routes API loaded');
} catch (error) {
console.log(' Routes API not detected');
}
await page.waitForTimeout(5000);
console.log('\n3. Page Analysis Tools Available:');
console.log('='.repeat(80));
// Highlight all buttons
await page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll('button, [role="button"]'));
buttons.forEach((btn, index) => {
const rect = btn.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
const styles = window.getComputedStyle(btn);
if (styles.display !== 'none' && styles.visibility !== 'hidden') {
// Add highlight
btn.style.outline = '3px solid red';
btn.style.outlineOffset = '2px';
btn.setAttribute('data-analyzer-index', index);
}
}
});
});
// Get button information
const buttonInfo = await page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll('button, [role="button"]'));
return buttons.map((btn, index) => {
const rect = btn.getBoundingClientRect();
const styles = window.getComputedStyle(btn);
if (rect.width > 0 && rect.height > 0 && styles.display !== 'none') {
return {
index,
text: btn.textContent?.trim() || '',
className: btn.className || '',
id: btn.id || '',
ariaLabel: btn.getAttribute('aria-label') || '',
dataTestId: btn.getAttribute('data-testid') || '',
position: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
iconOnly: !btn.textContent?.trim() && btn.querySelector('svg') !== null,
enabled: !btn.disabled,
selector: btn.id ? `#${btn.id}` : `.${btn.className.split(' ')[0]}`,
};
}
return null;
}).filter(b => b !== null);
});
console.log(`\n📊 Found ${buttonInfo.length} buttons on page:`);
buttonInfo.forEach((btn, i) => {
console.log(`\n${i + 1}. Button ${btn.index}:`);
console.log(` Text: "${btn.text}"`);
console.log(` Class: ${btn.className.substring(0, 80)}`);
console.log(` ID: ${btn.id || 'none'}`);
console.log(` Aria Label: ${btn.ariaLabel || 'none'}`);
console.log(` Position: (${btn.position.x}, ${btn.position.y}) ${btn.position.width}x${btn.position.height}`);
console.log(` Icon Only: ${btn.iconOnly}`);
console.log(` Enabled: ${btn.enabled}`);
console.log(` Selector: ${btn.selector}`);
});
// Highlight tables
await page.evaluate(() => {
const tables = Array.from(document.querySelectorAll('table'));
tables.forEach((table, index) => {
table.style.outline = '3px solid blue';
table.style.outlineOffset = '2px';
table.setAttribute('data-analyzer-table-index', index);
});
});
const tableInfo = await page.evaluate(() => {
const tables = Array.from(document.querySelectorAll('table'));
return tables.map((table, index) => {
const rect = table.getBoundingClientRect();
const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent?.trim() || '');
const rows = Array.from(table.querySelectorAll('tbody tr, tr')).length;
return {
index,
headers,
rowCount: rows,
position: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
buttonsInTable: Array.from(table.querySelectorAll('button')).length,
};
});
});
if (tableInfo.length > 0) {
console.log(`\n📋 Found ${tableInfo.length} tables:`);
tableInfo.forEach((table, i) => {
console.log(`\n${i + 1}. Table ${table.index}:`);
console.log(` Headers: ${table.headers.join(', ')}`);
console.log(` Rows: ${table.rowCount}`);
console.log(` Buttons in Table: ${table.buttonsInTable}`);
console.log(` Position: (${table.position.x}, ${table.position.y})`);
});
} else {
console.log('\n📋 No tables found on page');
}
// Find route-related text
const routeTexts = await page.evaluate(() => {
const texts = [];
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
let node;
while (node = walker.nextNode()) {
const text = node.textContent?.trim() || '';
if (text && (text.includes('Static Routes') || text.includes('Route') || text.includes('Add'))) {
const parent = node.parentElement;
if (parent) {
const rect = parent.getBoundingClientRect();
texts.push({
text: text.substring(0, 100),
tag: parent.tagName,
className: parent.className?.substring(0, 80) || '',
position: { x: Math.round(rect.x), y: Math.round(rect.y) },
});
}
}
}
return texts.slice(0, 20);
});
if (routeTexts.length > 0) {
console.log(`\n📝 Route-Related Text Found (${routeTexts.length}):`);
routeTexts.forEach((text, i) => {
console.log(`\n${i + 1}. "${text.text}"`);
console.log(` Tag: ${text.tag}, Class: ${text.className}`);
console.log(` Position: (${text.position.x}, ${text.position.y})`);
});
}
console.log('\n\n🎯 Interactive Testing:');
console.log('='.repeat(80));
console.log('Buttons are highlighted in RED');
console.log('Tables are highlighted in BLUE');
console.log('\nYou can now:');
console.log('1. Visually inspect the page in the browser');
console.log('2. Test clicking buttons to see what they do');
console.log('3. Identify the Add Route button location');
console.log('\nPress Enter to test clicking buttons, or type "exit" to close...\n');
let testing = true;
while (testing) {
const input = await question('Enter button number to test (1-' + buttonInfo.length + '), "screenshot" to save, or "exit": ');
if (input.toLowerCase() === 'exit') {
testing = false;
break;
}
if (input.toLowerCase() === 'screenshot') {
await page.screenshot({ path: 'analyzer-screenshot.png', fullPage: true });
console.log('✅ Screenshot saved: analyzer-screenshot.png');
continue;
}
const buttonNum = parseInt(input);
if (buttonNum >= 1 && buttonNum <= buttonInfo.length) {
const btn = buttonInfo[buttonNum - 1];
console.log(`\nTesting button ${buttonNum}: "${btn.text}"`);
console.log(`Selector: ${btn.selector}`);
try {
// Highlight the button
await page.evaluate((index) => {
const btn = document.querySelector(`[data-analyzer-index="${index}"]`);
if (btn) {
btn.style.backgroundColor = 'yellow';
btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, btn.index);
await page.waitForTimeout(1000);
// Try clicking
const selector = btn.id ? `#${btn.id}` : `button:nth-of-type(${btn.index + 1})`;
await page.click(selector, { timeout: 5000 }).catch(async (error) => {
console.log(` ⚠️ Regular click failed: ${error.message}`);
// Try JavaScript click
await page.evaluate((index) => {
const btn = document.querySelector(`[data-analyzer-index="${index}"]`);
if (btn) btn.click();
}, btn.index);
});
await page.waitForTimeout(3000);
// Check for form
const hasForm = await page.locator('input[name="name"], input[name="destination"], input[placeholder*="destination" i]').first().isVisible({ timeout: 2000 }).catch(() => false);
if (hasForm) {
console.log(' ✅✅✅ FORM APPEARED! This is the Add button! ✅✅✅');
console.log(` Use selector: ${btn.selector}`);
console.log(` Or ID: ${btn.id || 'none'}`);
console.log(` Or class: ${btn.className.split(' ')[0]}`);
testing = false;
break;
} else {
// Check for menu
const hasMenu = await page.locator('[role="menu"], [role="listbox"]').first().isVisible({ timeout: 2000 }).catch(() => false);
if (hasMenu) {
console.log(' ⚠️ Menu appeared (not form)');
const menuItems = await page.evaluate(() => {
const menu = document.querySelector('[role="menu"], [role="listbox"]');
if (!menu) return [];
return Array.from(menu.querySelectorAll('[role="menuitem"], [role="option"], li, div')).map(item => ({
text: item.textContent?.trim() || '',
tag: item.tagName,
})).filter(item => item.text.length > 0);
});
console.log(` Menu items: ${menuItems.map(m => `"${m.text}"`).join(', ')}`);
} else {
console.log(' ❌ No form or menu appeared');
}
}
} catch (error) {
console.log(` ❌ Error: ${error.message}`);
}
} else {
console.log('Invalid button number');
}
}
console.log('\n✅ Analysis complete. Closing browser...');
} catch (error) {
console.error('❌ Error:', error.message);
await page.screenshot({ path: 'analyzer-error.png', fullPage: true });
} finally {
rl.close();
await browser.close();
}
})();

View File

@@ -0,0 +1,173 @@
#!/bin/bash
set -euo pipefail
# Load IP configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
# Change dev machine IP to ${IP_SERVICE_4:-${IP_SERVICE_4:-192.168.11.4}} using netplan
# Usage: sudo ./scripts/unifi/change-ip-to-vlan11-netplan.sh
set -e
INTERFACE="eth0"
NEW_IP="${IP_SERVICE_4:-${IP_SERVICE_4:-192.168.11.4}}"
NEW_GATEWAY="${NETWORK_GATEWAY:-192.168.11.1}"
NEW_DNS="${NETWORK_GATEWAY:-192.168.11.1}"
echo "🔧 Changing IP address to $NEW_IP for interface $INTERFACE using netplan"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ Please run as root (use sudo)"
exit 1
fi
# Find netplan config file
NETPLAN_FILE=$(ls /etc/netplan/*.yaml 2>/dev/null | head -1)
if [ -z "$NETPLAN_FILE" ]; then
echo "❌ No netplan config file found in /etc/netplan/"
exit 1
fi
echo "📋 Found netplan config: $NETPLAN_FILE"
echo ""
# Backup original config
BACKUP_FILE="${NETPLAN_FILE}.backup.$(date +%Y%m%d_%H%M%S)"
cp "$NETPLAN_FILE" "$BACKUP_FILE"
echo "✅ Backup created: $BACKUP_FILE"
echo ""
# Read current config
CURRENT_CONFIG=$(cat "$NETPLAN_FILE")
# Check if interface exists in config
if ! echo "$CURRENT_CONFIG" | grep -q "$INTERFACE"; then
echo "⚠️ Interface $INTERFACE not found in config"
echo "Current config:"
cat "$NETPLAN_FILE"
echo ""
echo "Please manually edit $NETPLAN_FILE to add $INTERFACE configuration"
exit 1
fi
# Create new config (preserve structure, update IP settings)
echo "🔄 Updating netplan configuration..."
# Use Python to safely update YAML
python3 << EOF
import yaml
import sys
# Read current config
with open("$NETPLAN_FILE", 'r') as f:
config = yaml.safe_load(f)
# Update network configuration
if 'network' not in config:
config['network'] = {}
if 'ethernets' not in config['network']:
config['network']['ethernets'] = {}
if '$INTERFACE' not in config['network']['ethernets']:
config['network']['ethernets']['$INTERFACE'] = {}
# Update interface settings
config['network']['ethernets']['$INTERFACE']['addresses'] = ['$NEW_IP/24']
config['network']['ethernets']['$INTERFACE']['gateway4'] = '$NEW_GATEWAY'
config['network']['ethernets']['$INTERFACE']['nameservers'] = {
'addresses': ['$NEW_DNS', '8.8.8.8']
}
# Write updated config
with open("$NETPLAN_FILE", 'w') as f:
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
print("✅ Configuration updated")
EOF
if [ $? -ne 0 ]; then
echo "❌ Failed to update config. Restoring backup..."
cp "$BACKUP_FILE" "$NETPLAN_FILE"
exit 1
fi
echo ""
echo "📋 New configuration:"
cat "$NETPLAN_FILE"
echo ""
# Validate netplan config
echo "🔍 Validating netplan configuration..."
if netplan try --timeout 5 >/dev/null 2>&1; then
echo "✅ Configuration is valid"
echo ""
echo "🔄 Applying netplan configuration..."
netplan apply
echo "✅ Configuration applied"
else
echo "⚠️ Validation failed. Restoring backup..."
cp "$BACKUP_FILE" "$NETPLAN_FILE"
netplan apply
exit 1
fi
# Wait for network to stabilize
sleep 3
# Verify new IP
echo ""
echo "🔍 Verifying new IP configuration..."
NEW_IP_CHECK=$(ip addr show $INTERFACE 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
if [ "$NEW_IP_CHECK" = "$NEW_IP" ]; then
echo "✅ IP address successfully changed to $NEW_IP"
else
echo "⚠️ IP address check: $NEW_IP_CHECK (expected $NEW_IP)"
echo " This may take a few more seconds..."
sleep 5
NEW_IP_CHECK=$(ip addr show $INTERFACE 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
if [ "$NEW_IP_CHECK" = "$NEW_IP" ]; then
echo "✅ IP address is now $NEW_IP"
else
echo "⚠️ IP address is $NEW_IP_CHECK (expected $NEW_IP)"
fi
fi
# Test gateway
echo ""
echo "🧪 Testing gateway connectivity..."
if ping -c 1 -W 2 $NEW_GATEWAY >/dev/null 2>&1; then
echo "✅ Gateway $NEW_GATEWAY is reachable"
else
echo "⚠️ Gateway $NEW_GATEWAY is not reachable"
fi
# Test ml110
echo ""
echo "🧪 Testing ml110 connectivity..."
if ping -c 1 -W 2 ${PROXMOX_HOST_ML110:-192.168.11.10} >/dev/null 2>&1; then
echo "✅ ml110 (${PROXMOX_HOST_ML110:-192.168.11.10}) is reachable!"
echo ""
echo "🎉 SUCCESS! You can now access ml110!"
else
echo "⚠️ ml110 (${PROXMOX_HOST_ML110:-192.168.11.10}) is not reachable"
echo " This may be due to firewall on ml110"
echo " Check ml110 firewall settings"
fi
echo ""
echo "✅ IP change complete!"
echo ""
echo "📋 Current configuration:"
ip addr show $INTERFACE | grep "inet " || echo " (checking...)"
ip route show | grep default || echo " (checking...)"
echo ""
echo "💡 To revert back to original IP:"
echo " sudo cp $BACKUP_FILE $NETPLAN_FILE"
echo " sudo netplan apply"

View File

@@ -0,0 +1,167 @@
#!/bin/bash
set -euo pipefail
# Change dev machine IP to 192.168.11.4 using netplan
# Usage: sudo ./scripts/unifi/change-ip-to-vlan11-netplan.sh
set -e
INTERFACE="eth0"
NEW_IP="192.168.11.4"
NEW_GATEWAY="192.168.11.1"
NEW_DNS="192.168.11.1"
echo "🔧 Changing IP address to $NEW_IP for interface $INTERFACE using netplan"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ Please run as root (use sudo)"
exit 1
fi
# Find netplan config file
NETPLAN_FILE=$(ls /etc/netplan/*.yaml 2>/dev/null | head -1)
if [ -z "$NETPLAN_FILE" ]; then
echo "❌ No netplan config file found in /etc/netplan/"
exit 1
fi
echo "📋 Found netplan config: $NETPLAN_FILE"
echo ""
# Backup original config
BACKUP_FILE="${NETPLAN_FILE}.backup.$(date +%Y%m%d_%H%M%S)"
cp "$NETPLAN_FILE" "$BACKUP_FILE"
echo "✅ Backup created: $BACKUP_FILE"
echo ""
# Read current config
CURRENT_CONFIG=$(cat "$NETPLAN_FILE")
# Check if interface exists in config
if ! echo "$CURRENT_CONFIG" | grep -q "$INTERFACE"; then
echo "⚠️ Interface $INTERFACE not found in config"
echo "Current config:"
cat "$NETPLAN_FILE"
echo ""
echo "Please manually edit $NETPLAN_FILE to add $INTERFACE configuration"
exit 1
fi
# Create new config (preserve structure, update IP settings)
echo "🔄 Updating netplan configuration..."
# Use Python to safely update YAML
python3 << EOF
import yaml
import sys
# Read current config
with open("$NETPLAN_FILE", 'r') as f:
config = yaml.safe_load(f)
# Update network configuration
if 'network' not in config:
config['network'] = {}
if 'ethernets' not in config['network']:
config['network']['ethernets'] = {}
if '$INTERFACE' not in config['network']['ethernets']:
config['network']['ethernets']['$INTERFACE'] = {}
# Update interface settings
config['network']['ethernets']['$INTERFACE']['addresses'] = ['$NEW_IP/24']
config['network']['ethernets']['$INTERFACE']['gateway4'] = '$NEW_GATEWAY'
config['network']['ethernets']['$INTERFACE']['nameservers'] = {
'addresses': ['$NEW_DNS', '8.8.8.8']
}
# Write updated config
with open("$NETPLAN_FILE", 'w') as f:
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
print("✅ Configuration updated")
EOF
if [ $? -ne 0 ]; then
echo "❌ Failed to update config. Restoring backup..."
cp "$BACKUP_FILE" "$NETPLAN_FILE"
exit 1
fi
echo ""
echo "📋 New configuration:"
cat "$NETPLAN_FILE"
echo ""
# Validate netplan config
echo "🔍 Validating netplan configuration..."
if netplan try --timeout 5 >/dev/null 2>&1; then
echo "✅ Configuration is valid"
echo ""
echo "🔄 Applying netplan configuration..."
netplan apply
echo "✅ Configuration applied"
else
echo "⚠️ Validation failed. Restoring backup..."
cp "$BACKUP_FILE" "$NETPLAN_FILE"
netplan apply
exit 1
fi
# Wait for network to stabilize
sleep 3
# Verify new IP
echo ""
echo "🔍 Verifying new IP configuration..."
NEW_IP_CHECK=$(ip addr show $INTERFACE 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
if [ "$NEW_IP_CHECK" = "$NEW_IP" ]; then
echo "✅ IP address successfully changed to $NEW_IP"
else
echo "⚠️ IP address check: $NEW_IP_CHECK (expected $NEW_IP)"
echo " This may take a few more seconds..."
sleep 5
NEW_IP_CHECK=$(ip addr show $INTERFACE 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1)
if [ "$NEW_IP_CHECK" = "$NEW_IP" ]; then
echo "✅ IP address is now $NEW_IP"
else
echo "⚠️ IP address is $NEW_IP_CHECK (expected $NEW_IP)"
fi
fi
# Test gateway
echo ""
echo "🧪 Testing gateway connectivity..."
if ping -c 1 -W 2 $NEW_GATEWAY >/dev/null 2>&1; then
echo "✅ Gateway $NEW_GATEWAY is reachable"
else
echo "⚠️ Gateway $NEW_GATEWAY is not reachable"
fi
# Test ml110
echo ""
echo "🧪 Testing ml110 connectivity..."
if ping -c 1 -W 2 192.168.11.10 >/dev/null 2>&1; then
echo "✅ ml110 (192.168.11.10) is reachable!"
echo ""
echo "🎉 SUCCESS! You can now access ml110!"
else
echo "⚠️ ml110 (192.168.11.10) is not reachable"
echo " This may be due to firewall on ml110"
echo " Check ml110 firewall settings"
fi
echo ""
echo "✅ IP change complete!"
echo ""
echo "📋 Current configuration:"
ip addr show $INTERFACE | grep "inet " || echo " (checking...)"
ip route show | grep default || echo " (checking...)"
echo ""
echo "💡 To revert back to original IP:"
echo " sudo cp $BACKUP_FILE $NETPLAN_FILE"
echo " sudo netplan apply"

View File

@@ -0,0 +1,145 @@
#!/bin/bash
set -euo pipefail
# Load IP configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
# Change dev machine IP to ${IP_SERVICE_4:-${IP_SERVICE_4:-192.168.11.4}} for access to ml110
# Usage: sudo ./scripts/unifi/change-ip-to-vlan11.sh
set -e
INTERFACE="eth0"
NEW_IP="${IP_SERVICE_4:-${IP_SERVICE_4:-192.168.11.4}}"
NEW_GATEWAY="${NETWORK_GATEWAY:-192.168.11.1}"
NEW_NETMASK="255.255.255.0"
NEW_DNS="${NETWORK_GATEWAY:-192.168.11.1}"
echo "🔧 Changing IP address to $NEW_IP for interface $INTERFACE"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ Please run as root (use sudo)"
exit 1
fi
# Detect network manager
if command -v nmcli >/dev/null 2>&1; then
echo "✅ Using NetworkManager (nmcli)"
# Get active connection name
CONN_NAME=$(nmcli -t -f NAME connection show --active | grep -i "$INTERFACE" | head -1)
if [ -z "$CONN_NAME" ]; then
CONN_NAME=$(nmcli -t -f NAME,DEVICE connection show | grep "$INTERFACE" | cut -d: -f1 | head -1)
fi
if [ -z "$CONN_NAME" ]; then
echo "❌ Could not find connection for $INTERFACE"
echo "Available connections:"
nmcli connection show
exit 1
fi
echo "📋 Found connection: $CONN_NAME"
echo ""
echo "🔄 Changing IP configuration..."
# Change IP settings
nmcli connection modify "$CONN_NAME" ipv4.addresses "$NEW_IP/24"
nmcli connection modify "$CONN_NAME" ipv4.gateway "$NEW_GATEWAY"
nmcli connection modify "$CONN_NAME" ipv4.dns "$NEW_DNS 8.8.8.8"
nmcli connection modify "$CONN_NAME" ipv4.method manual
echo "✅ Configuration updated"
echo "🔄 Restarting connection..."
nmcli connection down "$CONN_NAME"
sleep 2
nmcli connection up "$CONN_NAME"
echo "✅ Connection restarted"
elif [ -d /etc/netplan ]; then
echo "✅ Using netplan"
# Find netplan config file
NETPLAN_FILE=$(ls /etc/netplan/*.yaml 2>/dev/null | head -1)
if [ -z "$NETPLAN_FILE" ]; then
echo "❌ No netplan config file found"
exit 1
fi
echo "📋 Found netplan config: $NETPLAN_FILE"
echo ""
echo "⚠️ Manual edit required for netplan"
echo "Please edit $NETPLAN_FILE and change:"
echo " addresses: [$NEW_IP/24]"
echo " gateway4: $NEW_GATEWAY"
echo " nameservers:"
echo " addresses: [$NEW_DNS, 8.8.8.8]"
echo ""
echo "Then run: sudo netplan apply"
exit 0
elif [ -f /etc/network/interfaces ]; then
echo "✅ Using /etc/network/interfaces"
echo "⚠️ Manual edit required"
echo "Please edit /etc/network/interfaces and change:"
echo " address $NEW_IP"
echo " netmask $NEW_NETMASK"
echo " gateway $NEW_GATEWAY"
echo ""
echo "Then run: sudo systemctl restart networking"
exit 0
else
echo "❌ Could not detect network manager"
echo "Please configure manually:"
echo " IP: $NEW_IP/24"
echo " Gateway: $NEW_GATEWAY"
echo " DNS: $NEW_DNS"
exit 1
fi
# Wait a moment for network to stabilize
sleep 3
# Verify new IP
echo ""
echo "🔍 Verifying new IP configuration..."
NEW_IP_CHECK=$(ip addr show $INTERFACE | grep -oP 'inet \K[\d.]+' | head -1)
if [ "$NEW_IP_CHECK" = "$NEW_IP" ]; then
echo "✅ IP address successfully changed to $NEW_IP"
else
echo "⚠️ IP address check: $NEW_IP_CHECK (expected $NEW_IP)"
fi
# Test gateway
echo ""
echo "🧪 Testing gateway connectivity..."
if ping -c 1 -W 2 $NEW_GATEWAY >/dev/null 2>&1; then
echo "✅ Gateway $NEW_GATEWAY is reachable"
else
echo "⚠️ Gateway $NEW_GATEWAY is not reachable"
fi
# Test ml110
echo ""
echo "🧪 Testing ml110 connectivity..."
if ping -c 1 -W 2 ${PROXMOX_HOST_ML110:-192.168.11.10} >/dev/null 2>&1; then
echo "✅ ml110 (${PROXMOX_HOST_ML110:-192.168.11.10}) is reachable!"
else
echo "⚠️ ml110 (${PROXMOX_HOST_ML110:-192.168.11.10}) is not reachable (may need firewall config)"
fi
echo ""
echo "✅ IP change complete!"
echo ""
echo "📋 Current configuration:"
ip addr show $INTERFACE | grep "inet "
ip route show | grep default

View File

@@ -0,0 +1,139 @@
#!/bin/bash
set -euo pipefail
# Change dev machine IP to 192.168.11.4 for access to ml110
# Usage: sudo ./scripts/unifi/change-ip-to-vlan11.sh
set -e
INTERFACE="eth0"
NEW_IP="192.168.11.4"
NEW_GATEWAY="192.168.11.1"
NEW_NETMASK="255.255.255.0"
NEW_DNS="192.168.11.1"
echo "🔧 Changing IP address to $NEW_IP for interface $INTERFACE"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ Please run as root (use sudo)"
exit 1
fi
# Detect network manager
if command -v nmcli >/dev/null 2>&1; then
echo "✅ Using NetworkManager (nmcli)"
# Get active connection name
CONN_NAME=$(nmcli -t -f NAME connection show --active | grep -i "$INTERFACE" | head -1)
if [ -z "$CONN_NAME" ]; then
CONN_NAME=$(nmcli -t -f NAME,DEVICE connection show | grep "$INTERFACE" | cut -d: -f1 | head -1)
fi
if [ -z "$CONN_NAME" ]; then
echo "❌ Could not find connection for $INTERFACE"
echo "Available connections:"
nmcli connection show
exit 1
fi
echo "📋 Found connection: $CONN_NAME"
echo ""
echo "🔄 Changing IP configuration..."
# Change IP settings
nmcli connection modify "$CONN_NAME" ipv4.addresses "$NEW_IP/24"
nmcli connection modify "$CONN_NAME" ipv4.gateway "$NEW_GATEWAY"
nmcli connection modify "$CONN_NAME" ipv4.dns "$NEW_DNS 8.8.8.8"
nmcli connection modify "$CONN_NAME" ipv4.method manual
echo "✅ Configuration updated"
echo "🔄 Restarting connection..."
nmcli connection down "$CONN_NAME"
sleep 2
nmcli connection up "$CONN_NAME"
echo "✅ Connection restarted"
elif [ -d /etc/netplan ]; then
echo "✅ Using netplan"
# Find netplan config file
NETPLAN_FILE=$(ls /etc/netplan/*.yaml 2>/dev/null | head -1)
if [ -z "$NETPLAN_FILE" ]; then
echo "❌ No netplan config file found"
exit 1
fi
echo "📋 Found netplan config: $NETPLAN_FILE"
echo ""
echo "⚠️ Manual edit required for netplan"
echo "Please edit $NETPLAN_FILE and change:"
echo " addresses: [$NEW_IP/24]"
echo " gateway4: $NEW_GATEWAY"
echo " nameservers:"
echo " addresses: [$NEW_DNS, 8.8.8.8]"
echo ""
echo "Then run: sudo netplan apply"
exit 0
elif [ -f /etc/network/interfaces ]; then
echo "✅ Using /etc/network/interfaces"
echo "⚠️ Manual edit required"
echo "Please edit /etc/network/interfaces and change:"
echo " address $NEW_IP"
echo " netmask $NEW_NETMASK"
echo " gateway $NEW_GATEWAY"
echo ""
echo "Then run: sudo systemctl restart networking"
exit 0
else
echo "❌ Could not detect network manager"
echo "Please configure manually:"
echo " IP: $NEW_IP/24"
echo " Gateway: $NEW_GATEWAY"
echo " DNS: $NEW_DNS"
exit 1
fi
# Wait a moment for network to stabilize
sleep 3
# Verify new IP
echo ""
echo "🔍 Verifying new IP configuration..."
NEW_IP_CHECK=$(ip addr show $INTERFACE | grep -oP 'inet \K[\d.]+' | head -1)
if [ "$NEW_IP_CHECK" = "$NEW_IP" ]; then
echo "✅ IP address successfully changed to $NEW_IP"
else
echo "⚠️ IP address check: $NEW_IP_CHECK (expected $NEW_IP)"
fi
# Test gateway
echo ""
echo "🧪 Testing gateway connectivity..."
if ping -c 1 -W 2 $NEW_GATEWAY >/dev/null 2>&1; then
echo "✅ Gateway $NEW_GATEWAY is reachable"
else
echo "⚠️ Gateway $NEW_GATEWAY is not reachable"
fi
# Test ml110
echo ""
echo "🧪 Testing ml110 connectivity..."
if ping -c 1 -W 2 192.168.11.10 >/dev/null 2>&1; then
echo "✅ ml110 (192.168.11.10) is reachable!"
else
echo "⚠️ ml110 (192.168.11.10) is not reachable (may need firewall config)"
fi
echo ""
echo "✅ IP change complete!"
echo ""
echo "📋 Current configuration:"
ip addr show $INTERFACE | grep "inet "
ip route show | grep default

View File

@@ -0,0 +1,369 @@
#!/usr/bin/env node
/**
* Comprehensive Page Mapper
*
* This script fully maps the UDM Pro routing page to understand:
* - All UI elements and their relationships
* - Page state and how it changes
* - Where buttons appear based on context
* - How scrolling and interactions affect element visibility
*/
import { chromium } from 'playwright';
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
// Load environment variables
const envPath = join(homedir(), '.env');
function loadEnvFile(filePath) {
try {
const envFile = readFileSync(filePath, 'utf8');
const envVars = envFile.split('\n').filter(
(line) => line.includes('=') && !line.trim().startsWith('#')
);
for (const line of envVars) {
const [key, ...values] = line.split('=');
if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) {
let value = values.join('=').trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key.trim()] = value;
}
}
return true;
} catch {
return false;
}
}
loadEnvFile(envPath);
const UDM_URL = process.env.UNIFI_UDM_URL || 'https://192.168.0.1';
const USERNAME = process.env.UNIFI_BROWSER_USERNAME || process.env.UNIFI_USERNAME || 'unifi_api';
const PASSWORD = process.env.UNIFI_BROWSER_PASSWORD || process.env.UNIFI_PASSWORD;
console.log('🗺️ Comprehensive UDM Pro Page Mapper');
console.log('=====================================\n');
if (!PASSWORD) {
console.error('❌ UNIFI_PASSWORD must be set in ~/.env');
process.exit(1);
}
(async () => {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
try {
console.log('1. Logging in...');
await page.goto(UDM_URL, { waitUntil: 'networkidle' });
await page.waitForSelector('input[type="text"]');
await page.fill('input[type="text"]', USERNAME);
await page.fill('input[type="password"]', PASSWORD);
await page.click('button[type="submit"]');
await page.waitForTimeout(5000);
console.log('2. Navigating to Routing page...');
// First ensure we're logged in and on dashboard
const currentUrl = page.url();
if (currentUrl.includes('/login')) {
console.log(' Still on login, waiting for redirect...');
await page.waitForURL('**/network/**', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(3000);
}
// Navigate to routing page
console.log(' Navigating to routing settings...');
await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// Wait for URL to be correct (handle redirects)
try {
await page.waitForURL('**/settings/routing**', { timeout: 20000 });
console.log(' ✅ On routing page');
} catch (error) {
console.log(` ⚠️ URL check failed, current: ${page.url()}`);
// Try navigating again
await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'networkidle' });
await page.waitForTimeout(5000);
}
// Verify we're on the right page
const pageText = await page.textContent('body').catch(() => '');
if (!pageText.includes('Route') && !pageText.includes('routing') && !pageText.includes('Static')) {
console.log(' ⚠️ Page may not be fully loaded, waiting more...');
await page.waitForTimeout(10000);
}
console.log(` Current URL: ${page.url()}`);
// Wait for routes API
try {
await page.waitForResponse(response =>
response.url().includes('/rest/routing') || response.url().includes('/trafficroutes'),
{ timeout: 15000 }
);
console.log(' Routes API loaded');
} catch (error) {
console.log(' Routes API not detected');
}
await page.waitForTimeout(5000);
console.log('3. Comprehensive page mapping...\n');
// Take full page screenshot
await page.screenshot({ path: 'mapper-full-page.png', fullPage: true });
console.log(' Screenshot saved: mapper-full-page.png');
// Map page at different scroll positions
const scrollPositions = [0, 500, 1000, 1500, 2000];
const pageMaps = [];
for (const scrollY of scrollPositions) {
await page.evaluate((y) => window.scrollTo(0, y), scrollY);
await page.waitForTimeout(2000);
const map = await page.evaluate(() => {
const result = {
scrollY: window.scrollY,
viewport: { width: window.innerWidth, height: window.innerHeight },
elements: {
buttons: [],
links: [],
inputs: [],
tables: [],
sections: [],
text: [],
},
};
// Get all buttons with full context
const buttons = Array.from(document.querySelectorAll('button, [role="button"]'));
buttons.forEach((btn, index) => {
const rect = btn.getBoundingClientRect();
const styles = window.getComputedStyle(btn);
if (rect.width > 0 && rect.height > 0 && styles.display !== 'none') {
// Check if in viewport
const inViewport = rect.top >= 0 && rect.top < window.innerHeight &&
rect.left >= 0 && rect.left < window.innerWidth;
if (inViewport) {
// Get full parent hierarchy
let parent = btn;
const hierarchy = [];
for (let i = 0; i < 5; i++) {
parent = parent.parentElement;
if (!parent || parent === document.body) break;
const parentText = parent.textContent?.trim().substring(0, 100) || '';
const parentClass = parent.className || '';
hierarchy.push({
tag: parent.tagName,
class: parentClass.substring(0, 80),
text: parentText,
hasRoute: parentText.includes('Route') || parentText.includes('Static'),
hasTable: parent.tagName === 'TABLE' || parent.querySelector('table'),
});
}
result.elements.buttons.push({
index,
text: btn.textContent?.trim() || '',
className: btn.className || '',
id: btn.id || '',
ariaLabel: btn.getAttribute('aria-label') || '',
dataTestId: btn.getAttribute('data-testid') || '',
position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
iconOnly: !btn.textContent?.trim() && btn.querySelector('svg'),
enabled: !btn.disabled,
visible: styles.visibility !== 'hidden',
hierarchy: hierarchy.slice(0, 3), // Top 3 parents
});
}
}
});
// Get all tables
const tables = Array.from(document.querySelectorAll('table'));
tables.forEach((table, index) => {
const rect = table.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
const inViewport = rect.top >= 0 && rect.top < window.innerHeight;
if (inViewport) {
const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent?.trim() || '');
const rows = Array.from(table.querySelectorAll('tbody tr, tr')).length;
const tableText = table.textContent || '';
result.elements.tables.push({
index,
headers,
rowCount: rows,
hasRouteText: tableText.includes('Route') || tableText.includes('Static'),
position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
buttonsInTable: Array.from(table.querySelectorAll('button')).length,
});
}
}
});
// Get all text content that might indicate sections
const routeTexts = [];
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
let node;
while (node = walker.nextNode()) {
const text = node.textContent?.trim() || '';
if (text && (text.includes('Static Routes') || text.includes('Route') || text.includes('Add'))) {
const parent = node.parentElement;
if (parent) {
routeTexts.push({
text: text.substring(0, 100),
tag: parent.tagName,
className: parent.className?.substring(0, 80) || '',
position: parent.getBoundingClientRect(),
});
}
}
}
result.elements.text = routeTexts.slice(0, 20);
return result;
});
pageMaps.push(map);
}
// Analyze results
console.log('📊 Page Mapping Results:');
console.log('='.repeat(80));
// Combine all buttons found at different scroll positions
const allButtons = new Map();
pageMaps.forEach((map, scrollIndex) => {
map.elements.buttons.forEach(btn => {
const key = btn.id || btn.className || `${btn.position.x}-${btn.position.y}`;
if (!allButtons.has(key)) {
allButtons.set(key, { ...btn, foundAtScroll: [scrollIndex] });
} else {
allButtons.get(key).foundAtScroll.push(scrollIndex);
}
});
});
console.log(`\n🔘 Unique Buttons Found: ${allButtons.size}`);
Array.from(allButtons.values()).forEach((btn, i) => {
console.log(`\n${i + 1}. Button:`);
console.log(` Text: "${btn.text}"`);
console.log(` Class: ${btn.className.substring(0, 80)}`);
console.log(` ID: ${btn.id || 'none'}`);
console.log(` Position: (${btn.position.x}, ${btn.position.y})`);
console.log(` Icon Only: ${btn.iconOnly}`);
console.log(` Enabled: ${btn.enabled}`);
console.log(` Found at scroll positions: ${btn.foundAtScroll.join(', ')}`);
if (btn.hierarchy.length > 0) {
console.log(` Parent Context:`);
btn.hierarchy.forEach((parent, j) => {
console.log(` ${j + 1}. <${parent.tag}> ${parent.class} - "${parent.text.substring(0, 50)}"`);
console.log(` Has Route: ${parent.hasRoute}, Has Table: ${parent.hasTable}`);
});
}
});
// Find tables
const allTables = [];
pageMaps.forEach(map => {
map.elements.tables.forEach(table => {
if (!allTables.find(t => t.index === table.index)) {
allTables.push(table);
}
});
});
console.log(`\n📋 Tables Found: ${allTables.length}`);
allTables.forEach((table, i) => {
console.log(`\n${i + 1}. Table ${table.index}:`);
console.log(` Headers: ${table.headers.join(', ')}`);
console.log(` Rows: ${table.rowCount}`);
console.log(` Has Route Text: ${table.hasRouteText}`);
console.log(` Buttons in Table: ${table.buttonsInTable}`);
console.log(` Position: (${table.position.x}, ${table.position.y})`);
});
// Find route-related text
const routeTexts = [];
pageMaps.forEach(map => {
map.elements.text.forEach(text => {
if (!routeTexts.find(t => t.text === text.text)) {
routeTexts.push(text);
}
});
});
console.log(`\n📝 Route-Related Text Found: ${routeTexts.length}`);
routeTexts.forEach((text, i) => {
console.log(`\n${i + 1}. "${text.text}"`);
console.log(` Tag: ${text.tag}, Class: ${text.className}`);
console.log(` Position: (${text.position.x}, ${text.position.y})`);
});
// Save full map to file
const mapData = {
url: page.url(),
timestamp: new Date().toISOString(),
scrollMaps: pageMaps,
allButtons: Array.from(allButtons.values()),
allTables,
routeTexts,
};
writeFileSync('page-map.json', JSON.stringify(mapData, null, 2));
console.log('\n💾 Full page map saved to: page-map.json');
// Identify most likely Add button
console.log(`\n🎯 Most Likely Add Button Candidates:`);
console.log('='.repeat(80));
const candidates = Array.from(allButtons.values())
.filter(btn => btn.iconOnly || btn.text.toLowerCase().includes('add') || btn.text.toLowerCase().includes('create'))
.sort((a, b) => {
// Prioritize buttons with route context
const aHasRoute = a.hierarchy.some(p => p.hasRoute);
const bHasRoute = b.hierarchy.some(p => p.hasRoute);
if (aHasRoute && !bHasRoute) return -1;
if (!aHasRoute && bHasRoute) return 1;
// Then prioritize buttons near tables
const aNearTable = a.hierarchy.some(p => p.hasTable);
const bNearTable = b.hierarchy.some(p => p.hasTable);
if (aNearTable && !bNearTable) return -1;
if (!aNearTable && bNearTable) return 1;
return 0;
});
candidates.slice(0, 5).forEach((btn, i) => {
console.log(`\n${i + 1}. "${btn.text}" - ${btn.className.substring(0, 60)}`);
console.log(` ID: ${btn.id || 'none'}`);
console.log(` Has Route Context: ${btn.hierarchy.some(p => p.hasRoute)}`);
console.log(` Near Table: ${btn.hierarchy.some(p => p.hasTable)}`);
console.log(` Selector: ${btn.id ? `#${btn.id}` : `.${btn.className.split(' ')[0]}`}`);
});
console.log('\n\n⏸ Page is open in browser. Inspect manually if needed.');
console.log('Press Ctrl+C to close...\n');
await page.waitForTimeout(60000);
} catch (error) {
console.error('❌ Error:', error.message);
await page.screenshot({ path: 'mapper-error.png', fullPage: true });
} finally {
await browser.close();
}
})();

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env node
/**
* Configure Inter-VLAN Firewall Rules via API
* Creates firewall rules for inter-VLAN communication
*/
import https from 'https';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
// Load environment variables
const envFile = join(homedir(), '.env');
let env = {};
if (existsSync(envFile)) {
readFileSync(envFile, 'utf8').split('\n').forEach(line => {
const match = line.match(/^([^=]+)=(.*)$/);
if (match) {
const key = match[1].trim();
const value = match[2].trim().replace(/^['"]|['"]$/g, '');
env[key] = value;
}
});
}
const UDM_PRO_URL = env.UNIFI_UDM_URL || 'https://192.168.0.1';
const API_KEY = env.UNIFI_API_KEY || '';
const SITE_ID = env.UNIFI_SITE_ID || 'default';
const log = (message) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${message}`);
};
// Network IDs (will be fetched)
const NETWORKS = {
'MGMT-LAN': { vlanId: 11, subnet: '192.168.11.0/24' },
'BESU-VAL': { vlanId: 110, subnet: '10.110.0.0/24' },
'BESU-SEN': { vlanId: 111, subnet: '10.111.0.0/24' },
'BESU-RPC': { vlanId: 112, subnet: '10.112.0.0/24' },
'BLOCKSCOUT': { vlanId: 120, subnet: '10.120.0.0/24' },
'CACTI': { vlanId: 121, subnet: '10.121.0.0/24' },
'CCIP-OPS': { vlanId: 130, subnet: '10.130.0.0/24' },
'CCIP-COMMIT': { vlanId: 132, subnet: '10.132.0.0/24' },
'CCIP-EXEC': { vlanId: 133, subnet: '10.133.0.0/24' },
'CCIP-RMN': { vlanId: 134, subnet: '10.134.0.0/24' },
'FABRIC': { vlanId: 140, subnet: '10.140.0.0/24' },
'FIREFLY': { vlanId: 141, subnet: '10.141.0.0/24' },
'INDY': { vlanId: 150, subnet: '10.150.0.0/24' },
'SANKOFA-SVC': { vlanId: 160, subnet: '10.160.0.0/22' },
'PHX-SOV-SMOM': { vlanId: 200, subnet: '10.200.0.0/20' },
'PHX-SOV-ICCC': { vlanId: 201, subnet: '10.201.0.0/20' },
'PHX-SOV-DBIS': { vlanId: 202, subnet: '10.202.0.0/24' },
'PHX-SOV-AR': { vlanId: 203, subnet: '10.203.0.0/20' },
};
function makeRequest(path, method = 'GET', data = null) {
return new Promise((resolve, reject) => {
const url = new URL(path, UDM_PRO_URL);
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname + url.search,
method: method,
headers: {
'X-API-KEY': API_KEY,
'Content-Type': 'application/json',
},
rejectUnauthorized: false,
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => {
try {
const json = JSON.parse(body);
resolve(json);
} catch (e) {
resolve({ data: body, status: res.statusCode });
}
});
});
req.on('error', reject);
if (data) {
req.write(JSON.stringify(data));
}
req.end();
});
}
async function getNetworks() {
log('📋 Fetching network list...');
try {
const response = await makeRequest(`/proxy/network/integration/v1/sites/${SITE_ID}/networks`);
return response.data || [];
} catch (error) {
log(`❌ Error fetching networks: ${error.message}`);
return [];
}
}
async function createFirewallRule(rule) {
log(`🔧 Creating firewall rule: ${rule.name}...`);
try {
const response = await makeRequest(
`/proxy/network/integration/v1/sites/${SITE_ID}/firewall/rules`,
'POST',
rule
);
if (response.meta && response.meta.rc === 'ok') {
log(` ✅ Rule created successfully`);
return true;
} else {
log(` ⚠️ Response: ${JSON.stringify(response)}`);
return false;
}
} catch (error) {
log(` ❌ Error: ${error.message}`);
return false;
}
}
async function main() {
log('🚀 Starting Inter-VLAN Firewall Rules Configuration');
log(`UDM Pro URL: ${UDM_PRO_URL}`);
log(`Site ID: ${SITE_ID}`);
log('');
if (!API_KEY) {
log('❌ UNIFI_API_KEY not set. Please set it in ~/.env');
log('💡 Note: Firewall rules can also be configured via UDM Pro web UI');
process.exit(1);
}
// Get networks to find network IDs
const networks = await getNetworks();
log(`✅ Found ${networks.length} networks`);
log('');
// Build network ID map
const networkIdMap = {};
networks.forEach(net => {
if (net.name) {
networkIdMap[net.name] = net._id;
}
});
log('📋 Firewall Rules to Create:');
log('');
log('1. Management VLAN (11) → Service VLANs');
log(' Allow: SSH (22), HTTPS (443), Database (5432, 3306), Monitoring (161, 9090)');
log('');
log('2. Service VLANs → Management VLAN (11)');
log(' Allow: Monitoring, Logging');
log('');
log('3. Sovereign Tenant Isolation');
log(' Block: Inter-tenant communication');
log('');
log('⚠️ Note: Firewall rule creation via API may have limitations.');
log('💡 For complete control, configure rules via UDM Pro web UI:');
log(' Settings → Firewall & Security → Firewall Rules');
log('');
log('✅ Firewall rules configuration guide complete!');
log('');
log('📋 Manual Configuration Steps:');
log(' 1. Access UDM Pro: https://192.168.0.1');
log(' 2. Navigate: Settings → Firewall & Security → Firewall Rules');
log(' 3. Create rules as described in:');
log(' docs/04-configuration/UDM_PRO_VLAN_UTILIZATION_COMPLETE_GUIDE.md');
log('');
}
main().catch(console.error);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,189 @@
#!/usr/bin/env node
/**
* Configure all VLANs on UDM Pro via Private API
* Node.js version for better password handling
*/
import { readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { UnifiClient, ApiMode } from '../../unifi-api/dist/index.js';
import { NetworksService } from '../../unifi-api/dist/index.js';
// Load environment variables
const envPath = join(homedir(), '.env');
function loadEnvFile(filePath) {
try {
const envFile = readFileSync(filePath, 'utf8');
const envVars = envFile.split('\n').filter(
(line) => line.includes('=') && !line.trim().startsWith('#')
);
for (const line of envVars) {
const [key, ...values] = line.split('=');
if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) {
let value = values.join('=').trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key.trim()] = value;
}
}
return true;
} catch {
return false;
}
}
loadEnvFile(envPath);
const baseUrl = process.env.UNIFI_UDM_URL || 'https://192.168.0.1';
const username = process.env.UNIFI_USERNAME || 'unifi_api';
const password = process.env.UNIFI_PASSWORD;
const siteId = process.env.UNIFI_SITE_ID || 'default';
if (!password) {
console.error('❌ UNIFI_PASSWORD not set in environment');
process.exit(1);
}
console.log('UDM Pro VLAN Configuration Script');
console.log('==================================');
console.log('');
console.log(`UDM URL: ${baseUrl}`);
console.log(`Site ID: ${siteId}`);
console.log(`Username: ${username}`);
console.log('');
// Create client
const client = new UnifiClient({
baseUrl,
apiMode: ApiMode.PRIVATE,
username,
password,
siteId,
verifySSL: false,
});
const networksService = new NetworksService(client);
// VLAN configurations
const vlanConfigs = [
{ id: 11, name: 'MGMT-LAN', subnet: '192.168.11.0/24', gateway: '192.168.11.1', dhcpStart: '192.168.11.100', dhcpStop: '192.168.11.200' },
{ id: 110, name: 'BESU-VAL', subnet: '10.110.0.0/24', gateway: '10.110.0.1', dhcpStart: '10.110.0.10', dhcpStop: '10.110.0.250' },
{ id: 111, name: 'BESU-SEN', subnet: '10.111.0.0/24', gateway: '10.111.0.1', dhcpStart: '10.111.0.10', dhcpStop: '10.111.0.250' },
{ id: 112, name: 'BESU-RPC', subnet: '10.112.0.0/24', gateway: '10.112.0.1', dhcpStart: '10.112.0.10', dhcpStop: '10.112.0.250' },
{ id: 120, name: 'BLOCKSCOUT', subnet: '10.120.0.0/24', gateway: '10.120.0.1', dhcpStart: '10.120.0.10', dhcpStop: '10.120.0.250' },
{ id: 121, name: 'CACTI', subnet: '10.121.0.0/24', gateway: '10.121.0.1', dhcpStart: '10.121.0.10', dhcpStop: '10.121.0.250' },
{ id: 130, name: 'CCIP-OPS', subnet: '10.130.0.0/24', gateway: '10.130.0.1', dhcpStart: '10.130.0.10', dhcpStop: '10.130.0.250' },
{ id: 132, name: 'CCIP-COMMIT', subnet: '10.132.0.0/24', gateway: '10.132.0.1', dhcpStart: '10.132.0.10', dhcpStop: '10.132.0.250' },
{ id: 133, name: 'CCIP-EXEC', subnet: '10.133.0.0/24', gateway: '10.133.0.1', dhcpStart: '10.133.0.10', dhcpStop: '10.133.0.250' },
{ id: 134, name: 'CCIP-RMN', subnet: '10.134.0.0/24', gateway: '10.134.0.1', dhcpStart: '10.134.0.10', dhcpStop: '10.134.0.250' },
{ id: 140, name: 'FABRIC', subnet: '10.140.0.0/24', gateway: '10.140.0.1', dhcpStart: '10.140.0.10', dhcpStop: '10.140.0.250' },
{ id: 141, name: 'FIREFLY', subnet: '10.141.0.0/24', gateway: '10.141.0.1', dhcpStart: '10.141.0.10', dhcpStop: '10.141.0.250' },
{ id: 150, name: 'INDY', subnet: '10.150.0.0/24', gateway: '10.150.0.1', dhcpStart: '10.150.0.10', dhcpStop: '10.150.0.250' },
{ id: 160, name: 'SANKOFA-SVC', subnet: '10.160.0.0/22', gateway: '10.160.0.1', dhcpStart: '10.160.0.10', dhcpStop: '10.160.0.254' },
{ id: 200, name: 'PHX-SOV-SMOM', subnet: '10.200.0.0/20', gateway: '10.200.0.1', dhcpStart: '10.200.0.10', dhcpStop: '10.200.15.250' },
{ id: 201, name: 'PHX-SOV-ICCC', subnet: '10.201.0.0/20', gateway: '10.201.0.1', dhcpStart: '10.201.0.10', dhcpStop: '10.201.15.250' },
{ id: 202, name: 'PHX-SOV-DBIS', subnet: '10.202.0.0/20', gateway: '10.202.0.1', dhcpStart: '10.202.0.10', dhcpStop: '10.202.15.250' },
{ id: 203, name: 'PHX-SOV-AR', subnet: '10.203.0.0/20', gateway: '10.203.0.1', dhcpStart: '10.203.0.10', dhcpStop: '10.203.15.250' },
];
// Helper function to sleep
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function createVlan(config, networksService) {
try {
console.log(`Creating VLAN ${config.id}: ${config.name} (${config.subnet})...`);
const networkConfig = {
name: config.name,
purpose: 'corporate',
vlan: config.id,
ip_subnet: config.subnet,
ipv6_interface_type: 'none',
dhcpd_enabled: true,
dhcpd_start: config.dhcpStart,
dhcpd_stop: config.dhcpStop,
dhcpd_leasetime: 86400,
domain_name: 'localdomain',
is_nat: true,
networkgroup: 'LAN',
};
await networksService.createNetwork(networkConfig);
console.log(` ✅ VLAN ${config.id} created successfully`);
return true;
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
if (errorMsg.includes('already exists') || errorMsg.includes('duplicate')) {
console.log(` ⚠️ VLAN ${config.id} already exists (skipping)`);
return true;
} else {
console.log(` ❌ Failed to create VLAN ${config.id}: ${errorMsg}`);
return false;
}
}
}
async function main() {
console.log('Authenticating...');
// Force authentication by making a test request
try {
await networksService.listNetworks();
console.log('✅ Authentication successful');
} catch (error) {
console.error('❌ Authentication failed:', error);
process.exit(1);
}
console.log('');
console.log('Creating VLANs...');
console.log('');
// Create VLANs sequentially to avoid rate limiting
const results = [];
for (const config of vlanConfigs) {
const result = await createVlan(config, networksService);
results.push({ status: result ? 'fulfilled' : 'rejected', value: result });
// Small delay between requests to avoid rate limiting
if (config !== vlanConfigs[vlanConfigs.length - 1]) {
await sleep(500);
}
}
console.log('');
console.log('Verifying created networks...');
try {
const networks = await networksService.listNetworks();
const vlanNetworks = networks.filter(n => n.vlan && n.vlan > 0);
console.log('');
console.log(`✅ Found ${vlanNetworks.length} VLAN networks:`);
vlanNetworks.forEach(network => {
console.log(` VLAN ${network.vlan}: ${network.name} (${network.ip_subnet || 'N/A'})`);
});
} catch (error) {
console.error('Error listing networks:', error);
}
const successCount = results.filter(r => r.status === 'fulfilled' && r.value).length;
const failCount = results.length - successCount;
console.log('');
console.log('==================================');
console.log(`✅ Successfully created: ${successCount}/${vlanConfigs.length} VLANs`);
if (failCount > 0) {
console.log(`❌ Failed: ${failCount} VLANs`);
}
console.log('');
}
main().catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});

View File

@@ -0,0 +1,203 @@
#!/usr/bin/env node
/**
* Create firewall rules via UniFi Network API
* Node.js version for better JSON handling
*/
import { readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
// Load environment variables
const envPath = join(homedir(), '.env');
function loadEnvFile(filePath) {
try {
const envFile = readFileSync(filePath, 'utf8');
const envVars = envFile.split('\n').filter(
(line) => line.includes('=') && !line.trim().startsWith('#')
);
for (const line of envVars) {
const [key, ...values] = line.split('=');
if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) {
let value = values.join('=').trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key.trim()] = value;
}
}
return true;
} catch {
return false;
}
}
loadEnvFile(envPath);
const baseUrl = process.env.UNIFI_UDM_URL || 'https://192.168.0.1';
const apiKey = process.env.UNIFI_API_KEY;
const siteId = '88f7af54-98f8-306a-a1c7-c9349722b1f6';
if (!apiKey) {
console.error('❌ UNIFI_API_KEY not set in environment');
process.exit(1);
}
console.log('Creating Firewall Rules via API');
console.log('==================================');
console.log('');
console.log(`UDM URL: ${baseUrl}`);
console.log(`Site ID: ${siteId}`);
console.log('');
// Fetch networks
async function fetchNetworks() {
const response = await fetch(`${baseUrl}/proxy/network/integration/v1/sites/${siteId}/networks`, {
headers: {
'X-API-KEY': apiKey,
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch networks: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const networks = data.data || [];
// Create VLAN to network ID mapping
const vlanMap = {};
for (const net of networks) {
const vlanId = net.vlanId;
if (vlanId && vlanId > 1) {
vlanMap[vlanId] = {
id: net.id,
name: net.name,
};
}
}
return vlanMap;
}
// Create ACL rule
async function createACLRule(ruleConfig) {
const { name, description, action, index, sourceNetworks, destNetworks, protocolFilter } = ruleConfig;
const rule = {
type: 'IPV4',
enabled: true,
name,
description,
action,
index,
sourceFilter: sourceNetworks && sourceNetworks.length > 0
? { type: 'NETWORKS', networkIds: sourceNetworks }
: null,
destinationFilter: destNetworks && destNetworks.length > 0
? { type: 'NETWORKS', networkIds: destNetworks }
: null,
protocolFilter: protocolFilter || null,
enforcingDeviceFilter: null,
};
const response = await fetch(`${baseUrl}/proxy/network/integration/v1/sites/${siteId}/acl-rules`, {
method: 'POST',
headers: {
'X-API-KEY': apiKey,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(rule),
});
const responseText = await response.text();
let responseData;
try {
responseData = JSON.parse(responseText);
} catch {
responseData = responseText;
}
if (response.ok) {
return { success: true, data: responseData };
} else {
return { success: false, error: responseData, status: response.status };
}
}
// Main function
async function main() {
try {
console.log('Fetching network IDs...');
const vlanMap = await fetchNetworks();
// Key VLANs we need
const keyVlans = [11, 110, 111, 112, 120, 121, 130, 132, 133, 134, 140, 141, 150, 160, 200, 201, 202, 203];
console.log('Network ID Mapping:');
console.log('='.repeat(80));
for (const vlan of keyVlans.sort((a, b) => a - b)) {
if (vlanMap[vlan]) {
console.log(`VLAN ${String(vlan).padStart(3)} (${vlanMap[vlan].name.padEnd(15)}): ${vlanMap[vlan].id}`);
}
}
console.log('');
// Create firewall rules
console.log('Creating firewall rules...');
console.log('');
const rules = [];
// Rule 1: Block sovereign tenant inter-VLAN traffic (VLANs 200-203)
const sovereignVlans = [200, 201, 202, 203].map(v => vlanMap[v]?.id).filter(Boolean);
if (sovereignVlans.length === 4) {
rules.push({
name: 'Block Sovereign Tenant East-West Traffic',
description: 'Deny traffic between sovereign tenant VLANs (200-203) for isolation',
action: 'BLOCK',
index: 100,
sourceNetworks: sovereignVlans,
destNetworks: sovereignVlans,
protocolFilter: null,
});
}
// Create rules
for (const ruleConfig of rules) {
console.log(`Creating rule: ${ruleConfig.name}`);
const result = await createACLRule(ruleConfig);
if (result.success) {
console.log(' ✅ Rule created successfully');
} else {
console.log(` ❌ Failed to create rule (HTTP ${result.status})`);
console.log(` Error: ${JSON.stringify(result.error, null, 2)}`);
}
console.log('');
// Small delay between requests
await new Promise(resolve => setTimeout(resolve, 500));
}
console.log('✅ Firewall rule creation complete!');
console.log('');
console.log('Note: Additional rules can be added for:');
console.log(' - Management VLAN access (VLAN 11 → Service VLANs)');
console.log(' - Monitoring access (Service VLANs → VLAN 11)');
console.log(' - Other inter-VLAN rules as needed');
} catch (error) {
console.error('❌ Error:', error.message);
if (error.stack) {
console.error(error.stack);
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,218 @@
#!/bin/bash
set -euo pipefail
# Create firewall rules via UniFi Network API
# This script creates ACL rules for network segmentation and security
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$PROJECT_ROOT"
# Load UNIFI_* from repo .env, unifi-api/.env, or ~/.env
if [ -f "$PROJECT_ROOT/.env" ]; then
set -a && source "$PROJECT_ROOT/.env" 2>/dev/null && set +a
fi
if [ -f "$PROJECT_ROOT/unifi-api/.env" ]; then
set -a && source "$PROJECT_ROOT/unifi-api/.env" 2>/dev/null && set +a
fi
if [ -f ~/.env ]; then
source <(grep "^UNIFI_" ~/.env 2>/dev/null | sed 's/^/export /') 2>/dev/null || true
fi
UDM_URL="${UNIFI_UDM_URL:-https://192.168.0.1}"
API_KEY="${UNIFI_API_KEY}"
SITE_ID="88f7af54-98f8-306a-a1c7-c9349722b1f6"
if [ -z "$API_KEY" ]; then
echo "❌ UNIFI_API_KEY not set in environment"
exit 1
fi
echo "Creating Firewall Rules via API"
echo "=================================="
echo ""
echo "UDM URL: $UDM_URL"
echo "Site ID: $SITE_ID"
echo ""
# Get network IDs
echo "Fetching network IDs..."
NETWORKS_JSON=$(curl -k -s -X GET "$UDM_URL/proxy/network/integration/v1/sites/$SITE_ID/networks" \
-H "X-API-KEY: $API_KEY" \
-H 'Accept: application/json')
# Extract network IDs using Python
NETWORK_IDS=$(python3 << 'PYEOF'
import sys, json
data = json.load(sys.stdin)
networks = data.get('data', [])
vlan_map = {}
for net in networks:
vlan_id = net.get('vlanId')
if vlan_id and vlan_id > 1:
vlan_map[vlan_id] = net.get('id')
# Key VLANs we need
key_vlans = {
11: 'MGMT-LAN',
110: 'BESU-VAL',
111: 'BESU-SEN',
112: 'BESU-RPC',
120: 'BLOCKSCOUT',
121: 'CACTI',
130: 'CCIP-OPS',
132: 'CCIP-COMMIT',
133: 'CCIP-EXEC',
134: 'CCIP-RMN',
140: 'FABRIC',
141: 'FIREFLY',
150: 'INDY',
160: 'SANKOFA-SVC',
200: 'PHX-SOV-SMOM',
201: 'PHX-SOV-ICCC',
202: 'PHX-SOV-DBIS',
203: 'PHX-SOV-AR'
}
# Export as shell variables
for vlan, name in key_vlans.items():
if vlan in vlan_map:
print(f"NETWORK_ID_VLAN{vlan}={vlan_map[vlan]}")
print(f"NETWORK_NAME_VLAN{vlan}={name}")
PYEOF
)
# Source the network IDs
eval "$NETWORK_IDS"
echo "✅ Network IDs loaded"
echo ""
# Function to create ACL rule
create_acl_rule() {
local name=$1
local description=$2
local action=$3
local index=$4
local source_networks=$5
local dest_networks=$6
local protocol=$7
echo "Creating rule: $name"
# Build source filter
if [ -n "$source_networks" ]; then
SOURCE_FILTER=$(python3 << PYEOF
import json
networks = "$source_networks".split()
network_ids = []
for net in networks:
var_name = f"NETWORK_ID_VLAN{net}"
import os
net_id = os.environ.get(var_name)
if net_id:
network_ids.append(net_id)
print(json.dumps({
"type": "NETWORK",
"networkIds": network_ids
}))
PYEOF
)
else
SOURCE_FILTER="null"
fi
# Build destination filter
if [ -n "$dest_networks" ]; then
DEST_FILTER=$(python3 << PYEOF
import json
networks = "$dest_networks".split()
network_ids = []
for net in networks:
var_name = f"NETWORK_ID_VLAN{net}"
import os
net_id = os.environ.get(var_name)
if net_id:
network_ids.append(net_id)
print(json.dumps({
"type": "NETWORK",
"networkIds": network_ids
}))
PYEOF
)
else
DEST_FILTER="null"
fi
# Build protocol filter
if [ -n "$protocol" ]; then
PROTOCOL_FILTER="[\"$protocol\"]"
else
PROTOCOL_FILTER="null"
fi
# Create rule JSON
RULE_JSON=$(python3 << PYEOF
import json, sys
source = json.loads('$SOURCE_FILTER') if '$SOURCE_FILTER' != 'null' else None
dest = json.loads('$DEST_FILTER') if '$DEST_FILTER' != 'null' else None
protocol = json.loads('$PROTOCOL_FILTER') if '$PROTOCOL_FILTER' != 'null' else None
rule = {
"type": "IPV4",
"enabled": True,
"name": "$name",
"description": "$description",
"action": "$action",
"index": $index,
"sourceFilter": source,
"destinationFilter": dest,
"protocolFilter": protocol,
"enforcingDeviceFilter": None
}
print(json.dumps(rule))
PYEOF
)
# Create the rule
RESPONSE=$(curl -k -s -w "\n%{http_code}" -X POST "$UDM_URL/proxy/network/integration/v1/sites/$SITE_ID/acl-rules" \
-H "X-API-KEY: $API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d "$RULE_JSON")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
RESPONSE_BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
echo " ✅ Rule created successfully"
return 0
else
echo " ❌ Failed to create rule (HTTP $HTTP_CODE)"
echo " Response: $RESPONSE_BODY"
return 1
fi
}
# Create firewall rules
echo "Creating firewall rules..."
echo ""
# Rule 1: Block sovereign tenant inter-VLAN traffic (VLANs 200-203)
create_acl_rule \
"Block Sovereign Tenant East-West Traffic" \
"Deny traffic between sovereign tenant VLANs (200-203) for isolation" \
"BLOCK" \
100 \
"200 201 202 203" \
"200 201 202 203" \
""
echo ""
echo "✅ Firewall rule creation complete!"
echo ""
echo "Note: Additional rules (management access, monitoring) can be added"
echo " after verifying the API request format works correctly."

View File

@@ -0,0 +1,236 @@
#!/usr/bin/env node
/**
* Create management VLAN and monitoring firewall rules via UniFi Network API
* These rules CAN be automated (non-overlapping source/destination)
*/
import { readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
// Load environment variables
const envPath = join(homedir(), '.env');
function loadEnvFile(filePath) {
try {
const envFile = readFileSync(filePath, 'utf8');
const envVars = envFile.split('\n').filter(
(line) => line.includes('=') && !line.trim().startsWith('#')
);
for (const line of envVars) {
const [key, ...values] = line.split('=');
if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) {
let value = values.join('=').trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key.trim()] = value;
}
}
return true;
} catch {
return false;
}
}
loadEnvFile(envPath);
const baseUrl = process.env.UNIFI_UDM_URL || 'https://192.168.0.1';
const apiKey = process.env.UNIFI_API_KEY;
const siteId = '88f7af54-98f8-306a-a1c7-c9349722b1f6';
if (!apiKey) {
console.error('❌ UNIFI_API_KEY not set in environment');
process.exit(1);
}
console.log('Creating Management VLAN Firewall Rules via API');
console.log('=================================================');
console.log('');
// Fetch networks
async function fetchNetworks() {
const response = await fetch(`${baseUrl}/proxy/network/integration/v1/sites/${siteId}/networks`, {
headers: {
'X-API-KEY': apiKey,
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch networks: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const networks = data.data || [];
const vlanMap = {};
for (const net of networks) {
const vlanId = net.vlanId;
if (vlanId && vlanId > 1) {
vlanMap[vlanId] = {
id: net.id,
name: net.name,
};
}
}
return vlanMap;
}
// Create ACL rule
async function createACLRule(ruleConfig) {
const { name, description, action, index, sourceNetworks, destNetworks, protocolFilter } = ruleConfig;
const rule = {
type: 'IPV4',
enabled: true,
name,
description,
action,
index,
sourceFilter: sourceNetworks && sourceNetworks.length > 0
? { type: 'NETWORKS', networkIds: sourceNetworks }
: null,
destinationFilter: destNetworks && destNetworks.length > 0
? { type: 'NETWORKS', networkIds: destNetworks }
: null,
protocolFilter: protocolFilter || null,
enforcingDeviceFilter: null,
};
const response = await fetch(`${baseUrl}/proxy/network/integration/v1/sites/${siteId}/acl-rules`, {
method: 'POST',
headers: {
'X-API-KEY': apiKey,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(rule),
});
const responseText = await response.text();
let responseData;
try {
responseData = JSON.parse(responseText);
} catch {
responseData = responseText;
}
if (response.ok) {
return { success: true, data: responseData };
} else {
return { success: false, error: responseData, status: response.status };
}
}
// Main function
async function main() {
try {
console.log('Fetching network IDs...');
const vlanMap = await fetchNetworks();
const mgmtVlanId = vlanMap[11]?.id;
if (!mgmtVlanId) {
console.error('❌ VLAN 11 (MGMT-LAN) not found');
process.exit(1);
}
// Service VLANs (excluding sovereign tenants for now)
const serviceVlanIds = [110, 111, 112, 120, 121, 130, 132, 133, 134, 140, 141, 150, 160]
.map(v => vlanMap[v]?.id)
.filter(Boolean);
console.log(`✅ Found ${serviceVlanIds.length} service VLANs`);
console.log('');
const rules = [];
// Rule 1: Allow Management VLAN → Service VLANs (TCP for SSH, HTTPS, etc.)
if (serviceVlanIds.length > 0) {
rules.push({
name: 'Allow Management to Service VLANs (TCP)',
description: 'Allow Management VLAN (11) to access Service VLANs via TCP (SSH, HTTPS, database admin ports)',
action: 'ALLOW',
index: 10,
sourceNetworks: [mgmtVlanId],
destNetworks: serviceVlanIds,
protocolFilter: ['TCP'],
});
}
// Rule 2: Allow Service VLANs → Management VLAN (UDP/TCP for monitoring)
if (serviceVlanIds.length > 0) {
rules.push({
name: 'Allow Monitoring to Management VLAN',
description: 'Allow Service VLANs to send monitoring/logging data to Management VLAN (SNMP, monitoring agents)',
action: 'ALLOW',
index: 20,
sourceNetworks: serviceVlanIds,
destNetworks: [mgmtVlanId],
protocolFilter: ['TCP', 'UDP'],
});
}
console.log(`Creating ${rules.length} firewall rules...`);
console.log('');
let successCount = 0;
let failCount = 0;
for (const ruleConfig of rules) {
console.log(`Creating rule: ${ruleConfig.name}`);
const result = await createACLRule(ruleConfig);
if (result.success) {
console.log(' ✅ Rule created successfully');
successCount++;
} else {
console.log(` ❌ Failed to create rule (HTTP ${result.status})`);
if (result.error && typeof result.error === 'object') {
console.log(` Error: ${result.error.message || JSON.stringify(result.error)}`);
} else {
console.log(` Error: ${result.error}`);
}
failCount++;
}
console.log('');
// Small delay between requests
await new Promise(resolve => setTimeout(resolve, 500));
}
console.log('='.repeat(50));
console.log(`✅ Successfully created: ${successCount}/${rules.length} rules`);
if (failCount > 0) {
console.log(`❌ Failed: ${failCount} rules`);
}
console.log('');
// Verify rules
console.log('Verifying created rules...');
const verifyResponse = await fetch(`${baseUrl}/proxy/network/integration/v1/sites/${siteId}/acl-rules`, {
headers: {
'X-API-KEY': apiKey,
'Accept': 'application/json',
},
});
if (verifyResponse.ok) {
const verifyData = await verifyResponse.json();
const ruleCount = verifyData.count || 0;
console.log(`✅ Found ${ruleCount} ACL rules total`);
}
} catch (error) {
console.error('❌ Error:', error.message);
if (error.stack) {
console.error(error.stack);
}
process.exit(1);
}
}
main();

34
scripts/unifi/curl-sites.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Call UniFi Network API /sites using API key from dotenv.
# Usage: ./scripts/unifi/curl-sites.sh or bash scripts/unifi/curl-sites.sh
# Loads UNIFI_UDM_URL and UNIFI_API_KEY from .env, unifi-api/.env, or ~/.env.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$PROJECT_ROOT"
# Load UNIFI_* from repo .env, unifi-api/.env, or ~/.env (later overrides)
if [ -f "$PROJECT_ROOT/.env" ]; then
set -a && source "$PROJECT_ROOT/.env" 2>/dev/null && set +a
fi
if [ -f "$PROJECT_ROOT/unifi-api/.env" ]; then
set -a && source "$PROJECT_ROOT/unifi-api/.env" 2>/dev/null && set +a
fi
if [ -f ~/.env ]; then
source <(grep -E '^UNIFI_' ~/.env 2>/dev/null | sed 's/^/export /') 2>/dev/null || true
fi
UDM_URL="${UNIFI_UDM_URL:-https://192.168.0.1}"
API_KEY="${UNIFI_API_KEY:-}"
if [ -z "$API_KEY" ]; then
echo "UNIFI_API_KEY is not set. Add it to .env, unifi-api/.env, or ~/.env (see .env.example UNIFI_* section)." >&2
exit 1
fi
curl -k -s -X GET "$UDM_URL/proxy/network/integration/v1/sites" \
-H "X-API-KEY: $API_KEY" \
-H "Accept: application/json" \
"$@"

View File

@@ -0,0 +1,348 @@
#!/usr/bin/env node
/**
* Comprehensive Add Button Finder
*
* This script uses multiple strategies to find the Add button:
* 1. Waits for page to fully load
* 2. Maps all page sections
* 3. Finds all tables and their associated buttons
* 4. Identifies buttons in table headers/toolbars
* 5. Tests each potential button to see what it does
*/
import { chromium } from 'playwright';
import { readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
// Load environment variables
const envPath = join(homedir(), '.env');
function loadEnvFile(filePath) {
try {
const envFile = readFileSync(filePath, 'utf8');
const envVars = envFile.split('\n').filter(
(line) => line.includes('=') && !line.trim().startsWith('#')
);
for (const line of envVars) {
const [key, ...values] = line.split('=');
if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) {
let value = values.join('=').trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key.trim()] = value;
}
}
return true;
} catch {
return false;
}
}
loadEnvFile(envPath);
const UDM_URL = process.env.UNIFI_UDM_URL || 'https://192.168.0.1';
const USERNAME = process.env.UNIFI_BROWSER_USERNAME || process.env.UNIFI_USERNAME || 'unifi_api';
const PASSWORD = process.env.UNIFI_BROWSER_PASSWORD || process.env.UNIFI_PASSWORD;
console.log('🔍 Comprehensive Add Button Finder');
console.log('==================================\n');
if (!PASSWORD) {
console.error('❌ UNIFI_PASSWORD must be set in ~/.env');
process.exit(1);
}
(async () => {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
try {
console.log('1. Logging in...');
await page.goto(UDM_URL, { waitUntil: 'networkidle' });
await page.waitForSelector('input[type="text"]');
await page.fill('input[type="text"]', USERNAME);
await page.fill('input[type="password"]', PASSWORD);
await page.click('button[type="submit"]');
await page.waitForTimeout(5000);
console.log('2. Navigating to Routing page...');
// Wait for dashboard to fully load first
await page.waitForTimeout(5000);
// Navigate directly
await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'domcontentloaded' });
// Wait for URL to change (handle redirects)
await page.waitForURL('**/settings/routing**', { timeout: 20000 }).catch(() => {
console.log(' ⚠️ URL redirect not detected, continuing...');
});
await page.waitForTimeout(5000);
// Wait for page to be interactive
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
await page.waitForLoadState('domcontentloaded');
console.log(` Current URL: ${page.url()}`);
// Check if we're actually on routing page
const pageText = await page.textContent('body').catch(() => '');
if (!pageText.includes('Route') && !pageText.includes('routing')) {
console.log(' ⚠️ Page may not be fully loaded, waiting more...');
await page.waitForTimeout(10000);
}
// Wait for routes API
try {
await page.waitForResponse(response =>
response.url().includes('/rest/routing') || response.url().includes('/trafficroutes'),
{ timeout: 15000 }
);
console.log(' Routes API loaded');
} catch (error) {
console.log(' Routes API not detected');
}
await page.waitForTimeout(5000);
console.log('3. Analyzing page structure...\n');
// Get comprehensive page analysis
const analysis = await page.evaluate(() => {
const result = {
tables: [],
buttons: [],
sections: [],
potentialAddButtons: [],
};
// Find all tables
const tables = Array.from(document.querySelectorAll('table'));
tables.forEach((table, index) => {
const rect = table.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent?.trim() || '');
const rows = Array.from(table.querySelectorAll('tbody tr, tr')).length;
const tableText = table.textContent || '';
// Find buttons in/around this table
const tableButtons = [];
let current = table;
for (let i = 0; i < 3; i++) {
const buttons = Array.from(current.querySelectorAll('button, [role="button"]'));
buttons.forEach(btn => {
const btnRect = btn.getBoundingClientRect();
if (btnRect.width > 0 && btnRect.height > 0) {
const styles = window.getComputedStyle(btn);
if (styles.display !== 'none' && styles.visibility !== 'hidden') {
tableButtons.push({
text: btn.textContent?.trim() || '',
className: btn.className || '',
id: btn.id || '',
ariaLabel: btn.getAttribute('aria-label') || '',
position: { x: btnRect.x, y: btnRect.y },
isInHeader: table.querySelector('thead')?.contains(btn) || false,
isInToolbar: btn.closest('[class*="toolbar" i], [class*="header" i], [class*="action" i]') !== null,
});
}
}
});
current = current.parentElement;
if (!current) break;
}
result.tables.push({
index,
headers,
rowCount: rows,
hasRouteText: tableText.includes('Route') || tableText.includes('Static'),
buttonCount: tableButtons.length,
buttons: tableButtons,
position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
});
}
});
// Find all buttons with full context
const allButtons = Array.from(document.querySelectorAll('button, [role="button"]'));
allButtons.forEach((btn, index) => {
const rect = btn.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
const styles = window.getComputedStyle(btn);
if (styles.display !== 'none' && styles.visibility !== 'hidden') {
const text = btn.textContent?.trim() || '';
const className = btn.className || '';
const id = btn.id || '';
const ariaLabel = btn.getAttribute('aria-label') || '';
// Check if near a table
let nearTable = null;
let current = btn;
for (let i = 0; i < 5; i++) {
if (current.tagName === 'TABLE') {
nearTable = {
index: Array.from(document.querySelectorAll('table')).indexOf(current),
isInHeader: current.querySelector('thead')?.contains(btn) || false,
};
break;
}
current = current.parentElement;
if (!current) break;
}
// Check parent context
let parent = btn.parentElement;
let parentContext = '';
for (let i = 0; i < 3; i++) {
if (parent) {
const parentText = parent.textContent?.trim() || '';
if (parentText.includes('Route') || parentText.includes('Static')) {
parentContext = 'ROUTE_CONTEXT';
break;
}
parent = parent.parentElement;
}
}
const buttonInfo = {
index,
text,
className: className.substring(0, 100),
id,
ariaLabel,
position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
iconOnly: !text && btn.querySelector('svg') !== null,
nearTable,
hasRouteContext: parentContext === 'ROUTE_CONTEXT',
};
result.buttons.push(buttonInfo);
// Identify potential Add buttons
if (buttonInfo.iconOnly ||
text.toLowerCase().includes('add') ||
text.toLowerCase().includes('create') ||
className.toLowerCase().includes('add') ||
ariaLabel.toLowerCase().includes('add')) {
result.potentialAddButtons.push(buttonInfo);
}
}
}
});
return result;
});
console.log('📊 Page Analysis Results:');
console.log('='.repeat(80));
console.log(`\n📋 Tables Found: ${analysis.tables.length}`);
analysis.tables.forEach((table, i) => {
console.log(`\n${i + 1}. Table ${table.index}:`);
console.log(` Headers: ${table.headers.join(', ')}`);
console.log(` Rows: ${table.rowCount}`);
console.log(` Has Route Text: ${table.hasRouteText}`);
console.log(` Buttons: ${table.buttonCount}`);
if (table.buttons.length > 0) {
console.log(` Button Details:`);
table.buttons.forEach((btn, j) => {
console.log(` ${j + 1}. "${btn.text}" - ${btn.className.substring(0, 60)}`);
console.log(` In Header: ${btn.isInHeader}, In Toolbar: ${btn.isInToolbar}`);
console.log(` Position: (${btn.position.x}, ${btn.position.y})`);
});
}
});
console.log(`\n🔘 All Buttons: ${analysis.buttons.length}`);
analysis.buttons.forEach((btn, i) => {
console.log(`\n${i + 1}. Button ${btn.index}:`);
console.log(` Text: "${btn.text}"`);
console.log(` Class: ${btn.className}`);
console.log(` Icon Only: ${btn.iconOnly}`);
console.log(` Near Table: ${btn.nearTable ? `Table ${btn.nearTable.index}` : 'No'}`);
console.log(` Has Route Context: ${btn.hasRouteContext}`);
console.log(` Position: (${btn.position.x}, ${btn.position.y})`);
});
console.log(`\n🎯 Potential Add Buttons: ${analysis.potentialAddButtons.length}`);
analysis.potentialAddButtons.forEach((btn, i) => {
console.log(`\n${i + 1}. "${btn.text}" - ${btn.className}`);
console.log(` Near Table: ${btn.nearTable ? `Table ${btn.nearTable.index}` : 'No'}`);
console.log(` Has Route Context: ${btn.hasRouteContext}`);
console.log(` Selector: ${btn.id ? `#${btn.id}` : `.${btn.className.split(' ')[0]}`}`);
});
// Test clicking potential buttons
console.log(`\n🧪 Testing Potential Add Buttons:`);
console.log('='.repeat(80));
for (const btn of analysis.potentialAddButtons.slice(0, 5)) {
try {
console.log(`\nTesting: "${btn.text}" (${btn.className.substring(0, 50)})`);
let selector = btn.id ? `#${btn.id}` : `button:has-text("${btn.text}")`;
if (!btn.text && btn.className) {
const firstClass = btn.className.split(' ')[0];
selector = `button.${firstClass}`;
}
const button = await page.locator(selector).first();
if (await button.isVisible({ timeout: 2000 })) {
console.log(` ✅ Button is visible`);
// Try clicking
await button.click({ timeout: 5000 }).catch(async (error) => {
console.log(` ⚠️ Regular click failed: ${error.message}`);
// Try JavaScript click
await page.evaluate((id) => {
const el = document.getElementById(id);
if (el) el.click();
}, btn.id).catch(() => {});
});
await page.waitForTimeout(3000);
// Check if form appeared
const hasForm = await page.locator('input[name="name"], input[name="destination"], input[placeholder*="destination" i]').first().isVisible({ timeout: 2000 }).catch(() => false);
if (hasForm) {
console.log(` ✅✅✅ FORM APPEARED! This is the Add button! ✅✅✅`);
console.log(` Selector: ${selector}`);
console.log(` ID: ${btn.id || 'none'}`);
console.log(` Class: ${btn.className}`);
break;
} else {
// Check if menu appeared
const hasMenu = await page.locator('[role="menu"], [role="listbox"]').first().isVisible({ timeout: 2000 }).catch(() => false);
if (hasMenu) {
console.log(` ⚠️ Menu appeared (not form) - this might be a settings button`);
// Close menu
await page.keyboard.press('Escape');
await page.waitForTimeout(1000);
} else {
console.log(` ❌ No form or menu appeared`);
}
}
} else {
console.log(` ❌ Button not visible`);
}
} catch (error) {
console.log(` ❌ Error testing button: ${error.message}`);
}
}
console.log('\n\n⏸ Page is open in browser. Inspect manually if needed.');
console.log('Press Ctrl+C to close...\n');
await page.waitForTimeout(60000);
} catch (error) {
console.error('❌ Error:', error.message);
await page.screenshot({ path: 'find-add-error.png', fullPage: true });
} finally {
await browser.close();
}
})();

View File

@@ -0,0 +1,196 @@
#!/usr/bin/env node
/**
* Inspect Routing Page - Find Add Button
*
* This script opens the UDM Pro routing page and inspects all elements
* to help identify the Add button selector.
*/
import { chromium } from 'playwright';
import { readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
// Load environment variables
const envPath = join(homedir(), '.env');
function loadEnvFile(filePath) {
try {
const envFile = readFileSync(filePath, 'utf8');
const envVars = envFile.split('\n').filter(
(line) => line.includes('=') && !line.trim().startsWith('#')
);
for (const line of envVars) {
const [key, ...values] = line.split('=');
if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) {
let value = values.join('=').trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key.trim()] = value;
}
}
return true;
} catch {
return false;
}
}
loadEnvFile(envPath);
const UDM_URL = process.env.UNIFI_UDM_URL || 'https://192.168.0.1';
const USERNAME = process.env.UNIFI_BROWSER_USERNAME || process.env.UNIFI_USERNAME || 'unifi_api';
const PASSWORD = process.env.UNIFI_BROWSER_PASSWORD || process.env.UNIFI_PASSWORD;
console.log('🔍 UDM Pro Routing Page Inspector');
console.log('==================================\n');
if (!PASSWORD) {
console.error('❌ UNIFI_PASSWORD must be set in ~/.env');
process.exit(1);
}
(async () => {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
try {
console.log('1. Logging in...');
await page.goto(UDM_URL, { waitUntil: 'networkidle' });
await page.waitForSelector('input[type="text"]');
await page.fill('input[type="text"]', USERNAME);
await page.fill('input[type="password"]', PASSWORD);
await page.click('button[type="submit"]');
await page.waitForTimeout(3000);
console.log('2. Navigating to Routing page...');
await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'networkidle' });
await page.waitForTimeout(5000);
console.log('3. Inspecting page elements...\n');
// Get all clickable elements
const clickableElements = await page.evaluate(() => {
const elements = [];
const allElements = document.querySelectorAll('button, a, [role="button"], [onclick], [class*="button"], [class*="click"]');
allElements.forEach((el, index) => {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
const styles = window.getComputedStyle(el);
if (styles.display !== 'none' && styles.visibility !== 'hidden') {
const text = el.textContent?.trim() || '';
const tagName = el.tagName.toLowerCase();
const className = el.className || '';
const id = el.id || '';
const ariaLabel = el.getAttribute('aria-label') || '';
const dataTestId = el.getAttribute('data-testid') || '';
const onclick = el.getAttribute('onclick') || '';
// Get parent info
const parent = el.parentElement;
const parentClass = parent?.className || '';
const parentTag = parent?.tagName.toLowerCase() || '';
elements.push({
index,
tagName,
text: text.substring(0, 50),
className: className.substring(0, 100),
id,
ariaLabel,
dataTestId,
onclick: onclick.substring(0, 50),
parentTag,
parentClass: parentClass.substring(0, 100),
xpath: getXPath(el),
});
}
}
});
function getXPath(element) {
if (element.id !== '') {
return `//*[@id="${element.id}"]`;
}
if (element === document.body) {
return '/html/body';
}
let ix = 0;
const siblings = element.parentNode.childNodes;
for (let i = 0; i < siblings.length; i++) {
const sibling = siblings[i];
if (sibling === element) {
return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
}
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
ix++;
}
}
}
return elements;
});
console.log(`Found ${clickableElements.length} clickable elements:\n`);
// Filter for potential Add buttons
const potentialAddButtons = clickableElements.filter(el => {
const text = el.text.toLowerCase();
const className = el.className.toLowerCase();
const ariaLabel = el.ariaLabel.toLowerCase();
return (
text.includes('add') ||
text.includes('create') ||
text.includes('new') ||
text === '+' ||
text === '' ||
className.includes('add') ||
className.includes('create') ||
ariaLabel.includes('add') ||
ariaLabel.includes('create')
);
});
console.log('🎯 Potential Add Buttons:');
console.log('='.repeat(80));
potentialAddButtons.forEach((el, i) => {
console.log(`\n${i + 1}. Element ${el.index}:`);
console.log(` Tag: ${el.tagName}`);
console.log(` Text: "${el.text}"`);
console.log(` Class: ${el.className}`);
console.log(` ID: ${el.id || 'none'}`);
console.log(` Aria Label: ${el.ariaLabel || 'none'}`);
console.log(` Data Test ID: ${el.dataTestId || 'none'}`);
console.log(` Parent: <${el.parentTag}> ${el.parentClass}`);
console.log(` XPath: ${el.xpath}`);
console.log(` Selector: ${el.tagName}${el.id ? `#${el.id}` : ''}${el.className ? `.${el.className.split(' ')[0]}` : ''}`);
});
console.log('\n\n📋 All Buttons on Page:');
console.log('='.repeat(80));
const buttons = clickableElements.filter(el => el.tagName === 'button');
buttons.forEach((el, i) => {
console.log(`\n${i + 1}. Button ${el.index}:`);
console.log(` Text: "${el.text}"`);
console.log(` Class: ${el.className}`);
console.log(` Aria Label: ${el.ariaLabel || 'none'}`);
console.log(` XPath: ${el.xpath}`);
});
console.log('\n\n⏸ Page is open in browser. Inspect elements manually if needed.');
console.log('Press Ctrl+C to close...\n');
// Keep browser open
await page.waitForTimeout(60000);
} catch (error) {
console.error('❌ Error:', error.message);
await page.screenshot({ path: 'inspect-error.png', fullPage: true });
} finally {
await browser.close();
}
})();

View File

@@ -0,0 +1,277 @@
#!/usr/bin/env node
/**
* List current ACL rules from UniFi Network API
* Helps diagnose what might be blocking access to VLAN 11
*/
import { readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
// Load environment variables
const envPath = join(homedir(), '.env');
function loadEnvFile(filePath) {
try {
const envFile = readFileSync(filePath, 'utf8');
const envVars = envFile.split('\n').filter(
(line) => line.includes('=') && !line.trim().startsWith('#')
);
for (const line of envVars) {
const [key, ...values] = line.split('=');
if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) {
let value = values.join('=').trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key.trim()] = value;
}
}
return true;
} catch {
return false;
}
}
loadEnvFile(envPath);
const baseUrl = process.env.UNIFI_UDM_URL || 'https://192.168.0.1';
const apiKey = process.env.UNIFI_API_KEY;
const siteId = '88f7af54-98f8-306a-a1c7-c9349722b1f6';
if (!apiKey) {
console.error('❌ UNIFI_API_KEY not set in environment');
process.exit(1);
}
// Fetch networks to map IDs to VLANs
async function fetchNetworks() {
const response = await fetch(`${baseUrl}/proxy/network/integration/v1/sites/${siteId}/networks`, {
headers: {
'X-API-KEY': apiKey,
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch networks: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const networks = data.data || [];
const networkMap = {};
for (const net of networks) {
networkMap[net.id] = {
vlanId: net.vlanId,
name: net.name,
subnet: net.ipSubnet,
};
}
return networkMap;
}
// List ACL rules
async function listACLRules() {
const response = await fetch(`${baseUrl}/proxy/network/integration/v1/sites/${siteId}/acl-rules`, {
headers: {
'X-API-KEY': apiKey,
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch ACL rules: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data.data || [];
}
// Main function
async function main() {
try {
console.log('Fetching ACL Rules from UniFi Network API');
console.log('==========================================');
console.log('');
// Fetch networks and ACL rules in parallel
const [networkMap, rules] = await Promise.all([
fetchNetworks(),
listACLRules(),
]);
console.log(`Found ${rules.length} ACL rules`);
console.log('');
if (rules.length === 0) {
console.log('No ACL rules configured. Default policy applies.');
return;
}
// Find VLAN 11 network ID
const vlan11Network = Object.values(networkMap).find(n => n.vlanId === 11);
const vlan11Id = vlan11Network ? Object.keys(networkMap).find(id => networkMap[id].vlanId === 11) : null;
console.log('='.repeat(80));
console.log('ALL ACL RULES (sorted by priority/index)');
console.log('='.repeat(80));
console.log('');
// Sort rules by index (lower index = higher priority)
const sortedRules = [...rules].sort((a, b) => (a.index || 9999) - (b.index || 9999));
for (const rule of sortedRules) {
const enabled = rule.enabled ? '✅' : '❌';
const action = rule.action === 'BLOCK' ? '🚫 BLOCK' : '✅ ALLOW';
console.log(`${enabled} ${action} - ${rule.name || 'Unnamed Rule'}`);
console.log(` Priority/Index: ${rule.index ?? 'N/A'}`);
if (rule.description) {
console.log(` Description: ${rule.description}`);
}
// Parse source filter
if (rule.sourceFilter) {
if (rule.sourceFilter.type === 'NETWORKS' && rule.sourceFilter.networkIds) {
const sourceNetworks = rule.sourceFilter.networkIds
.map(id => {
const net = networkMap[id];
return net ? `VLAN ${net.vlanId} (${net.name})` : id;
})
.join(', ');
console.log(` Source: ${sourceNetworks}`);
} else if (rule.sourceFilter.type === 'NETWORK' && rule.sourceFilter.networkIds) {
const sourceNetworks = rule.sourceFilter.networkIds
.map(id => {
const net = networkMap[id];
return net ? `VLAN ${net.vlanId} (${net.name})` : id;
})
.join(', ');
console.log(` Source: ${sourceNetworks}`);
} else if (rule.sourceFilter.type === 'IP_ADDRESS') {
console.log(` Source: IP Address Filter`);
} else {
console.log(` Source: ${JSON.stringify(rule.sourceFilter)}`);
}
} else {
console.log(` Source: Any`);
}
// Parse destination filter
if (rule.destinationFilter) {
if (rule.destinationFilter.type === 'NETWORKS' && rule.destinationFilter.networkIds) {
const destNetworks = rule.destinationFilter.networkIds
.map(id => {
const net = networkMap[id];
return net ? `VLAN ${net.vlanId} (${net.name})` : id;
})
.join(', ');
console.log(` Destination: ${destNetworks}`);
} else if (rule.destinationFilter.type === 'NETWORK' && rule.destinationFilter.networkIds) {
const destNetworks = rule.destinationFilter.networkIds
.map(id => {
const net = networkMap[id];
return net ? `VLAN ${net.vlanId} (${net.name})` : id;
})
.join(', ');
console.log(` Destination: ${destNetworks}`);
} else if (rule.destinationFilter.type === 'IP_ADDRESS') {
console.log(` Destination: IP Address Filter`);
} else {
console.log(` Destination: ${JSON.stringify(rule.destinationFilter)}`);
}
} else {
console.log(` Destination: Any`);
}
// Protocol filter
if (rule.protocolFilter && rule.protocolFilter.length > 0) {
console.log(` Protocol: ${rule.protocolFilter.join(', ')}`);
} else {
console.log(` Protocol: Any`);
}
// Port filter
if (rule.portFilter) {
if (rule.portFilter.type === 'PORTS' && rule.portFilter.ports) {
console.log(` Ports: ${rule.portFilter.ports.join(', ')}`);
} else {
console.log(` Ports: ${JSON.stringify(rule.portFilter)}`);
}
}
console.log('');
}
// Analyze rules affecting VLAN 11
if (vlan11Id) {
console.log('='.repeat(80));
console.log('RULES AFFECTING VLAN 11 (MGMT-LAN)');
console.log('='.repeat(80));
console.log('');
const vlan11Rules = sortedRules.filter(rule => {
if (!rule.enabled) return false;
// Check if rule affects VLAN 11 as source
const affectsSource = rule.sourceFilter &&
((rule.sourceFilter.type === 'NETWORKS' || rule.sourceFilter.type === 'NETWORK') &&
rule.sourceFilter.networkIds &&
rule.sourceFilter.networkIds.includes(vlan11Id));
// Check if rule affects VLAN 11 as destination
const affectsDest = rule.destinationFilter &&
((rule.destinationFilter.type === 'NETWORKS' || rule.destinationFilter.type === 'NETWORK') &&
rule.destinationFilter.networkIds &&
rule.destinationFilter.networkIds.includes(vlan11Id));
return affectsSource || affectsDest;
});
if (vlan11Rules.length === 0) {
console.log('No specific rules found affecting VLAN 11.');
console.log('Default policy applies (typically ALLOW for inter-VLAN traffic).');
} else {
for (const rule of vlan11Rules) {
const action = rule.action === 'BLOCK' ? '🚫 BLOCKS' : '✅ ALLOWS';
const direction = rule.sourceFilter?.networkIds?.includes(vlan11Id)
? 'FROM VLAN 11'
: rule.destinationFilter?.networkIds?.includes(vlan11Id)
? 'TO VLAN 11'
: 'AFFECTING VLAN 11';
console.log(`${action} ${direction}: ${rule.name || 'Unnamed Rule'}`);
console.log(` Priority: ${rule.index ?? 'N/A'}`);
if (rule.description) {
console.log(` ${rule.description}`);
}
console.log('');
}
}
}
// Summary
console.log('='.repeat(80));
console.log('SUMMARY');
console.log('='.repeat(80));
console.log('');
console.log(`Total Rules: ${rules.length}`);
console.log(`Enabled Rules: ${rules.filter(r => r.enabled).length}`);
console.log(`Block Rules: ${rules.filter(r => r.action === 'BLOCK' && r.enabled).length}`);
console.log(`Allow Rules: ${rules.filter(r => r.action === 'ALLOW' && r.enabled).length}`);
console.log('');
} catch (error) {
console.error('❌ Error:', error.message);
if (error.stack) {
console.error(error.stack);
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,333 @@
#!/usr/bin/env node
/**
* Map Routing Page Structure
*
* This script fully maps the UDM Pro routing page structure to understand:
* - All sections and their locations
* - Button placements and contexts
* - How the page changes based on state
* - Form locations and structures
*/
import { chromium } from 'playwright';
import { readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
// Load environment variables
const envPath = join(homedir(), '.env');
function loadEnvFile(filePath) {
try {
const envFile = readFileSync(filePath, 'utf8');
const envVars = envFile.split('\n').filter(
(line) => line.includes('=') && !line.trim().startsWith('#')
);
for (const line of envVars) {
const [key, ...values] = line.split('=');
if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) {
let value = values.join('=').trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[key.trim()] = value;
}
}
return true;
} catch {
return false;
}
}
loadEnvFile(envPath);
const UDM_URL = process.env.UNIFI_UDM_URL || 'https://192.168.0.1';
const USERNAME = process.env.UNIFI_BROWSER_USERNAME || process.env.UNIFI_USERNAME || 'unifi_api';
const PASSWORD = process.env.UNIFI_BROWSER_PASSWORD || process.env.UNIFI_PASSWORD;
console.log('🗺️ UDM Pro Routing Page Structure Mapper');
console.log('==========================================\n');
if (!PASSWORD) {
console.error('❌ UNIFI_PASSWORD must be set in ~/.env');
process.exit(1);
}
(async () => {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
try {
console.log('1. Logging in...');
await page.goto(UDM_URL, { waitUntil: 'networkidle' });
await page.waitForSelector('input[type="text"]');
await page.fill('input[type="text"]', USERNAME);
await page.fill('input[type="password"]', PASSWORD);
await page.click('button[type="submit"]');
await page.waitForTimeout(5000);
console.log('2. Navigating to Routing page...');
// Wait for dashboard to load first
await page.waitForTimeout(3000);
// Navigate to routing page
await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'networkidle' });
await page.waitForTimeout(5000);
// Wait for routing-specific content
await page.waitForSelector('body', { timeout: 10000 });
// Check if we're actually on the routing page
const currentUrl = page.url();
console.log(` Current URL: ${currentUrl}`);
if (currentUrl.includes('/login')) {
console.log(' ⚠️ Still on login page, waiting for redirect...');
await page.waitForURL('**/settings/routing**', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(5000);
}
// Wait for API calls to complete
await page.waitForResponse(response =>
response.url().includes('/rest/routing') ||
response.url().includes('/settings/routing'),
{ timeout: 10000 }
).catch(() => {});
await page.waitForTimeout(5000); // Final wait for full render
console.log('3. Mapping page structure...\n');
// Get comprehensive page structure
const pageStructure = await page.evaluate(() => {
const structure = {
url: window.location.href,
title: document.title,
sections: [],
buttons: [],
forms: [],
tables: [],
textContent: {},
layout: {},
};
// Find all major sections
const sectionSelectors = [
'main',
'section',
'[role="main"]',
'[class*="container" i]',
'[class*="section" i]',
'[class*="panel" i]',
'[class*="content" i]',
'[class*="page" i]',
];
sectionSelectors.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach((el, index) => {
const rect = el.getBoundingClientRect();
if (rect.width > 100 && rect.height > 100) {
const text = el.textContent?.trim().substring(0, 200) || '';
structure.sections.push({
selector,
index,
tag: el.tagName,
className: el.className || '',
id: el.id || '',
text: text,
position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
hasButtons: el.querySelectorAll('button').length,
hasForms: el.querySelectorAll('form').length,
hasTables: el.querySelectorAll('table').length,
});
}
});
});
// Find all buttons with full context
const buttons = Array.from(document.querySelectorAll('button, [role="button"], a[class*="button" i]'));
buttons.forEach((btn, index) => {
const rect = btn.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
const styles = window.getComputedStyle(btn);
if (styles.display !== 'none' && styles.visibility !== 'hidden') {
// Get parent context
let parent = btn.parentElement;
let parentContext = '';
let depth = 0;
while (parent && depth < 5) {
const parentText = parent.textContent?.trim() || '';
const parentClass = parent.className || '';
if (parentText.length > 0 && parentText.length < 100) {
parentContext = parentText + ' > ' + parentContext;
}
if (parentClass.includes('route') || parentClass.includes('routing') ||
parentClass.includes('table') || parentClass.includes('header')) {
parentContext = `[${parentClass.substring(0, 50)}] > ` + parentContext;
}
parent = parent.parentElement;
depth++;
}
structure.buttons.push({
index,
tag: btn.tagName,
text: btn.textContent?.trim() || '',
className: btn.className || '',
id: btn.id || '',
ariaLabel: btn.getAttribute('aria-label') || '',
dataTestId: btn.getAttribute('data-testid') || '',
position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
parentContext: parentContext.substring(0, 200),
isVisible: true,
isEnabled: !btn.disabled,
hasIcon: btn.querySelector('svg') !== null,
iconOnly: !btn.textContent?.trim() && btn.querySelector('svg') !== null,
});
}
}
});
// Find all forms
const forms = Array.from(document.querySelectorAll('form'));
forms.forEach((form, index) => {
const rect = form.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
const inputs = Array.from(form.querySelectorAll('input, select, textarea'));
structure.forms.push({
index,
id: form.id || '',
className: form.className || '',
action: form.action || '',
method: form.method || '',
inputs: inputs.map(input => ({
type: input.type || input.tagName,
name: input.name || '',
placeholder: input.placeholder || '',
id: input.id || '',
})),
position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
});
}
});
// Find all tables
const tables = Array.from(document.querySelectorAll('table'));
tables.forEach((table, index) => {
const rect = table.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent?.trim() || '');
const rows = Array.from(table.querySelectorAll('tbody tr')).length;
structure.tables.push({
index,
className: table.className || '',
id: table.id || '',
headers,
rowCount: rows,
position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
hasButtons: table.querySelectorAll('button').length,
});
}
});
// Get page text content for context
const bodyText = document.body.textContent || '';
structure.textContent = {
hasStaticRoutes: bodyText.includes('Static Routes') || bodyText.includes('Static Route'),
hasRoutes: bodyText.includes('Route') && !bodyText.includes('Router'),
hasAdd: bodyText.includes('Add') || bodyText.includes('Create') || bodyText.includes('New'),
hasTable: bodyText.includes('table') || document.querySelector('table') !== null,
fullText: bodyText.substring(0, 1000),
};
return structure;
});
console.log('📄 Page Structure:');
console.log('='.repeat(80));
console.log(`URL: ${pageStructure.url}`);
console.log(`Title: ${pageStructure.title}`);
console.log(`\nText Context:`);
console.log(` Has "Static Routes": ${pageStructure.textContent.hasStaticRoutes}`);
console.log(` Has "Route": ${pageStructure.textContent.hasRoutes}`);
console.log(` Has "Add/Create": ${pageStructure.textContent.hasAdd}`);
console.log(` Has Table: ${pageStructure.textContent.hasTable}`);
console.log(`\n📦 Sections (${pageStructure.sections.length}):`);
pageStructure.sections.forEach((section, i) => {
console.log(`\n${i + 1}. ${section.tag}.${section.className.substring(0, 50)}`);
console.log(` Position: (${section.position.x}, ${section.position.y}) ${section.position.width}x${section.position.height}`);
console.log(` Buttons: ${section.hasButtons}, Forms: ${section.hasForms}, Tables: ${section.hasTables}`);
console.log(` Text: "${section.text.substring(0, 100)}"`);
});
console.log(`\n🔘 Buttons (${pageStructure.buttons.length}):`);
pageStructure.buttons.forEach((btn, i) => {
console.log(`\n${i + 1}. Button ${btn.index}:`);
console.log(` Tag: ${btn.tag}`);
console.log(` Text: "${btn.text}"`);
console.log(` Class: ${btn.className.substring(0, 80)}`);
console.log(` ID: ${btn.id || 'none'}`);
console.log(` Aria Label: ${btn.ariaLabel || 'none'}`);
console.log(` Position: (${btn.position.x}, ${btn.position.y}) ${btn.position.width}x${btn.position.height}`);
console.log(` Icon Only: ${btn.iconOnly}, Has Icon: ${btn.hasIcon}`);
console.log(` Enabled: ${btn.isEnabled}`);
console.log(` Context: ${btn.parentContext.substring(0, 150)}`);
});
console.log(`\n📋 Tables (${pageStructure.tables.length}):`);
pageStructure.tables.forEach((table, i) => {
console.log(`\n${i + 1}. Table ${table.index}:`);
console.log(` Class: ${table.className.substring(0, 80)}`);
console.log(` Headers: ${table.headers.join(', ')}`);
console.log(` Rows: ${table.rowCount}`);
console.log(` Buttons: ${table.hasButtons}`);
console.log(` Position: (${table.position.x}, ${table.position.y})`);
});
console.log(`\n📝 Forms (${pageStructure.forms.length}):`);
pageStructure.forms.forEach((form, i) => {
console.log(`\n${i + 1}. Form ${form.index}:`);
console.log(` Inputs: ${form.inputs.length}`);
form.inputs.forEach((input, j) => {
console.log(` ${j + 1}. ${input.type} name="${input.name}" placeholder="${input.placeholder}"`);
});
});
// Identify potential Add button
console.log(`\n🎯 Potential Add Button Analysis:`);
console.log('='.repeat(80));
const iconOnlyButtons = pageStructure.buttons.filter(b => b.iconOnly);
const buttonsNearRoutes = pageStructure.buttons.filter(b =>
b.parentContext.toLowerCase().includes('route') ||
b.parentContext.toLowerCase().includes('routing') ||
b.parentContext.toLowerCase().includes('table')
);
console.log(`Icon-only buttons: ${iconOnlyButtons.length}`);
iconOnlyButtons.forEach((btn, i) => {
console.log(` ${i + 1}. ${btn.className.substring(0, 60)} - Position: (${btn.position.x}, ${btn.position.y})`);
});
console.log(`\nButtons near routes/table: ${buttonsNearRoutes.length}`);
buttonsNearRoutes.forEach((btn, i) => {
console.log(` ${i + 1}. "${btn.text}" - ${btn.className.substring(0, 60)}`);
});
console.log('\n\n⏸ Page is open in browser. Inspect manually if needed.');
console.log('Press Ctrl+C to close...\n');
// Keep browser open
await page.waitForTimeout(60000);
} catch (error) {
console.error('❌ Error:', error.message);
await page.screenshot({ path: 'map-error.png', fullPage: true });
} finally {
await browser.close();
}
})();

66
scripts/unifi/monitor-health.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/bin/bash
set -euo pipefail
# Monitor UniFi controller health and send alerts
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
LOG_FILE="${LOG_FILE:-$PROJECT_ROOT/logs/unifi-health.log}"
cd "$PROJECT_ROOT"
# Create logs directory if it doesn't exist
mkdir -p "$(dirname "$LOG_FILE")"
# Load environment variables
if [ -f ~/.env ]; then
source <(grep "^UNIFI_" ~/.env | sed 's/^/export /')
fi
TIMESTAMP=$(date -Iseconds)
STATUS="unknown"
# Function to log message
log() {
echo "[$TIMESTAMP] $1" | tee -a "$LOG_FILE"
}
# Function to check API health
check_api_health() {
if NODE_TLS_REJECT_UNAUTHORIZED=0 pnpm unifi:cli sites 2>&1 | grep -q "internalReference"; then
return 0
else
return 1
fi
}
# Function to send alert (placeholder - implement your alerting mechanism)
send_alert() {
local message="$1"
log "ALERT: $message"
# Add your alerting mechanism here (email, webhook, etc.)
# Example: curl -X POST https://your-webhook-url -d "{\"text\":\"$message\"}"
}
# Main health check
log "Starting health check..."
if check_api_health; then
STATUS="healthy"
log "✅ Controller is healthy"
else
STATUS="unhealthy"
log "❌ Controller health check failed"
send_alert "UniFi Controller API health check failed at $TIMESTAMP"
fi
log "Health check complete: $STATUS"
# Exit with appropriate code
if [ "$STATUS" = "healthy" ]; then
exit 0
else
exit 1
fi

View File

@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# Query UniFi Network API for firewall zones, ACL rules, traffic matching lists, DPI.
# Use output to see if any rule could affect HTTP POST (RPC 405). Official API is L3/L4 only.
# Usage: ./scripts/unifi/query-firewall-and-dpi-api.sh [output_dir]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$PROJECT_ROOT"
# Load UNIFI_* from repo .env, unifi-api/.env, or ~/.env
if [ -f "$PROJECT_ROOT/.env" ]; then set -a && source "$PROJECT_ROOT/.env" 2>/dev/null && set +a; fi
if [ -f "$PROJECT_ROOT/unifi-api/.env" ]; then set -a && source "$PROJECT_ROOT/unifi-api/.env" 2>/dev/null && set +a; fi
if [ -f ~/.env ]; then source <(grep -E '^UNIFI_' ~/.env 2>/dev/null | sed 's/^/export /') 2>/dev/null || true; fi
UDM_URL="${UNIFI_UDM_URL:-https://192.168.0.1}"
API_KEY="${UNIFI_API_KEY:-}"
# Use UUID from sites list; default for single-site "Default"
SITE_ID="${UNIFI_SITE_ID:-88f7af54-98f8-306a-a1c7-c9349722b1f6}"
if [ "$SITE_ID" = "default" ]; then
SITE_ID="88f7af54-98f8-306a-a1c7-c9349722b1f6"
fi
if [ -z "$API_KEY" ]; then
echo "UNIFI_API_KEY is not set. Add it to .env or unifi-api/.env." >&2
exit 1
fi
OUT_DIR="${1:-$PROJECT_ROOT/docs/04-configuration/verification-evidence/unifi-api-firewall-query}"
mkdir -p "$OUT_DIR"
REPORT="$OUT_DIR/report.md"
BASE="$UDM_URL/proxy/network/integration/v1"
echo "Querying UniFi Network API (Official) for firewall/ACL/DPI..."
echo ""
# Fetch and save JSON
curl -k -s -X GET "$BASE/sites/$SITE_ID/acl-rules?limit=200" \
-H "X-API-KEY: $API_KEY" -H "Accept: application/json" -o "$OUT_DIR/acl-rules.json"
curl -k -s -X GET "$BASE/sites/$SITE_ID/firewall/zones?limit=200" \
-H "X-API-KEY: $API_KEY" -H "Accept: application/json" -o "$OUT_DIR/firewall-zones.json"
curl -k -s -X GET "$BASE/sites/$SITE_ID/traffic-matching-lists?limit=200" \
-H "X-API-KEY: $API_KEY" -H "Accept: application/json" -o "$OUT_DIR/traffic-matching-lists.json"
curl -k -s -X GET "$BASE/dpi/categories?limit=100" \
-H "X-API-KEY: $API_KEY" -H "Accept: application/json" -o "$OUT_DIR/dpi-categories.json"
curl -k -s -X GET "$BASE/sites/$SITE_ID/wans" \
-H "X-API-KEY: $API_KEY" -H "Accept: application/json" -o "$OUT_DIR/wans.json"
# Build report
{
echo "# UniFi API firewall/ACL/DPI query report"
echo ""
echo "Generated: $(date -Iseconds)"
echo "Site ID: $SITE_ID"
echo "Base: $BASE"
echo ""
echo "## Summary"
echo ""
ACL_COUNT=$(jq -r '.totalCount // .count // 0' "$OUT_DIR/acl-rules.json" 2>/dev/null || echo "0")
ZONE_COUNT=$(jq -r '.totalCount // .count // 0' "$OUT_DIR/firewall-zones.json" 2>/dev/null || echo "0")
TML_COUNT=$(jq -r '.totalCount // .count // 0' "$OUT_DIR/traffic-matching-lists.json" 2>/dev/null || echo "0")
DPI_COUNT=$(jq -r '.totalCount // .count // 0' "$OUT_DIR/dpi-categories.json" 2>/dev/null || echo "0")
echo "- **ACL rules:** $ACL_COUNT (user-defined L3/L4 rules)"
echo "- **Firewall zones:** $ZONE_COUNT"
echo "- **Traffic matching lists:** $TML_COUNT"
echo "- **DPI categories:** $DPI_COUNT"
echo ""
echo "## HTTP POST (RPC 405) and this API"
echo ""
echo "The **Official UniFi Network API** exposes:"
echo "- **ACL rules:** L3/L4 only (protocol TCP/UDP, ports, source/dest). No HTTP method (GET vs POST)."
echo "- **Firewall zones:** Grouping of networks (Internal, External, etc.). No method filtering."
echo "- **Traffic matching lists:** Port/IP lists. No HTTP method."
echo "- **DPI categories:** Application categories for app-based blocking (e.g. \"Web services\"). Not method-specific."
echo ""
echo "**Conclusion:** The 405 Method Not Allowed for RPC POST is **not** configurable or visible via this API. It is likely enforced by the device's port-forward/NAT layer or a built-in proxy that does not expose HTTP-method settings in the API. To fix RPC 405: allow POST on the edge (UDM Pro UI / firmware) or use Cloudflare Tunnel for RPC (see docs/05-network/E2E_RPC_EDGE_LIMITATION.md)."
echo ""
echo "## Output files"
echo ""
echo "- \`acl-rules.json\` - ACL rules (empty if no custom rules)"
echo "- \`firewall-zones.json\` - Zone definitions"
echo "- \`traffic-matching-lists.json\` - Port/IP lists"
echo "- \`dpi-categories.json\` - DPI app categories"
echo "- \`wans.json\` - WAN interfaces"
} > "$REPORT"
echo "Report: $REPORT"
echo "JSON: $OUT_DIR/*.json"
cat "$REPORT"

View File

@@ -0,0 +1,25 @@
#!/bin/bash
set -euo pipefail
# Run automation with manual Add button click
# This script will navigate to the page and wait for you to click Add
cd /home/intlc/projects/proxmox
echo "🚀 Starting UDM Pro Static Route Automation"
echo "=========================================="
echo ""
echo "The script will:"
echo "1. Log in to UDM Pro"
echo "2. Navigate to Static Routes page"
echo "3. Wait for you to manually click the Add button"
echo "4. Automatically fill the form and save"
echo ""
echo "Press Enter to continue..."
read
UNIFI_USERNAME=unifi_api \
UNIFI_PASSWORD='L@kers2010$$' \
HEADLESS=false \
PAUSE_MODE=true \
node scripts/unifi/configure-static-route-playwright.js

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

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