openapi: "3.1.0" info: title: Two API version: "0.1.0" description: REST API for managing VPCs and Subnets in the Two orchestrator. servers: - url: http://localhost:8080 description: Local development server paths: # ── VPC ──────────────────────────────────────────────────────────────────── /vpcs: get: summary: List all VPCs operationId: listVPCs responses: "200": description: List of VPCs content: application/json: schema: type: array items: $ref: "#/components/schemas/VPC" "500": $ref: "#/components/responses/InternalError" post: summary: Create a VPC operationId: createVPC requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/VPCCreateRequest" responses: "202": description: VPC creation accepted content: application/json: schema: $ref: "#/components/schemas/VPC" "400": description: Missing required field or invalid request body content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: VPC already exists content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /vpcs/{name}: parameters: - $ref: "#/components/parameters/ResourceName" get: summary: Get VPC status and info operationId: getVPC responses: "200": description: VPC found content: application/json: schema: $ref: "#/components/schemas/VPC" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" delete: summary: Delete a VPC operationId: deleteVPC responses: "202": description: VPC deletion accepted content: application/json: schema: $ref: "#/components/schemas/VPC" "404": $ref: "#/components/responses/NotFound" "409": description: VPC not deletable — only running or error states can be deleted, and all its subnets must be deleted first content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" # ── VM ───────────────────────────────────────────────────────────────────── /vms: get: summary: List all VMs operationId: listVMs responses: "200": description: List of VMs content: application/json: schema: type: array items: $ref: "#/components/schemas/VM" "500": $ref: "#/components/responses/InternalError" post: summary: Start a VM operationId: startVM requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/VMCreateRequest" responses: "202": description: VM start accepted content: application/json: schema: $ref: "#/components/schemas/VM" "400": description: Missing required field or invalid request body content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: VM already exists content: application/json: schema: $ref: "#/components/schemas/Error" "422": description: Subnet not found, or not in creating/running state content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /vms/{name}: parameters: - $ref: "#/components/parameters/ResourceName" get: summary: Get VM status and info operationId: getVM responses: "200": description: VM found content: application/json: schema: $ref: "#/components/schemas/VM" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" delete: summary: Stop a VM operationId: stopVM responses: "202": description: VM stop accepted content: application/json: schema: $ref: "#/components/schemas/VM" "404": $ref: "#/components/responses/NotFound" "409": description: VM not stoppable — only running or error states can be stopped content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" # ── Subnet ───────────────────────────────────────────────────────────────── /subnets: get: summary: List all subnets operationId: listSubnets responses: "200": description: List of subnets content: application/json: schema: type: array items: $ref: "#/components/schemas/Subnet" "500": $ref: "#/components/responses/InternalError" post: summary: Create a subnet operationId: createSubnet requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SubnetCreateRequest" responses: "202": description: Subnet creation accepted content: application/json: schema: $ref: "#/components/schemas/Subnet" "400": description: Missing required field or unknown iface_type content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Subnet already exists content: application/json: schema: $ref: "#/components/schemas/Error" "422": description: Parent VPC does not exist or is not ready content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /subnets/{name}: parameters: - $ref: "#/components/parameters/ResourceName" get: summary: Get subnet status and info operationId: getSubnet responses: "200": description: Subnet found content: application/json: schema: $ref: "#/components/schemas/Subnet" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" delete: summary: Delete a subnet operationId: deleteSubnet responses: "202": description: Subnet deletion accepted content: application/json: schema: $ref: "#/components/schemas/Subnet" "404": $ref: "#/components/responses/NotFound" "409": description: Subnet not deletable — only running or error states can be deleted content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" # ── Components ────────────────────────────────────────────────────────────── components: parameters: ResourceName: name: name in: path required: true schema: type: string description: Resource name schemas: VPCCreateRequest: type: object required: [name, cidr] properties: name: type: string description: Unique name for the VPC, must follow the format vp-[id] pattern: '^vp-.+' example: vp-00001 cidr: type: string description: CIDR block for the entire VPC address space example: "10.0.0.0/16" VPC: type: object properties: name: type: string example: vp-00001 state: type: string enum: [creating, running, error, deleting, deleted] example: running cidr: type: string example: "10.0.0.0/16" SubnetCreateRequest: type: object required: [name, vpc, interface_ip, cidr] properties: name: type: string description: Unique name for the subnet example: sn-00001 vpc: type: string description: Parent VPC name example: vpc1 mode: type: string description: > Subnet mode. "vxlan" (default): creates a VXLAN tunnel and a host bridge. "bridge": attaches directly to an existing bridge resolved from iface_type in the agent config. "public_ip": accepted and routed like vxlan for DHCP purposes, but its host network setup is not implemented yet — creating such a subnet currently fails in Execute. "vlan" is reserved for future use. enum: [vxlan, bridge, public_ip] default: vxlan example: vxlan vxlan_id: type: integer description: VXLAN VNI identifier. Required when mode is "vxlan", ignored otherwise. example: 100 iface_type: type: string description: Interface type key defined in the agent config (e.g. vms, internet, admin). Falls back to default_interface if omitted or unknown. example: vms interface_ip: type: string format: ipv4 description: Gateway IP for the subnet example: "10.10.10.1" cidr: type: string description: Subnet CIDR block example: "10.10.10.0/24" default_route: type: boolean description: > A default route is always advertised via DHCP; this flag only chooses its next-hop. When false, the next-hop is the subnet interface_ip. When true, it is the `gateway` field if supplied, otherwise the gateway read from the host routing table. The route to the VPC CIDR always keeps interface_ip as its next-hop (except in bridge mode, which has no VPC route), so VPC traffic never leaves through a public gateway. default: false gateway: type: string format: ipv4 description: > Optional next-hop for the default route. Only used when default_route is true; supplied with default_route false, it is ignored. Not validated by the agent: reachability and coherence with the subnet CIDR are the caller's responsibility. example: "10.10.10.254" Subnet: type: object properties: name: type: string example: sn-00001 state: type: string enum: [creating, running, error, deleting, deleted] example: running vpc: type: string example: vpc1 mode: type: string enum: [vxlan, bridge, public_ip] example: vxlan vxlan_id: type: integer description: VXLAN VNI. Present only when mode is "vxlan". example: 100 local_iface: type: string description: Resolved interface name from agent config example: br-000000 interface_ip: type: string example: "10.10.10.1" cidr: type: string example: "10.10.10.0/24" default_route: type: boolean example: false gateway: type: string example: "10.10.10.254" VMCreateRequest: type: object required: [name, interfaces, storage] properties: name: type: string example: vm-00001 memory: type: integer description: Memory in MB (default 512) example: 1024 cpus: type: integer description: Number of vCPUs (default 1) example: 2 metadata: $ref: "#/components/schemas/VMMetadata" interfaces: type: array minItems: 1 description: > Network interfaces, in order. The position determines the PCI slot (0x03 + index) and therefore the interface name inside the guest. **Exactly one** interface must be marked primary: it carries the default route and the metadata server. All subnets must belong to the same VPC. items: $ref: "#/components/schemas/VMInterface" storage: type: array minItems: 1 items: $ref: "#/components/schemas/VMStorage" uefi: type: boolean description: Boot with UEFI firmware (OVMF). Defaults to false (SeaBIOS). example: false VMMetadata: type: object description: > cloud-init inputs for this VM. Every field is optional. Documents not supplied here fall back to the agent's built-in templates; a document supplied as an empty string is served empty, which is not the same thing. properties: password: type: string description: > Password *hash* for the syonad account, as expected by the cloud-config `passwd` key (e.g. `$6$...`) — not a plaintext password. When omitted, the account is created locked (`lock_passwd: true`); when neither password nor sshkey is given, no account is created at all. example: "$6$rounds=4096$saltsalt$hash..." sshkey: type: string description: Public SSH key added to the syonad account. Sent as-is, not encoded. example: "ssh-ed25519 AAAA..." user_data: type: string format: byte description: > cloud-init user-data, **base64-encoded**. Encoding keeps multi-line documents out of JSON escaping and allows gzip+base64 payloads. Invalid base64 is rejected with 400 rather than silently serving an empty document. The agent never interprets this content. example: "I2Nsb3VkLWNvbmZpZwpwYWNrYWdlczoKICAtIG5naW54Cg==" VMInterface: type: object required: [subnet, ip, primary] properties: subnet: type: string example: sn-00001 ip: type: string format: ipv4 example: "10.0.0.5" primary: type: boolean example: true VMStorage: type: object required: [path, dev] properties: path: type: string description: Path to the disk image on the host example: /var/lib/two/volumes/abc.qcow2 dev: type: string description: Device name inside the VM pattern: '^[sv]d[a-z]$' example: vda VM: type: object properties: name: type: string example: vm-00001 state: type: string enum: [creating, running, error, deleting, deleted] example: running metadata_port: type: string example: "80" memory: type: integer example: 1024 cpus: type: integer example: 2 interfaces: type: array items: $ref: "#/components/schemas/VMInterface" storage: type: array items: $ref: "#/components/schemas/VMStorage" uefi: type: boolean example: false Error: type: object properties: error: type: string example: "resource not found" responses: NotFound: description: Resource not found content: application/json: schema: $ref: "#/components/schemas/Error" InternalError: description: Internal server error content: application/json: schema: $ref: "#/components/schemas/Error"