Initial commit: Vultr API v2 Python wrapper with FastAPI server

- vultr_api/: Python library wrapping Vultr API v2
  - 17 resource modules (instances, dns, firewall, vpc, etc.)
  - Pagination support, error handling

- server/: FastAPI REST server
  - All API endpoints exposed via HTTP
  - X-API-Key header authentication
  - Swagger docs at /docs

- Podman quadlet config for systemd deployment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
HWANG BYUNGHA
2026-01-22 01:08:17 +09:00
commit 184054c6c1
48 changed files with 6058 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
"""API Routers"""
from . import (
account,
instances,
dns,
firewall,
ssh_keys,
startup_scripts,
snapshots,
block_storage,
reserved_ips,
vpc,
load_balancers,
bare_metal,
backups,
plans,
regions,
os_api,
)
__all__ = [
"account",
"instances",
"dns",
"firewall",
"ssh_keys",
"startup_scripts",
"snapshots",
"block_storage",
"reserved_ips",
"vpc",
"load_balancers",
"bare_metal",
"backups",
"plans",
"regions",
"os_api",
]

38
server/routers/account.py Normal file
View File

@@ -0,0 +1,38 @@
"""Account router"""
from fastapi import APIRouter, Depends
from typing import List
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
@router.get("")
async def get_account(client: VultrClient = Depends(get_client)):
"""Get account information"""
return client.account.get()
@router.get("/acl")
async def get_acl(client: VultrClient = Depends(get_client)):
"""Get account ACL (IP access control list)"""
return client.account.get_acl()
@router.put("/acl")
async def set_acl(acls: List[str], client: VultrClient = Depends(get_client)):
"""Set account ACL"""
return client.account.set_acl(acls)
@router.post("/acl")
async def add_acl(ip: str, client: VultrClient = Depends(get_client)):
"""Add IP to ACL"""
return client.account.add_acl(ip)
@router.delete("/acl/{ip}")
async def remove_acl(ip: str, client: VultrClient = Depends(get_client)):
"""Remove IP from ACL"""
return client.account.remove_acl(ip)

34
server/routers/backups.py Normal file
View File

