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>
27
scripts/unifi/QUICK_START.md
Normal 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
@@ -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
|
||||
125
scripts/unifi/add-vlan11-secondary-ip-ifupdown.sh
Executable 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 ""
|
||||
119
scripts/unifi/add-vlan11-secondary-ip-ifupdown.sh.bak
Executable 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 ""
|
||||
199
scripts/unifi/add-vlan11-secondary-ip-netplan.sh
Executable 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 ""
|
||||
193
scripts/unifi/add-vlan11-secondary-ip-netplan.sh.bak
Executable 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 ""
|
||||
97
scripts/unifi/add-vlan11-secondary-ip-simple.sh
Executable 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 ""
|
||||
91
scripts/unifi/add-vlan11-secondary-ip-simple.sh.bak
Executable 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 ""
|
||||
152
scripts/unifi/add-vlan11-secondary-ip-systemd.sh
Executable 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 ""
|
||||
146
scripts/unifi/add-vlan11-secondary-ip-systemd.sh.bak
Executable 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 ""
|
||||
104
scripts/unifi/add-vlan11-secondary-ip.sh
Executable 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 ""
|
||||
98
scripts/unifi/add-vlan11-secondary-ip.sh.bak
Executable 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 ""
|
||||
193
scripts/unifi/allow-default-network-to-vlan11-node.js
Executable 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();
|
||||
332
scripts/unifi/analyze-page-visually.js
Executable 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();
|
||||
}
|
||||
})();
|
||||
173
scripts/unifi/change-ip-to-vlan11-netplan.sh
Executable 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"
|
||||
167
scripts/unifi/change-ip-to-vlan11-netplan.sh.bak
Executable 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"
|
||||
145
scripts/unifi/change-ip-to-vlan11.sh
Executable 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
|
||||
139
scripts/unifi/change-ip-to-vlan11.sh.bak
Executable 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
|
||||
369
scripts/unifi/comprehensive-page-mapper.js
Executable 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();
|
||||
}
|
||||
})();
|
||||
177
scripts/unifi/configure-inter-vlan-firewall-rules-api.js
Executable 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);
|
||||
2922
scripts/unifi/configure-static-route-playwright.js
Executable file
189
scripts/unifi/configure-vlans-node.js
Executable 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);
|
||||
});
|
||||
203
scripts/unifi/create-firewall-rules-node.js
Executable 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();
|
||||
218
scripts/unifi/create-firewall-rules.sh
Executable 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."
|
||||
236
scripts/unifi/create-management-firewall-rules-node.js
Executable 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
@@ -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" \
|
||||
"$@"
|
||||
348
scripts/unifi/find-add-button-comprehensive.js
Executable 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();
|
||||
}
|
||||
})();
|
||||
196
scripts/unifi/inspect-routing-page.js
Executable 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();
|
||||
}
|
||||
})();
|
||||
277
scripts/unifi/list-acl-rules-node.js
Executable 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();
|
||||
333
scripts/unifi/map-routing-page-structure.js
Executable 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
@@ -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
|
||||
91
scripts/unifi/query-firewall-and-dpi-api.sh
Executable 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"
|
||||
25
scripts/unifi/run-with-manual-add.sh
Executable 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
|
||||
BIN
scripts/unifi/screenshots/1768381870674-01-login-page.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768381877696-03-after-login.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768381878946-04-direct-navigation.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
scripts/unifi/screenshots/1768381881192-05-routing-page.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 39 KiB |
BIN
scripts/unifi/screenshots/1768381882095-error-state.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
scripts/unifi/screenshots/1768381968200-01-login-page.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768381976865-05-routing-page.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
BIN
scripts/unifi/screenshots/1768381977190-07-before-add-button.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
scripts/unifi/screenshots/1768381977426-08-add-route-form.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
scripts/unifi/screenshots/1768381977582-09-form-filled.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
scripts/unifi/screenshots/1768381977695-10-route-saved.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
scripts/unifi/screenshots/1768382191736-01-login-page.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 39 KiB |
BIN
scripts/unifi/screenshots/1768382195829-03-login-error.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382196027-03-login-error.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382196132-error-state.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382209799-01-login-page.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 39 KiB |
BIN
scripts/unifi/screenshots/1768382214862-03-login-error.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382214967-03-login-error.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382215075-error-state.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382229908-01-login-page.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 39 KiB |
BIN
scripts/unifi/screenshots/1768382234819-03-login-error.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382234921-03-login-error.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382235015-error-state.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382607085-01-login-page.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 39 KiB |
BIN
scripts/unifi/screenshots/1768382612036-03-login-error.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382612146-03-login-error.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382612230-error-state.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382626393-01-login-page.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 39 KiB |
BIN
scripts/unifi/screenshots/1768382631248-03-login-error.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382631358-03-login-error.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382631452-error-state.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
scripts/unifi/screenshots/1768382655099-01-login-page.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 38 KiB |
BIN
scripts/unifi/screenshots/1768382662190-03-after-login.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
scripts/unifi/screenshots/1768382667356-05-routing-page.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 56 KiB |
BIN
scripts/unifi/screenshots/1768382667833-07-before-add-button.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
scripts/unifi/screenshots/1768382668151-error-state.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
scripts/unifi/screenshots/1768382674446-01-login-page.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 38 KiB |
BIN
scripts/unifi/screenshots/1768382681269-03-after-login.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
scripts/unifi/screenshots/1768382686364-05-routing-page.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 56 KiB |
BIN
scripts/unifi/screenshots/1768382686828-07-before-add-button.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
scripts/unifi/screenshots/1768382687114-error-state.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
scripts/unifi/screenshots/1768382723427-01-login-page.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 38 KiB |
BIN
scripts/unifi/screenshots/1768382730399-03-after-login.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
scripts/unifi/screenshots/1768382735194-05-routing-page.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 56 KiB |
BIN
scripts/unifi/screenshots/1768382735508-07-before-add-button.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
scripts/unifi/screenshots/1768382737400-08-add-route-form.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
scripts/unifi/screenshots/1768382737635-09-form-filled.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
scripts/unifi/screenshots/1768382737816-error-state.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
scripts/unifi/screenshots/1768382744171-01-login-page.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 38 KiB |
BIN
scripts/unifi/screenshots/1768382751177-03-after-login.png
Normal file
|
After Width: | Height: | Size: 162 KiB |