Move NetBox scripts to system-apps repository
Moved NetBox DHCP automation scripts from homelab-agents to system-apps: - sys-apps-netbox-dhcp-setup.py → system-apps repo - sys-apps-netbox-reserve-dhcp-ips.py → system-apps repo Rationale: - homelab-agents = shared Claude Code agents - system-apps = VPS-specific configuration tools - NetBox scripts are VPS infrastructure, not shared agents Updated sys-apps-session-summary.md to reflect new location. Scripts now at: http://100.120.125.113:3000/pdm/system-apps 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,144 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
NetBox DHCP Pool Setup Script
|
|
||||||
Marks IPs .100-.150 in each subnet as DHCP pool range
|
|
||||||
"""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import ipaddress
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
NETBOX_URL = "https://netbox.pdmarf.co.uk"
|
|
||||||
NETBOX_TOKEN = os.environ.get("NETBOX_TOKEN") or sys.argv[1] if len(sys.argv) > 1 else None
|
|
||||||
|
|
||||||
if not NETBOX_TOKEN:
|
|
||||||
print("Error: NetBox API token required")
|
|
||||||
print("Usage: NETBOX_TOKEN='your-token' python3 sys-apps-netbox-dhcp-setup.py")
|
|
||||||
print(" or: python3 sys-apps-netbox-dhcp-setup.py 'your-token'")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# API headers
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Token {NETBOX_TOKEN}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_all_prefixes():
|
|
||||||
"""Get all IPv4 prefixes from NetBox"""
|
|
||||||
url = f"{NETBOX_URL}/api/ipam/prefixes/"
|
|
||||||
params = {"family": 4, "limit": 1000} # IPv4 only
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(url, headers=headers, params=params, verify=True)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()["results"]
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print(f"Error fetching prefixes: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def create_dhcp_range(prefix_str, prefix_id):
|
|
||||||
"""Create DHCP pool IP range (.100-.150) for a prefix"""
|
|
||||||
try:
|
|
||||||
network = ipaddress.ip_network(prefix_str, strict=False)
|
|
||||||
|
|
||||||
# Calculate .100 and .150 addresses
|
|
||||||
# For /24: x.x.x.100 and x.x.x.150
|
|
||||||
# For other sizes, adjust accordingly
|
|
||||||
|
|
||||||
base_ip = str(network.network_address)
|
|
||||||
octets = base_ip.split('.')
|
|
||||||
|
|
||||||
# Start: .100
|
|
||||||
start_octets = octets[:-1] + ['100']
|
|
||||||
start_ip = '.'.join(start_octets)
|
|
||||||
|
|
||||||
# End: .150
|
|
||||||
end_octets = octets[:-1] + ['150']
|
|
||||||
end_ip = '.'.join(end_octets)
|
|
||||||
|
|
||||||
# Validate IPs are within the network
|
|
||||||
start_addr = ipaddress.ip_address(start_ip)
|
|
||||||
end_addr = ipaddress.ip_address(end_ip)
|
|
||||||
|
|
||||||
if start_addr not in network or end_addr not in network:
|
|
||||||
print(f" ⚠️ Skipping {prefix_str}: DHCP range (.100-.150) not within network")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Create IP Range via API
|
|
||||||
url = f"{NETBOX_URL}/api/ipam/ip-ranges/"
|
|
||||||
data = {
|
|
||||||
"start_address": start_ip,
|
|
||||||
"end_address": end_ip,
|
|
||||||
"status": "active",
|
|
||||||
"description": "DHCP Pool - Dynamic Assignment Range (.100-.150)",
|
|
||||||
"comments": "Auto-created: IPs reserved for DHCP dynamic assignment"
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(url, headers=headers, json=data, verify=True)
|
|
||||||
|
|
||||||
if response.status_code == 201:
|
|
||||||
print(f" ✓ Created DHCP range: {start_ip} - {end_ip}")
|
|
||||||
return True
|
|
||||||
elif response.status_code == 400:
|
|
||||||
error_detail = response.json()
|
|
||||||
if "already exists" in str(error_detail).lower() or "overlap" in str(error_detail).lower():
|
|
||||||
print(f" ℹ️ Range already exists: {start_ip} - {end_ip}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f" ✗ Failed: {error_detail}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
print(f" ✗ HTTP {response.status_code}: {response.text}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Error processing {prefix_str}: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print(f"\nConnecting to NetBox: {NETBOX_URL}")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Get all prefixes
|
|
||||||
print("\nFetching prefixes...")
|
|
||||||
prefixes = get_all_prefixes()
|
|
||||||
print(f"Found {len(prefixes)} prefixes\n")
|
|
||||||
|
|
||||||
# Process each prefix
|
|
||||||
success_count = 0
|
|
||||||
skip_count = 0
|
|
||||||
fail_count = 0
|
|
||||||
|
|
||||||
for prefix in prefixes:
|
|
||||||
prefix_str = prefix["prefix"]
|
|
||||||
prefix_id = prefix["id"]
|
|
||||||
vlan_name = prefix.get("vlan", {}).get("name", "No VLAN") if prefix.get("vlan") else "No VLAN"
|
|
||||||
|
|
||||||
print(f"Processing: {prefix_str} ({vlan_name})")
|
|
||||||
|
|
||||||
result = create_dhcp_range(prefix_str, prefix_id)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
success_count += 1
|
|
||||||
elif result is None:
|
|
||||||
skip_count += 1
|
|
||||||
else:
|
|
||||||
fail_count += 1
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("Summary:")
|
|
||||||
print(f" ✓ Created/Exists: {success_count}")
|
|
||||||
print(f" ⚠️ Skipped: {skip_count}")
|
|
||||||
print(f" ✗ Failed: {fail_count}")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
print("\n✓ DHCP pool ranges configured!")
|
|
||||||
print(" IPs .100-.150 are now marked as DHCP pool in NetBox")
|
|
||||||
print(f" View at: {NETBOX_URL}/ipam/ip-ranges/")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
NetBox DHCP IP Reservation Script
|
|
||||||
Marks individual IPs .100-.150 in each subnet as reserved for DHCP
|
|
||||||
"""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import ipaddress
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
NETBOX_URL = "https://netbox.pdmarf.co.uk"
|
|
||||||
NETBOX_TOKEN = os.environ.get("NETBOX_TOKEN") or sys.argv[1] if len(sys.argv) > 1 else None
|
|
||||||
|
|
||||||
if not NETBOX_TOKEN:
|
|
||||||
print("Error: NetBox API token required")
|
|
||||||
print("Usage: NETBOX_TOKEN='your-token' python3 sys-apps-netbox-reserve-dhcp-ips.py")
|
|
||||||
print(" or: python3 sys-apps-netbox-reserve-dhcp-ips.py 'your-token'")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# API headers
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Token {NETBOX_TOKEN}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_all_prefixes():
|
|
||||||
"""Get all IPv4 prefixes from NetBox"""
|
|
||||||
url = f"{NETBOX_URL}/api/ipam/prefixes/"
|
|
||||||
params = {"family": 4, "limit": 1000}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(url, headers=headers, params=params, verify=True)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()["results"]
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print(f"Error fetching prefixes: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def create_dhcp_ip(ip_address, prefix_id):
|
|
||||||
"""Create or update individual IP as DHCP reserved"""
|
|
||||||
url = f"{NETBOX_URL}/api/ipam/ip-addresses/"
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"address": f"{ip_address}/24", # CIDR notation
|
|
||||||
"status": "reserved",
|
|
||||||
"dns_name": "",
|
|
||||||
"description": "DHCP Pool - Reserved for dynamic assignment"
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(url, headers=headers, json=data, verify=True)
|
|
||||||
|
|
||||||
if response.status_code == 201:
|
|
||||||
return "created"
|
|
||||||
elif response.status_code == 400:
|
|
||||||
error_detail = response.json()
|
|
||||||
if "already exists" in str(error_detail).lower() or "duplicate" in str(error_detail).lower():
|
|
||||||
return "exists"
|
|
||||||
else:
|
|
||||||
return f"error: {error_detail}"
|
|
||||||
else:
|
|
||||||
return f"error: HTTP {response.status_code}"
|
|
||||||
except Exception as e:
|
|
||||||
return f"error: {e}"
|
|
||||||
|
|
||||||
def reserve_dhcp_range(prefix_str, prefix_id):
|
|
||||||
"""Reserve IPs .100-.150 in a prefix"""
|
|
||||||
try:
|
|
||||||
network = ipaddress.ip_network(prefix_str, strict=False)
|
|
||||||
|
|
||||||
# Calculate .100 and .150 addresses
|
|
||||||
base_ip = str(network.network_address)
|
|
||||||
octets = base_ip.split('.')
|
|
||||||
|
|
||||||
created = 0
|
|
||||||
exists = 0
|
|
||||||
errors = 0
|
|
||||||
|
|
||||||
# Create IPs from .100 to .150
|
|
||||||
for i in range(100, 151):
|
|
||||||
ip_octets = octets[:-1] + [str(i)]
|
|
||||||
ip_address = '.'.join(ip_octets)
|
|
||||||
|
|
||||||
# Validate IP is within network
|
|
||||||
ip_addr = ipaddress.ip_address(ip_address)
|
|
||||||
if ip_addr not in network:
|
|
||||||
continue
|
|
||||||
|
|
||||||
result = create_dhcp_ip(ip_address, prefix_id)
|
|
||||||
|
|
||||||
if result == "created":
|
|
||||||
created += 1
|
|
||||||
if created % 10 == 0: # Progress indicator
|
|
||||||
print(f" ... {created} IPs created so far")
|
|
||||||
elif result == "exists":
|
|
||||||
exists += 1
|
|
||||||
else:
|
|
||||||
errors += 1
|
|
||||||
if errors <= 3: # Show first 3 errors only
|
|
||||||
print(f" ✗ {ip_address}: {result}")
|
|
||||||
|
|
||||||
return created, exists, errors
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Error processing {prefix_str}: {e}")
|
|
||||||
return 0, 0, 0
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print(f"\nConnecting to NetBox: {NETBOX_URL}")
|
|
||||||
print("=" * 60)
|
|
||||||
print("This will create individual IP objects for .100-.150 in each subnet")
|
|
||||||
print("Status: Reserved | Description: DHCP Pool")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Get all prefixes
|
|
||||||
print("\nFetching prefixes...")
|
|
||||||
prefixes = get_all_prefixes()
|
|
||||||
|
|
||||||
# Filter to /24 subnets only (skip /16)
|
|
||||||
prefixes = [p for p in prefixes if '/24' in p['prefix']]
|
|
||||||
print(f"Found {len(prefixes)} /24 prefixes\n")
|
|
||||||
|
|
||||||
# Process each prefix
|
|
||||||
total_created = 0
|
|
||||||
total_exists = 0
|
|
||||||
total_errors = 0
|
|
||||||
|
|
||||||
for prefix in prefixes:
|
|
||||||
prefix_str = prefix["prefix"]
|
|
||||||
prefix_id = prefix["id"]
|
|
||||||
vlan_name = prefix.get("vlan", {}).get("name", "No VLAN") if prefix.get("vlan") else "No VLAN"
|
|
||||||
|
|
||||||
print(f"Processing: {prefix_str} ({vlan_name})")
|
|
||||||
created, exists, errors = reserve_dhcp_range(prefix_str, prefix_id)
|
|
||||||
|
|
||||||
print(f" ✓ Created: {created}, Already exists: {exists}, Errors: {errors}")
|
|
||||||
|
|
||||||
total_created += created
|
|
||||||
total_exists += exists
|
|
||||||
total_errors += errors
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("Summary:")
|
|
||||||
print(f" ✓ Created: {total_created}")
|
|
||||||
print(f" ℹ️ Already existed: {total_exists}")
|
|
||||||
print(f" ✗ Errors: {total_errors}")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
print("\n✓ DHCP pool IPs reserved in NetBox!")
|
|
||||||
print(" IPs .100-.150 are now marked as 'Reserved' status")
|
|
||||||
print(" They will no longer show as 'Available' in the IP list")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -105,11 +105,16 @@ First run failed because script tried to create tag "dhcp-pool" which didn't exi
|
|||||||
|
|
||||||
## Files Created
|
## Files Created
|
||||||
|
|
||||||
|
**Scripts moved to system-apps repository:**
|
||||||
```
|
```
|
||||||
/home/pdm/.homelab-agents/sys-apps-netbox-dhcp-setup.py
|
http://100.120.125.113:3000/pdm/system-apps
|
||||||
/home/pdm/.homelab-agents/sys-apps-netbox-reserve-dhcp-ips.py
|
├── sys-apps-netbox-dhcp-setup.py
|
||||||
|
├── sys-apps-netbox-reserve-dhcp-ips.py
|
||||||
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** Scripts were initially created in homelab-agents but moved to system-apps repository as they are VPS-specific configuration tools, not shared agents.
|
||||||
|
|
||||||
**Both scripts:**
|
**Both scripts:**
|
||||||
- Follow naming convention: `{hostname}-{description}.py`
|
- Follow naming convention: `{hostname}-{description}.py`
|
||||||
- Are executable (`chmod +x`)
|
- Are executable (`chmod +x`)
|
||||||
@@ -134,18 +139,22 @@ This provides:
|
|||||||
When adding a new VLAN, run both scripts to configure DHCP pool:
|
When adding a new VLAN, run both scripts to configure DHCP pool:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Clone system-apps repository (if not already cloned)
|
||||||
|
git clone git@100.120.125.113:pdm/system-apps.git ~/system-apps
|
||||||
|
cd ~/system-apps
|
||||||
|
|
||||||
# 1. Create IP Range
|
# 1. Create IP Range
|
||||||
python3 /home/pdm/.homelab-agents/sys-apps-netbox-dhcp-setup.py 'YOUR_API_TOKEN'
|
python3 sys-apps-netbox-dhcp-setup.py 'YOUR_API_TOKEN'
|
||||||
|
|
||||||
# 2. Reserve Individual IPs
|
# 2. Reserve Individual IPs
|
||||||
python3 /home/pdm/.homelab-agents/sys-apps-netbox-reserve-dhcp-ips.py 'YOUR_API_TOKEN'
|
python3 sys-apps-netbox-reserve-dhcp-ips.py 'YOUR_API_TOKEN'
|
||||||
```
|
```
|
||||||
|
|
||||||
Or set environment variable once:
|
Or set environment variable once:
|
||||||
```bash
|
```bash
|
||||||
export NETBOX_TOKEN='YOUR_API_TOKEN'
|
export NETBOX_TOKEN='YOUR_API_TOKEN'
|
||||||
python3 /home/pdm/.homelab-agents/sys-apps-netbox-dhcp-setup.py
|
python3 sys-apps-netbox-dhcp-setup.py
|
||||||
python3 /home/pdm/.homelab-agents/sys-apps-netbox-reserve-dhcp-ips.py
|
python3 sys-apps-netbox-reserve-dhcp-ips.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Decisions
|
## Key Decisions
|
||||||
@@ -163,11 +172,12 @@ NetBox standard status values:
|
|||||||
- **Reserved is correct** for DHCP pool (allocated to DHCP server, not assigned to specific device)
|
- **Reserved is correct** for DHCP pool (allocated to DHCP server, not assigned to specific device)
|
||||||
|
|
||||||
### Script Location
|
### Script Location
|
||||||
Placed in `~/.homelab-agents/` because:
|
**Moved to `system-apps` repository** because:
|
||||||
- Infrastructure/automation tools
|
- VPS-specific configuration tools (not shared agents)
|
||||||
- Can be reused across VPS instances
|
- Belongs with other sys-apps infrastructure code
|
||||||
- Follows established pattern (agents, scripts, tools live here)
|
- `homelab-agents` = shared Claude Code agents only
|
||||||
- Prefixed with `sys-apps-` per naming convention
|
- `system-apps` = VPS-specific automation and config
|
||||||
|
- Makes more sense in project-specific repo vs shared agent repo
|
||||||
|
|
||||||
## Next Session Recommendations
|
## Next Session Recommendations
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user