@@ -0,0 +1,34 @@
"""Backups router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
@router.get("")
async def list_backups(
instance_id: Optional[str] = None,
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all backups"""
return client.backups.list(instance_id=instance_id, per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_backups(
instance_id: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all backups (auto-paginated)"""
return {"backups": client.backups.list_all(instance_id=instance_id)}
@router.get("/{backup_id}")
async def get_backup(backup_id: str, client: VultrClient = Depends(get_client)):
"""Get backup details"""
return client.backups.get(backup_id)

View File

@@ -0,0 +1,118 @@
"""Bare Metal router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional, List
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateBareMetalRequest(BaseModel):
region: str
plan: str
os_id: Optional[int] = None
snapshot_id: Optional[str] = None
app_id: Optional[int] = None
image_id: Optional[str] = None
script_id: Optional[str] = None
ssh_key_ids: Optional[List[str]] = None
enable_ipv6: Optional[bool] = None
hostname: Optional[str] = None
label: Optional[str] = None
tags: Optional[List[str]] = None
class UpdateBareMetalRequest(BaseModel):
label: Optional[str] = None
tags: Optional[List[str]] = None
enable_ipv6: Optional[bool] = None
@router.get("")
async def list_bare_metal(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all bare metal servers"""
return client.bare_metal.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_bare_metal(client: VultrClient = Depends(get_client)):
"""List all bare metal servers (auto-paginated)"""
return {"bare_metals": client.bare_metal.list_all()}
@router.get("/{baremetal_id}")
async def get_bare_metal(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Get bare metal server details"""
return client.bare_metal.get(baremetal_id)
@router.post("")
async def create_bare_metal(req: CreateBareMetalRequest, client: VultrClient = Depends(get_client)):
"""Create a bare metal server"""
return client.bare_metal.create(**req.model_dump(exclude_none=True))
@router.patch("/{baremetal_id}")
async def update_bare_metal(baremetal_id: str, req: UpdateBareMetalRequest, client: VultrClient = Depends(get_client)):
"""Update a bare metal server"""
return client.bare_metal.update(baremetal_id, **req.model_dump(exclude_none=True))
@router.delete("/{baremetal_id}")
async def delete_bare_metal(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Delete a bare metal server"""
return client.bare_metal.delete(baremetal_id)
@router.post("/{baremetal_id}/start")
async def start_bare_metal(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Start a bare metal server"""
return client.bare_metal.start(baremetal_id)
@router.post("/{baremetal_id}/halt")
async def halt_bare_metal(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Halt a bare metal server"""
return client.bare_metal.halt(baremetal_id)
@router.post("/{baremetal_id}/reboot")
async def reboot_bare_metal(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Reboot a bare metal server"""
return client.bare_metal.reboot(baremetal_id)
@router.post("/{baremetal_id}/reinstall")
async def reinstall_bare_metal(baremetal_id: str, hostname: Optional[str] = None, client: VultrClient = Depends(get_client)):
"""Reinstall a bare metal server"""
return client.bare_metal.reinstall(baremetal_id, hostname=hostname)
@router.get("/{baremetal_id}/ipv4")
async def list_ipv4(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""List IPv4 addresses for a bare metal server"""
return client.bare_metal.list_ipv4(baremetal_id)
@router.get("/{baremetal_id}/ipv6")
async def list_ipv6(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""List IPv6 addresses for a bare metal server"""
return client.bare_metal.list_ipv6(baremetal_id)
@router.get("/{baremetal_id}/bandwidth")
async def get_bandwidth(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Get bandwidth usage for a bare metal server"""
return client.bare_metal.bandwidth(baremetal_id)
@router.get("/{baremetal_id}/user-data")
async def get_user_data(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Get user data for a bare metal server"""
return client.bare_metal.get_user_data(baremetal_id)

View File

@@ -0,0 +1,78 @@
"""Block Storage router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateBlockStorageRequest(BaseModel):
region: str
size_gb: int
label: Optional[str] = None
block_type: Optional[str] = None
class UpdateBlockStorageRequest(BaseModel):
size_gb: Optional[int] = None
label: Optional[str] = None
class AttachBlockStorageRequest(BaseModel):
instance_id: str
live: Optional[bool] = True
@router.get("")
async def list_block_storage(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all block storage volumes"""
return client.block_storage.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_block_storage(client: VultrClient = Depends(get_client)):
"""List all block storage volumes (auto-paginated)"""
return {"blocks": client.block_storage.list_all()}
@router.get("/{block_id}")
async def get_block_storage(block_id: str, client: VultrClient = Depends(get_client)):
"""Get block storage details"""
return client.block_storage.get(block_id)
@router.post("")
async def create_block_storage(req: CreateBlockStorageRequest, client: VultrClient = Depends(get_client)):
"""Create a block storage volume"""
return client.block_storage.create(**req.model_dump(exclude_none=True))
@router.patch("/{block_id}")
async def update_block_storage(block_id: str, req: UpdateBlockStorageRequest, client: VultrClient = Depends(get_client)):
"""Update a block storage volume"""
return client.block_storage.update(block_id, **req.model_dump(exclude_none=True))
@router.delete("/{block_id}")
async def delete_block_storage(block_id: str, client: VultrClient = Depends(get_client)):
"""Delete a block storage volume"""
return client.block_storage.delete(block_id)
@router.post("/{block_id}/attach")
async def attach_block_storage(block_id: str, req: AttachBlockStorageRequest, client: VultrClient = Depends(get_client)):
"""Attach block storage to an instance"""
return client.block_storage.attach(block_id, instance_id=req.instance_id, live=req.live)
@router.post("/{block_id}/detach")
async def detach_block_storage(block_id: str, live: bool = True, client: VultrClient = Depends(get_client)):
"""Detach block storage from an instance"""
return client.block_storage.detach(block_id, live=live)

155
server/routers/dns.py Normal file
View File

@@ -0,0 +1,155 @@
"""DNS router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateDomainRequest(BaseModel):
domain: str
ip: Optional[str] = None
class CreateRecordRequest(BaseModel):
type: str
name: str
data: str
ttl: Optional[int] = 300
priority: Optional[int] = None
class UpdateRecordRequest(BaseModel):
name: Optional[str] = None
data: Optional[str] = None
ttl: Optional[int] = None
priority: Optional[int] = None
@router.get("/domains")
async def list_domains(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all DNS domains"""
return client.dns.list_domains(per_page=per_page, cursor=cursor)
@router.get("/domains/all")
async def list_all_domains(client: VultrClient = Depends(get_client)):
"""List all DNS domains (auto-paginated)"""
return {"domains": client.dns.list_all_domains()}
@router.get("/domains/{domain}")
async def get_domain(domain: str, client: VultrClient = Depends(get_client)):
"""Get DNS domain details"""
return client.dns.get_domain(domain)
@router.post("/domains")
async def create_domain(req: CreateDomainRequest, client: VultrClient = Depends(get_client)):
"""Create a DNS domain"""
return client.dns.create_domain(req.domain, ip=req.ip)
@router.delete("/domains/{domain}")
async def delete_domain(domain: str, client: VultrClient = Depends(get_client)):
"""Delete a DNS domain"""
return client.dns.delete_domain(domain)
@router.get("/domains/{domain}/soa")
async def get_soa(domain: str, client: VultrClient = Depends(get_client)):
"""Get SOA record for a domain"""
return client.dns.get_soa(domain)
@router.get("/domains/{domain}/dnssec")
async def get_dnssec(domain: str, client: VultrClient = Depends(get_client)):
"""Get DNSSEC info for a domain"""
return client.dns.get_dnssec(domain)
# Records
@router.get("/domains/{domain}/records")
async def list_records(
domain: str,
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List DNS records for a domain"""
return client.dns.list_records(domain, per_page=per_page, cursor=cursor)
@router.get("/domains/{domain}/records/all")
async def list_all_records(domain: str, client: VultrClient = Depends(get_client)):
"""List all DNS records (auto-paginated)"""
return {"records": client.dns.list_all_records(domain)}
@router.get("/domains/{domain}/records/{record_id}")
async def get_record(domain: str, record_id: str, client: VultrClient = Depends(get_client)):
"""Get a DNS record"""
return client.dns.get_record(domain, record_id)
@router.post("/domains/{domain}/records")
async def create_record(domain: str, req: CreateRecordRequest, client: VultrClient = Depends(get_client)):
"""Create a DNS record"""
return client.dns.create_record(
domain,
type=req.type,
name=req.name,
data=req.data,
ttl=req.ttl,
priority=req.priority
)
@router.patch("/domains/{domain}/records/{record_id}")
async def update_record(domain: str, record_id: str, req: UpdateRecordRequest, client: VultrClient = Depends(get_client)):
"""Update a DNS record"""
return client.dns.update_record(domain, record_id, **req.model_dump(exclude_none=True))
@router.delete("/domains/{domain}/records/{record_id}")
async def delete_record(domain: str, record_id: str, client: VultrClient = Depends(get_client)):
"""Delete a DNS record"""
return client.dns.delete_record(domain, record_id)
# Convenience endpoints
@router.post("/domains/{domain}/records/a")
async def create_a_record(domain: str, name: str, ip: str, ttl: int = 300, client: VultrClient = Depends(get_client)):
"""Create an A record"""
return client.dns.create_a_record(domain, name, ip, ttl)
@router.post("/domains/{domain}/records/aaaa")
async def create_aaaa_record(domain: str, name: str, ip: str, ttl: int = 300, client: VultrClient = Depends(get_client)):
"""Create an AAAA record"""
return client.dns.create_aaaa_record(domain, name, ip, ttl)
@router.post("/domains/{domain}/records/cname")
async def create_cname_record(domain: str, name: str, target: str, ttl: int = 300, client: VultrClient = Depends(get_client)):
"""Create a CNAME record"""
return client.dns.create_cname_record(domain, name, target, ttl)
@router.post("/domains/{domain}/records/txt")
async def create_txt_record(domain: str, name: str, data: str, ttl: int = 300, client: VultrClient = Depends(get_client)):
"""Create a TXT record"""
return client.dns.create_txt_record(domain, name, data, ttl)
@router.post("/domains/{domain}/records/mx")
async def create_mx_record(domain: str, name: str, data: str, priority: int = 10, ttl: int = 300, client: VultrClient = Depends(get_client)):
"""Create an MX record"""
return client.dns.create_mx_record(domain, name, data, priority, ttl)

124
server/routers/firewall.py Normal file
View File

@@ -0,0 +1,124 @@
"""Firewall router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateGroupRequest(BaseModel):
description: Optional[str] = None
class CreateRuleRequest(BaseModel):
ip_type: str # v4 or v6
protocol: str # tcp, udp, icmp, gre, esp, ah
subnet: str
subnet_size: int
port: Optional[str] = None
source: Optional[str] = None
notes: Optional[str] = None
@router.get("/groups")
async def list_groups(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all firewall groups"""
return client.firewall.list_groups(per_page=per_page, cursor=cursor)
@router.get("/groups/all")
async def list_all_groups(client: VultrClient = Depends(get_client)):
"""List all firewall groups (auto-paginated)"""
return {"firewall_groups": client.firewall.list_all_groups()}
@router.get("/groups/{group_id}")
async def get_group(group_id: str, client: VultrClient = Depends(get_client)):
"""Get firewall group details"""
return client.firewall.get_group(group_id)
@router.post("/groups")
async def create_group(req: CreateGroupRequest, client: VultrClient = Depends(get_client)):
"""Create a firewall group"""
return client.firewall.create_group(description=req.description)
@router.patch("/groups/{group_id}")
async def update_group(group_id: str, description: str, client: VultrClient = Depends(get_client)):
"""Update a firewall group"""
return client.firewall.update_group(group_id, description=description)
@router.delete("/groups/{group_id}")
async def delete_group(group_id: str, client: VultrClient = Depends(get_client)):
"""Delete a firewall group"""
return client.firewall.delete_group(group_id)
# Rules
@router.get("/groups/{group_id}/rules")
async def list_rules(
group_id: str,
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List firewall rules for a group"""
return client.firewall.list_rules(group_id, per_page=per_page, cursor=cursor)
@router.get("/groups/{group_id}/rules/all")
async def list_all_rules(group_id: str, client: VultrClient = Depends(get_client)):
"""List all firewall rules (auto-paginated)"""
return {"firewall_rules": client.firewall.list_all_rules(group_id)}
@router.get("/groups/{group_id}/rules/{rule_id}")
async def get_rule(group_id: str, rule_id: int, client: VultrClient = Depends(get_client)):
"""Get a firewall rule"""
return client.firewall.get_rule(group_id, rule_id)
@router.post("/groups/{group_id}/rules")
async def create_rule(group_id: str, req: CreateRuleRequest, client: VultrClient = Depends(get_client)):
"""Create a firewall rule"""
return client.firewall.create_rule(group_id, **req.model_dump(exclude_none=True))
@router.delete("/groups/{group_id}/rules/{rule_id}")
async def delete_rule(group_id: str, rule_id: int, client: VultrClient = Depends(get_client)):
"""Delete a firewall rule"""
return client.firewall.delete_rule(group_id, rule_id)
# Convenience endpoints
@router.post("/groups/{group_id}/rules/allow-ssh")
async def allow_ssh(group_id: str, source: str = "0.0.0.0/0", client: VultrClient = Depends(get_client)):
"""Allow SSH (port 22) from source"""
return client.firewall.allow_ssh(group_id, source=source)
@router.post("/groups/{group_id}/rules/allow-http")
async def allow_http(group_id: str, source: str = "0.0.0.0/0", client: VultrClient = Depends(get_client)):
"""Allow HTTP (port 80) from source"""
return client.firewall.allow_http(group_id, source=source)
@router.post("/groups/{group_id}/rules/allow-https")
async def allow_https(group_id: str, source: str = "0.0.0.0/0", client: VultrClient = Depends(get_client)):
"""Allow HTTPS (port 443) from source"""
return client.firewall.allow_https(group_id, source=source)
@router.post("/groups/{group_id}/rules/allow-ping")
async def allow_ping(group_id: str, source: str = "0.0.0.0/0", client: VultrClient = Depends(get_client)):
"""Allow ICMP ping from source"""
return client.firewall.allow_ping(group_id, source=source)

151
server/routers/instances.py Normal file
View File

@@ -0,0 +1,151 @@
"""Instances router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional, List
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateInstanceRequest(BaseModel):
region: str
plan: str
os_id: Optional[int] = None
iso_id: Optional[str] = None
snapshot_id: Optional[str] = None
app_id: Optional[int] = None
image_id: Optional[str] = None
script_id: Optional[str] = None
ssh_key_ids: Optional[List[str]] = None
backups: Optional[str] = None
enable_ipv6: Optional[bool] = None
hostname: Optional[str] = None
label: Optional[str] = None
tags: Optional[List[str]] = None
firewall_group_id: Optional[str] = None
vpc_id: Optional[str] = None
class UpdateInstanceRequest(BaseModel):
label: Optional[str] = None
tags: Optional[List[str]] = None
firewall_group_id: Optional[str] = None
enable_ipv6: Optional[bool] = None
backups: Optional[str] = None
@router.get("")
async def list_instances(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all instances"""
return client.instances.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_instances(client: VultrClient = Depends(get_client)):
"""List all instances (auto-paginated)"""
return {"instances": client.instances.list_all()}
@router.get("/{instance_id}")
async def get_instance(instance_id: str, client: VultrClient = Depends(get_client)):
"""Get instance details"""
return client.instances.get(instance_id)
@router.post("")
async def create_instance(req: CreateInstanceRequest, client: VultrClient = Depends(get_client)):
"""Create a new instance"""
return client.instances.create(**req.model_dump(exclude_none=True))
@router.patch("/{instance_id}")
async def update_instance(instance_id: str, req: UpdateInstanceRequest, client: VultrClient = Depends(get_client)):
"""Update an instance"""
return client.instances.update(instance_id, **req.model_dump(exclude_none=True))
@router.delete("/{instance_id}")
async def delete_instance(instance_id: str, client: VultrClient = Depends(get_client)):
"""Delete an instance"""
return client.instances.delete(instance_id)
@router.post("/{instance_id}/start")
async def start_instance(instance_id: str, client: VultrClient = Depends(get_client)):
"""Start an instance"""
return client.instances.start(instance_id)
@router.post("/{instance_id}/stop")
async def stop_instance(instance_id: str, client: VultrClient = Depends(get_client)):
"""Stop an instance (halt)"""
return client.instances.halt(instance_id)
@router.post("/{instance_id}/reboot")
async def reboot_instance(instance_id: str, client: VultrClient = Depends(get_client)):
"""Reboot an instance"""
return client.instances.reboot(instance_id)
@router.post("/{instance_id}/reinstall")
async def reinstall_instance(instance_id: str, hostname: Optional[str] = None, client: VultrClient = Depends(get_client)):
"""Reinstall an instance"""
return client.instances.reinstall(instance_id, hostname=hostname)
@router.get("/{instance_id}/ipv4")
async def list_ipv4(instance_id: str, client: VultrClient = Depends(get_client)):
"""List IPv4 addresses for an instance"""
return client.instances.list_ipv4(instance_id)
@router.get("/{instance_id}/ipv6")
async def list_ipv6(instance_id: str, client: VultrClient = Depends(get_client)):
"""List IPv6 addresses for an instance"""
return client.instances.list_ipv6(instance_id)
@router.get("/{instance_id}/bandwidth")
async def get_bandwidth(instance_id: str, client: VultrClient = Depends(get_client)):
"""Get bandwidth usage for an instance"""
return client.instances.bandwidth(instance_id)
@router.get("/{instance_id}/neighbors")
async def get_neighbors(instance_id: str, client: VultrClient = Depends(get_client)):
"""Get instance neighbors"""
return client.instances.neighbors(instance_id)
@router.get("/{instance_id}/user-data")
async def get_user_data(instance_id: str, client: VultrClient = Depends(get_client)):
"""Get instance user data"""
return client.instances.get_user_data(instance_id)
@router.post("/{instance_id}/backup")
async def create_backup(instance_id: str, client: VultrClient = Depends(get_client)):
"""Create a backup of an instance"""
return client.instances.create_backup(instance_id)
@router.post("/{instance_id}/restore")
async def restore_instance(
instance_id: str,
backup_id: Optional[str] = None,
snapshot_id: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""Restore an instance from backup or snapshot"""
if backup_id:
return client.instances.restore_backup(instance_id, backup_id)
elif snapshot_id:
return client.instances.restore_snapshot(instance_id, snapshot_id)
return {"error": "backup_id or snapshot_id required"}

View File

@@ -0,0 +1,108 @@
"""Load Balancers router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional, List
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class ForwardingRule(BaseModel):
frontend_protocol: str
frontend_port: int
backend_protocol: str
backend_port: int
class HealthCheck(BaseModel):
protocol: str
port: int
path: Optional[str] = None
check_interval: Optional[int] = None
response_timeout: Optional[int] = None
unhealthy_threshold: Optional[int] = None
healthy_threshold: Optional[int] = None
class CreateLoadBalancerRequest(BaseModel):
region: str
label: Optional[str] = None
balancing_algorithm: Optional[str] = None # roundrobin or leastconn
forwarding_rules: Optional[List[ForwardingRule]] = None
health_check: Optional[HealthCheck] = None
instances: Optional[List[str]] = None
ssl_redirect: Optional[bool] = None
proxy_protocol: Optional[bool] = None
class UpdateLoadBalancerRequest(BaseModel):
label: Optional[str] = None
balancing_algorithm: Optional[str] = None
ssl_redirect: Optional[bool] = None
proxy_protocol: Optional[bool] = None
@router.get("")
async def list_load_balancers(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all load balancers"""
return client.load_balancers.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_load_balancers(client: VultrClient = Depends(get_client)):
"""List all load balancers (auto-paginated)"""
return {"load_balancers": client.load_balancers.list_all()}
@router.get("/{lb_id}")
async def get_load_balancer(lb_id: str, client: VultrClient = Depends(get_client)):
"""Get load balancer details"""
return client.load_balancers.get(lb_id)
@router.post("")
async def create_load_balancer(req: CreateLoadBalancerRequest, client: VultrClient = Depends(get_client)):
"""Create a load balancer"""
data = req.model_dump(exclude_none=True)
if 'forwarding_rules' in data:
data['forwarding_rules'] = [r for r in data['forwarding_rules']]
if 'health_check' in data:
data['health_check'] = dict(data['health_check'])
return client.load_balancers.create(**data)
@router.patch("/{lb_id}")
async def update_load_balancer(lb_id: str, req: UpdateLoadBalancerRequest, client: VultrClient = Depends(get_client)):
"""Update a load balancer"""
return client.load_balancers.update(lb_id, **req.model_dump(exclude_none=True))
@router.delete("/{lb_id}")
async def delete_load_balancer(lb_id: str, client: VultrClient = Depends(get_client)):
"""Delete a load balancer"""
return client.load_balancers.delete(lb_id)
# Forwarding rules
@router.get("/{lb_id}/forwarding-rules")
async def list_forwarding_rules(lb_id: str, client: VultrClient = Depends(get_client)):
"""List forwarding rules for a load balancer"""
return client.load_balancers.list_forwarding_rules(lb_id)
@router.post("/{lb_id}/forwarding-rules")
async def create_forwarding_rule(lb_id: str, req: ForwardingRule, client: VultrClient = Depends(get_client)):
"""Create a forwarding rule"""
return client.load_balancers.create_forwarding_rule(lb_id, **req.model_dump())
@router.delete("/{lb_id}/forwarding-rules/{rule_id}")
async def delete_forwarding_rule(lb_id: str, rule_id: str, client: VultrClient = Depends(get_client)):
"""Delete a forwarding rule"""
return client.load_balancers.delete_forwarding_rule(lb_id, rule_id)

24
server/routers/os_api.py Normal file
View File

@@ -0,0 +1,24 @@
"""Operating Systems router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
@router.get("")
async def list_os(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all operating systems"""
return client.os.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_os(client: VultrClient = Depends(get_client)):
"""List all operating systems (auto-paginated)"""
return {"os": client.os.list_all()}

44
server/routers/plans.py Normal file
View File

@@ -0,0 +1,44 @@
"""Plans router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
@router.get("")
async def list_plans(
plan_type: Optional[str] = None, # vc2, vhf, vdc, etc.
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all cloud compute plans"""
return client.plans.list(plan_type=plan_type, per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_plans(
plan_type: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all cloud compute plans (auto-paginated)"""
return {"plans": client.plans.list_all(plan_type=plan_type)}
@router.get("/bare-metal")
async def list_bare_metal_plans(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all bare metal plans"""
return client.plans.list_bare_metal(per_page=per_page, cursor=cursor)
@router.get("/bare-metal/all")
async def list_all_bare_metal_plans(client: VultrClient = Depends(get_client)):
"""List all bare metal plans (auto-paginated)"""
return {"plans_metal": client.plans.list_all_bare_metal()}

34
server/routers/regions.py Normal file
View File

@@ -0,0 +1,34 @@
"""Regions router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
@router.get("")
async def list_regions(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all regions"""
return client.regions.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_regions(client: VultrClient = Depends(get_client)):
"""List all regions (auto-paginated)"""
return {"regions": client.regions.list_all()}
@router.get("/{region_id}/availability")
async def list_region_availability(
region_id: str,
plan_type: Optional[str] = None, # vc2, vhf, vdc, etc.
client: VultrClient = Depends(get_client)
):
"""List available plans in a region"""
return client.regions.list_availability(region_id, plan_type=plan_type)

View File

@@ -0,0 +1,86 @@
"""Reserved IPs router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateReservedIPRequest(BaseModel):
region: str
ip_type: str = "v4" # v4 or v6
label: Optional[str] = None
class UpdateReservedIPRequest(BaseModel):
label: str
class AttachReservedIPRequest(BaseModel):
instance_id: str
class ConvertReservedIPRequest(BaseModel):
ip_address: str
label: Optional[str] = None
@router.get("")
async def list_reserved_ips(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all reserved IPs"""
return client.reserved_ips.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_reserved_ips(client: VultrClient = Depends(get_client)):
"""List all reserved IPs (auto-paginated)"""
return {"reserved_ips": client.reserved_ips.list_all()}
@router.get("/{reserved_ip}")
async def get_reserved_ip(reserved_ip: str, client: VultrClient = Depends(get_client)):
"""Get reserved IP details"""
return client.reserved_ips.get(reserved_ip)
@router.post("")
async def create_reserved_ip(req: CreateReservedIPRequest, client: VultrClient = Depends(get_client)):
"""Create a reserved IP"""
return client.reserved_ips.create(**req.model_dump(exclude_none=True))
@router.patch("/{reserved_ip}")
async def update_reserved_ip(reserved_ip: str, req: UpdateReservedIPRequest, client: VultrClient = Depends(get_client)):
"""Update a reserved IP"""
return client.reserved_ips.update(reserved_ip, label=req.label)
@router.delete("/{reserved_ip}")
async def delete_reserved_ip(reserved_ip: str, client: VultrClient = Depends(get_client)):
"""Delete a reserved IP"""
return client.reserved_ips.delete(reserved_ip)
@router.post("/{reserved_ip}/attach")
async def attach_reserved_ip(reserved_ip: str, req: AttachReservedIPRequest, client: VultrClient = Depends(get_client)):
"""Attach reserved IP to an instance"""
return client.reserved_ips.attach(reserved_ip, instance_id=req.instance_id)
@router.post("/{reserved_ip}/detach")
async def detach_reserved_ip(reserved_ip: str, client: VultrClient = Depends(get_client)):
"""Detach reserved IP from an instance"""
return client.reserved_ips.detach(reserved_ip)
@router.post("/convert")
async def convert_to_reserved_ip(req: ConvertReservedIPRequest, client: VultrClient = Depends(get_client)):
"""Convert an instance IP to a reserved IP"""
return client.reserved_ips.convert(ip_address=req.ip_address, label=req.label)

View File

@@ -0,0 +1,59 @@
"""Snapshots router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateSnapshotRequest(BaseModel):
instance_id: str
description: Optional[str] = None
class CreateSnapshotFromURLRequest(BaseModel):
url: str
description: Optional[str] = None
@router.get("")
async def list_snapshots(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all snapshots"""
return client.snapshots.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_snapshots(client: VultrClient = Depends(get_client)):
"""List all snapshots (auto-paginated)"""
return {"snapshots": client.snapshots.list_all()}
@router.get("/{snapshot_id}")
async def get_snapshot(snapshot_id: str, client: VultrClient = Depends(get_client)):
"""Get snapshot details"""
return client.snapshots.get(snapshot_id)
@router.post("")
async def create_snapshot(req: CreateSnapshotRequest, client: VultrClient = Depends(get_client)):
"""Create a snapshot from an instance"""
return client.snapshots.create(instance_id=req.instance_id, description=req.description)
@router.post("/from-url")
async def create_snapshot_from_url(req: CreateSnapshotFromURLRequest, client: VultrClient = Depends(get_client)):
"""Create a snapshot from a URL"""
return client.snapshots.create_from_url(url=req.url, description=req.description)
@router.delete("/{snapshot_id}")
async def delete_snapshot(snapshot_id: str, client: VultrClient = Depends(get_client)):
"""Delete a snapshot"""
return client.snapshots.delete(snapshot_id)

View File

@@ -0,0 +1,59 @@
"""SSH Keys router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateSSHKeyRequest(BaseModel):
name: str
ssh_key: str
class UpdateSSHKeyRequest(BaseModel):
name: Optional[str] = None
ssh_key: Optional[str] = None
@router.get("")
async def list_ssh_keys(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all SSH keys"""
return client.ssh_keys.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_ssh_keys(client: VultrClient = Depends(get_client)):
"""List all SSH keys (auto-paginated)"""
return {"ssh_keys": client.ssh_keys.list_all()}
@router.get("/{ssh_key_id}")
async def get_ssh_key(ssh_key_id: str, client: VultrClient = Depends(get_client)):
"""Get SSH key details"""
return client.ssh_keys.get(ssh_key_id)
@router.post("")
async def create_ssh_key(req: CreateSSHKeyRequest, client: VultrClient = Depends(get_client)):
"""Create an SSH key"""
return client.ssh_keys.create(name=req.name, ssh_key=req.ssh_key)
@router.patch("/{ssh_key_id}")
async def update_ssh_key(ssh_key_id: str, req: UpdateSSHKeyRequest, client: VultrClient = Depends(get_client)):
"""Update an SSH key"""
return client.ssh_keys.update(ssh_key_id, **req.model_dump(exclude_none=True))
@router.delete("/{ssh_key_id}")
async def delete_ssh_key(ssh_key_id: str, client: VultrClient = Depends(get_client)):
"""Delete an SSH key"""
return client.ssh_keys.delete(ssh_key_id)

View File

@@ -0,0 +1,61 @@
"""Startup Scripts router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateScriptRequest(BaseModel):
name: str
script: str
type: Optional[str] = "boot" # boot or pxe
class UpdateScriptRequest(BaseModel):
name: Optional[str] = None
script: Optional[str] = None
type: Optional[str] = None
@router.get("")
async def list_startup_scripts(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all startup scripts"""
return client.startup_scripts.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_startup_scripts(client: VultrClient = Depends(get_client)):
"""List all startup scripts (auto-paginated)"""
return {"startup_scripts": client.startup_scripts.list_all()}
@router.get("/{script_id}")
async def get_startup_script(script_id: str, client: VultrClient = Depends(get_client)):
"""Get startup script details"""
return client.startup_scripts.get(script_id)
@router.post("")
async def create_startup_script(req: CreateScriptRequest, client: VultrClient = Depends(get_client)):
"""Create a startup script"""
return client.startup_scripts.create(name=req.name, script=req.script, type=req.type)
@router.patch("/{script_id}")
async def update_startup_script(script_id: str, req: UpdateScriptRequest, client: VultrClient = Depends(get_client)):
"""Update a startup script"""
return client.startup_scripts.update(script_id, **req.model_dump(exclude_none=True))
@router.delete("/{script_id}")
async def delete_startup_script(script_id: str, client: VultrClient = Depends(get_client)):
"""Delete a startup script"""
return client.startup_scripts.delete(script_id)

136
server/routers/vpc.py Normal file
View File

@@ -0,0 +1,136 @@
"""VPC router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional, List
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateVPCRequest(BaseModel):
region: str
description: Optional[str] = None
v4_subnet: Optional[str] = None
v4_subnet_mask: Optional[int] = None
class UpdateVPCRequest(BaseModel):
description: str
class CreateVPC2Request(BaseModel):
region: str
description: Optional[str] = None
ip_block: Optional[str] = None
prefix_length: Optional[int] = None
class AttachVPC2Request(BaseModel):
nodes: List[dict] # [{"id": "instance_id", "ip_address": "10.0.0.1"}, ...]
# VPC 1.0
@router.get("")
async def list_vpcs(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all VPCs"""
return client.vpc.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_vpcs(client: VultrClient = Depends(get_client)):
"""List all VPCs (auto-paginated)"""
return {"vpcs": client.vpc.list_all()}
@router.get("/{vpc_id}")
async def get_vpc(vpc_id: str, client: VultrClient = Depends(get_client)):
"""Get VPC details"""
return client.vpc.get(vpc_id)
@router.post("")
async def create_vpc(req: CreateVPCRequest, client: VultrClient = Depends(get_client)):
"""Create a VPC"""
return client.vpc.create(**req.model_dump(exclude_none=True))
@router.patch("/{vpc_id}")
async def update_vpc(vpc_id: str, req: UpdateVPCRequest, client: VultrClient = Depends(get_client)):
"""Update a VPC"""
return client.vpc.update(vpc_id, description=req.description)
@router.delete("/{vpc_id}")
async def delete_vpc(vpc_id: str, client: VultrClient = Depends(get_client)):
"""Delete a VPC"""
return client.vpc.delete(vpc_id)
# VPC 2.0
@router.get("/v2/list")
async def list_vpc2s(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all VPC 2.0 networks"""
return client.vpc.list_vpc2(per_page=per_page, cursor=cursor)
@router.get("/v2/all")
async def list_all_vpc2s(client: VultrClient = Depends(get_client)):
"""List all VPC 2.0 networks (auto-paginated)"""
return {"vpcs": client.vpc.list_all_vpc2()}
@router.get("/v2/{vpc_id}")
async def get_vpc2(vpc_id: str, client: VultrClient = Depends(get_client)):
"""Get VPC 2.0 details"""
return client.vpc.get_vpc2(vpc_id)
@router.post("/v2")
async def create_vpc2(req: CreateVPC2Request, client: VultrClient = Depends(get_client)):
"""Create a VPC 2.0 network"""
return client.vpc.create_vpc2(**req.model_dump(exclude_none=True))
@router.patch("/v2/{vpc_id}")
async def update_vpc2(vpc_id: str, description: str, client: VultrClient = Depends(get_client)):
"""Update a VPC 2.0 network"""
return client.vpc.update_vpc2(vpc_id, description=description)
@router.delete("/v2/{vpc_id}")
async def delete_vpc2(vpc_id: str, client: VultrClient = Depends(get_client)):
"""Delete a VPC 2.0 network"""
return client.vpc.delete_vpc2(vpc_id)
@router.get("/v2/{vpc_id}/nodes")
async def list_vpc2_nodes(
vpc_id: str,
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List nodes attached to a VPC 2.0"""
return client.vpc.list_vpc2_nodes(vpc_id, per_page=per_page, cursor=cursor)
@router.post("/v2/{vpc_id}/nodes/attach")
async def attach_vpc2_nodes(vpc_id: str, req: AttachVPC2Request, client: VultrClient = Depends(get_client)):
"""Attach nodes to a VPC 2.0"""
return client.vpc.attach_vpc2_nodes(vpc_id, nodes=req.nodes)
@router.post("/v2/{vpc_id}/nodes/detach")
async def detach_vpc2_nodes(vpc_id: str, req: AttachVPC2Request, client: VultrClient = Depends(get_client)):
"""Detach nodes from a VPC 2.0"""
return client.vpc.detach_vpc2_nodes(vpc_id, nodes=req.nodes)