Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:00:18 +08:00
commit 765529cd13
69 changed files with 18291 additions and 0 deletions

View File

@@ -0,0 +1,358 @@
#!/usr/bin/env -S uv run --script --quiet
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "pynetbox>=7.0.0",
# "infisical-python>=2.3.3",
# "rich>=13.0.0",
# ]
# ///
"""
NetBox API Client Example
Purpose: api-client-example
Team: infrastructure
Author: devops@spaceships.work
Demonstrates best practices for API client scripts using uv:
- PEP 723 inline dependencies
- Infisical for secrets management
- Error handling and validation
- Rich output formatting
- Type hints and documentation
This is a production-ready example showing all patterns together.
Usage:
# List Matrix cluster VMs
./netbox_client.py
# Query specific VM
./netbox_client.py --vm docker-01
Example output:
Matrix Cluster Virtual Machines
┌────┬───────────┬──────┬─────────┬──────────────┬──────────────────────────────────┐
│ ID │ Name │vCPUs │Memory MB│ Status │ Primary IP │
├────┼───────────┼──────┼─────────┼──────────────┼──────────────────────────────────┤
│ 1 │ docker-01 │ 4 │ 8192 │ active │ 192.168.3.10/24 │
│ 2 │ k8s-01 │ 8 │ 16384 │ active │ 192.168.3.20/24 │
└────┴───────────┴──────┴─────────┴──────────────┴──────────────────────────────────┘
"""
import re
import sys
from typing import Optional
from dataclasses import dataclass
import pynetbox
from infisical import InfisicalClient
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
console = Console()
# ============================================================================
# Configuration
# ============================================================================
@dataclass
class NetBoxConfig:
"""NetBox connection configuration."""
url: str = "https://netbox.spaceships.work"
project_id: str = "7b832220-24c0-45bc-a5f1-ce9794a31259"
environment: str = "prod"
path: str = "/matrix"
# ============================================================================
# Authentication
# ============================================================================
def get_netbox_client(config: NetBoxConfig) -> Optional[pynetbox.api]:
"""
Get authenticated NetBox API client.
Uses Infisical to securely retrieve API token (Virgo-Core security pattern).
Args:
config: NetBox configuration
Returns:
Authenticated pynetbox client or None on error
Raises:
Exception: If token cannot be retrieved or connection fails
"""
try:
# Get token from Infisical (never hardcoded)
client = InfisicalClient()
token = client.get_secret(
secret_name="NETBOX_API_TOKEN",
project_id=config.project_id,
environment=config.environment,
path=config.path
).secret_value
if not token:
raise ValueError("NETBOX_API_TOKEN is empty")
# Connect to NetBox
nb = pynetbox.api(config.url, token=token)
# Test connection
_ = nb.status()
return nb
except Exception as e:
console.print(f"[red]Failed to connect to NetBox: {e}[/red]")
return None
# ============================================================================
# Data Validation
# ============================================================================
def validate_vm_name(name: str) -> bool:
"""
Validate VM name format.
Pattern: lowercase letters, numbers, hyphens only
Example: docker-01, k8s-01-master
Args:
name: VM name to validate
Returns:
True if valid, False otherwise
"""
pattern = r'^[a-z0-9-]+$'
return bool(re.match(pattern, name))
# ============================================================================
# NetBox Queries
# ============================================================================
def get_cluster_vms(nb: pynetbox.api, cluster_name: str = "Matrix") -> list:
"""
Get all VMs in a cluster.
Args:
nb: NetBox API client
cluster_name: Cluster name to query
Returns:
List of VM objects
"""
try:
vms = nb.virtualization.virtual_machines.filter(cluster=cluster_name.lower())
return list(vms)
except Exception as e:
console.print(f"[red]Error querying VMs: {e}[/red]")
return []
def get_vm_details(nb: pynetbox.api, vm_name: str) -> Optional[dict]:
"""
Get detailed information about a VM.
Args:
nb: NetBox API client
vm_name: VM name
Returns:
VM details dict or None if not found
"""
try:
# Validate name
if not validate_vm_name(vm_name):
console.print(f"[red]Invalid VM name format: {vm_name}[/red]")
return None
vm = nb.virtualization.virtual_machines.get(name=vm_name)
if not vm:
console.print(f"[yellow]VM '{vm_name}' not found[/yellow]")
return None
# Get interfaces
interfaces = nb.virtualization.interfaces.filter(virtual_machine_id=vm.id)
# Get IPs
ips = []
for iface in interfaces:
iface_ips = nb.ipam.ip_addresses.filter(vminterface_id=iface.id)
ips.extend(iface_ips)
return {
"vm": vm,
"interfaces": list(interfaces),
"ips": ips
}
except Exception as e:
console.print(f"[red]Error getting VM details: {e}[/red]")
return None
# ============================================================================
# Output Formatting
# ============================================================================
def display_vms_table(vms: list) -> None:
"""
Display VMs in a formatted table.
Args:
vms: List of VM objects
"""
if not vms:
console.print("[yellow]No VMs found[/yellow]")
return
table = Table(title="Matrix Cluster Virtual Machines")
table.add_column("ID", style="cyan", justify="right")
table.add_column("Name", style="green")
table.add_column("vCPUs", justify="right")
table.add_column("Memory (MB)", justify="right")
table.add_column("Status")
table.add_column("Primary IP", style="yellow")
for vm in vms:
table.add_row(
str(vm.id),
vm.name,
str(vm.vcpus) if vm.vcpus else "N/A",
str(vm.memory) if vm.memory else "N/A",
vm.status.value if hasattr(vm.status, 'value') else str(vm.status),
str(vm.primary_ip4.address) if vm.primary_ip4 else "N/A"
)
console.print(table)
console.print(f"\n[green]Total VMs: {len(vms)}[/green]")
def display_vm_details(details: dict) -> None:
"""
Display detailed VM information.
Args:
details: VM details dict from get_vm_details()
"""
vm = details["vm"]
interfaces = details["interfaces"]
ips = details["ips"]
# VM info panel
info = (
f"[green]ID:[/green] {vm.id}\n"
f"[green]Name:[/green] {vm.name}\n"
f"[green]Cluster:[/green] {vm.cluster.name if vm.cluster else 'N/A'}\n"
f"[green]Status:[/green] {vm.status}\n"
f"[green]vCPUs:[/green] {vm.vcpus or 'N/A'}\n"
f"[green]Memory:[/green] {vm.memory or 'N/A'} MB\n"
f"[green]Disk:[/green] {vm.disk or 'N/A'} GB\n"
f"[green]Primary IP:[/green] {vm.primary_ip4.address if vm.primary_ip4 else 'N/A'}"
)
console.print(Panel(info, title=f"VM: {vm.name}", border_style="cyan"))
# Interfaces table
if interfaces:
iface_table = Table(title="Network Interfaces")
iface_table.add_column("Name", style="cyan")
iface_table.add_column("Type")
iface_table.add_column("Enabled")
iface_table.add_column("MTU")
for iface in interfaces:
iface_table.add_row(
iface.name,
iface.type.value if hasattr(iface.type, 'value') else str(iface.type),
"" if iface.enabled else "",
str(iface.mtu) if iface.mtu else "default"
)
console.print("\n", iface_table)
# IPs table
if ips:
ip_table = Table(title="IP Addresses")
ip_table.add_column("Address", style="yellow")
ip_table.add_column("DNS Name", style="green")
ip_table.add_column("Status")
for ip in ips:
ip_table.add_row(
str(ip.address),
ip.dns_name or "",
ip.status.value if hasattr(ip.status, 'value') else str(ip.status)
)
console.print("\n", ip_table)
# ============================================================================
# Main
# ============================================================================
def main(vm_name: Optional[str] = None) -> int:
"""
Main entry point.
Args:
vm_name: Optional specific VM to query
Returns:
Exit code (0 = success, 1 = error)
"""
config = NetBoxConfig()
# Get NetBox client
nb = get_netbox_client(config)
if not nb:
return 1
try:
if vm_name:
# Query specific VM
details = get_vm_details(nb, vm_name)
if details:
display_vm_details(details)
return 0
return 1
else:
# List all VMs in Matrix cluster
vms = get_cluster_vms(nb, cluster_name="Matrix")
display_vms_table(vms)
return 0
except KeyboardInterrupt:
console.print("\n[yellow]Interrupted by user[/yellow]")
return 1
except Exception as e:
console.print(f"[red]Unexpected error: {e}[/red]")
return 1
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="NetBox API client example",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
parser.add_argument(
"--vm",
help="Specific VM to query (default: list all)"
)
args = parser.parse_args()
sys.exit(main(vm_name=args.vm))