Homelab: [homelab-agents] - Session 2025-11-30
Added NetBox DHCP pool configuration automation scripts. Created tools to reserve DHCP pool IP ranges (.100-.150) via NetBox API, preventing them from showing as "available" for static assignment. Changes: - Created sys-apps-netbox-dhcp-setup.py: Creates IP Range objects for DHCP pools - Created sys-apps-netbox-reserve-dhcp-ips.py: Reserves individual IPs as "Reserved" status - Updated sys-apps-session-summary.md with comprehensive session documentation - Updated CLAUDE.md with session 2025-11-30 history entry - Configured 255 IPs across 5 VLANs (51 IPs per /24 subnet) - Established DHCP pool standard: .100-.150 for all networks Repository: http://100.120.125.113:3000/pdm/homelab-agents Next Session Focus: Document NetBox token management and create dhcp-pool tag 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
158
sys-apps-netbox-reserve-dhcp-ips.py
Executable file
158
sys-apps-netbox-reserve-dhcp-ips.py
Executable file
@@ -0,0 +1,158 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user