diff --git a/server/deps.py b/server/deps.py index cded675..e6b01ee 100644 --- a/server/deps.py +++ b/server/deps.py @@ -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) diff --git a/server/main.py b/server/main.py index 9dba3f3..6d78496 100644 --- a/server/main.py +++ b/server/main.py @@ -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"]) diff --git a/server/routers/__init__.py b/server/routers/__init__.py index a8328f9..b89be20 100644 --- a/server/routers/__init__.py +++ b/server/routers/__init__.py @@ -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", diff --git a/server/routers/dns.py b/server/routers/dns.py index 36ff148..f2337aa 100644 --- a/server/routers/dns.py +++ b/server/routers/dns.py @@ -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) diff --git a/server/routers/firewall.py b/server/routers/firewall.py index 736418d..f89f9d2 100644 --- a/server/routers/firewall.py +++ b/server/routers/firewall.py @@ -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) diff --git a/server/routers/vpc.py b/server/routers/vpc.py index 3601e0a..43e04b8 100644 --- a/server/routers/vpc.py +++ b/server/routers/vpc.py @@ -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) diff --git a/server/routers/vpc2.py b/server/routers/vpc2.py new file mode 100644 index 0000000..a97042f --- /dev/null +++ b/server/routers/vpc2.py @@ -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)