Make API endpoints compatible with Vultr API v2 format

- Change auth from X-API-Key header to Authorization: Bearer format
- Add /v2 prefix to all endpoints to match Vultr API URL structure
- Fix router paths (dns, firewall) to avoid duplicate path segments
- Split VPC 2.0 into separate router at /v2/vpc2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
HWANG BYUNGHA
2026-01-22 17:03:19 +09:00
parent 184054c6c1
commit b807b9d267
7 changed files with 159 additions and 136 deletions

View File

@@ -1,20 +1,33 @@
"""Dependencies for FastAPI"""
import os
from fastapi import HTTPException, Security
from fastapi.security import APIKeyHeader
from fastapi import HTTPException, Header
from typing import Optional
from vultr_api import VultrClient
API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
VULTR_API_KEY = os.environ.get("VULTR_API_KEY")
def get_client(api_key: str = Security(API_KEY_HEADER)) -> VultrClient:
"""Get VultrClient instance with API key from header or environment"""
key = api_key or VULTR_API_KEY
def get_client(authorization: Optional[str] = Header(None)) -> VultrClient:
"""
Get VultrClient instance with API key from Authorization header or environment.
Supports Vultr API compatible format:
Authorization: Bearer YOUR_API_KEY
"""
key = None
# Parse "Authorization: Bearer API_KEY" header
if authorization and authorization.startswith("Bearer "):
key = authorization[7:] # Remove "Bearer " prefix
# Fallback to environment variable
if not key:
key = VULTR_API_KEY
if not key:
raise HTTPException(
status_code=401,
detail="API key required. Set X-API-Key header or VULTR_API_KEY env var"
detail="API key required. Use 'Authorization: Bearer YOUR_API_KEY' header or set VULTR_API_KEY env var"
)
return VultrClient(api_key=key)

View File

@@ -8,7 +8,7 @@ from contextlib import asynccontextmanager
from routers import (
account, instances, dns, firewall, ssh_keys,
startup_scripts, snapshots, block_storage, reserved_ips,
vpc, load_balancers, bare_metal, backups, plans, regions, os_api
vpc, vpc2, load_balancers, bare_metal, backups, plans, regions, os_api
)
@@ -27,23 +27,24 @@ app = FastAPI(
lifespan=lifespan,
)
# Include routers
app.include_router(account.router, prefix="/account", tags=["Account"])
app.include_router(instances.router, prefix="/instances", tags=["Instances"])
app.include_router(dns.router, prefix="/dns", tags=["DNS"])
app.include_router(firewall.router, prefix="/firewall", tags=["Firewall"])
app.include_router(ssh_keys.router, prefix="/ssh-keys", tags=["SSH Keys"])
app.include_router(startup_scripts.router, prefix="/startup-scripts", tags=["Startup Scripts"])
app.include_router(snapshots.router, prefix="/snapshots", tags=["Snapshots"])
app.include_router(block_storage.router, prefix="/block-storage", tags=["Block Storage"])
app.include_router(reserved_ips.router, prefix="/reserved-ips", tags=["Reserved IPs"])
app.include_router(vpc.router, prefix="/vpc", tags=["VPC"])
app.include_router(load_balancers.router, prefix="/load-balancers", tags=["Load Balancers"])
app.include_router(bare_metal.router, prefix="/bare-metal", tags=["Bare Metal"])
app.include_router(backups.router, prefix="/backups", tags=["Backups"])
app.include_router(plans.router, prefix="/plans", tags=["Plans"])
app.include_router(regions.router, prefix="/regions", tags=["Regions"])
app.include_router(os_api.router, prefix="/os", tags=["Operating Systems"])
# Include routers with /v2 prefix to match Vultr API
app.include_router(account.router, prefix="/v2/account", tags=["Account"])
app.include_router(instances.router, prefix="/v2/instances", tags=["Instances"])
app.include_router(dns.router, prefix="/v2/domains", tags=["DNS"])
app.include_router(firewall.router, prefix="/v2/firewalls", tags=["Firewall"])
app.include_router(ssh_keys.router, prefix="/v2/ssh-keys", tags=["SSH Keys"])
app.include_router(startup_scripts.router, prefix="/v2/startup-scripts", tags=["Startup Scripts"])
app.include_router(snapshots.router, prefix="/v2/snapshots", tags=["Snapshots"])
app.include_router(block_storage.router, prefix="/v2/blocks", tags=["Block Storage"])
app.include_router(reserved_ips.router, prefix="/v2/reserved-ips", tags=["Reserved IPs"])
app.include_router(vpc.router, prefix="/v2/vpcs", tags=["VPC"])
app.include_router(vpc2.router, prefix="/v2/vpc2", tags=["VPC 2.0"])
app.include_router(load_balancers.router, prefix="/v2/load-balancers", tags=["Load Balancers"])
app.include_router(bare_metal.router, prefix="/v2/bare-metals", tags=["Bare Metal"])
app.include_router(backups.router, prefix="/v2/backups", tags=["Backups"])
app.include_router(plans.router, prefix="/v2/plans", tags=["Plans"])
app.include_router(regions.router, prefix="/v2/regions", tags=["Regions"])
app.include_router(os_api.router, prefix="/v2/os", tags=["Operating Systems"])
@app.get("/", tags=["Health"])

View File

@@ -10,6 +10,7 @@ from . import (
block_storage,
reserved_ips,
vpc,
vpc2,
load_balancers,
bare_metal,
backups,
@@ -29,6 +30,7 @@ __all__ = [
"block_storage",
"reserved_ips",
"vpc",
"vpc2",
"load_balancers",
"bare_metal",
"backups",

View File

@@ -29,7 +29,7 @@ class UpdateRecordRequest(BaseModel):
priority: Optional[int] = None
@router.get("/domains")
@router.get("")
async def list_domains(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
@@ -39,44 +39,44 @@ async def list_domains(
return client.dns.list_domains(per_page=per_page, cursor=cursor)
@router.get("/domains/all")
@router.get("/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}")
@router.get("/{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")
@router.post("")
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}")
@router.delete("/{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")
@router.get("/{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")
@router.get("/{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")
@router.get("/{domain}/records")
async def list_records(
domain: str,
per_page: int = Query(25, le=500),
@@ -87,19 +87,19 @@ async def list_records(
return client.dns.list_records(domain, per_page=per_page, cursor=cursor)
@router.get("/domains/{domain}/records/all")
@router.get("/{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}")
@router.get("/{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")
@router.post("/{domain}/records")
async def create_record(domain: str, req: CreateRecordRequest, client: VultrClient = Depends(get_client)):
"""Create a DNS record"""
return client.dns.create_record(
@@ -112,44 +112,44 @@ async def create_record(domain: str, req: CreateRecordRequest, client: VultrClie
)
@router.patch("/domains/{domain}/records/{record_id}")
@router.patch("/{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}")
@router.delete("/{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")
@router.post("/{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")
@router.post("/{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")
@router.post("/{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")
@router.post("/{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")
@router.post("/{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)

View File

@@ -23,7 +23,7 @@ class CreateRuleRequest(BaseModel):
notes: Optional[str] = None
@router.get("/groups")
@router.get("")
async def list_groups(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
@@ -33,38 +33,38 @@ async def list_groups(
return client.firewall.list_groups(per_page=per_page, cursor=cursor)
@router.get("/groups/all")
@router.get("/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}")
@router.get("/{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")
@router.post("")
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}")
@router.patch("/{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}")
@router.delete("/{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")
@router.get("/{group_id}/rules")
async def list_rules(
group_id: str,
per_page: int = Query(25, le=500),
@@ -75,50 +75,50 @@ async def list_rules(
return client.firewall.list_rules(group_id, per_page=per_page, cursor=cursor)
@router.get("/groups/{group_id}/rules/all")
@router.get("/{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}")
@router.get("/{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")
@router.post("/{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}")
@router.delete("/{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")
@router.post("/{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")
@router.post("/{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")
@router.post("/{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")
@router.post("/{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)

View File

@@ -1,6 +1,6 @@
"""VPC router"""
"""VPC router (VPC 1.0)"""
from fastapi import APIRouter, Depends, Query
from typing import Optional, List
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
@@ -20,18 +20,6 @@ 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),
@@ -70,67 +58,3 @@ async def update_vpc(vpc_id: str, req: UpdateVPCRequest, client: VultrClient = D
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)

83
server/routers/vpc2.py Normal file
View File

@@ -0,0 +1,83 @@
"""VPC 2.0 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 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"}, ...]
@router.get("")
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("/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("/{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("")
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("/{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("/{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("/{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("/{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("/{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)