From f1315881e922efc3a06a1094931ad1e66effc238 Mon Sep 17 00:00:00 2001 From: Jason King Date: Wed, 9 Dec 2020 17:09:28 -0600 Subject: [PATCH 1/6] Docs --- docs/index.md | 630 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 628 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 76d35687..521da9e5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ markdown2extras: tables, code-friendly --> @@ -880,6 +880,10 @@ Note that a `Triton-Datacenter-Name` response header was added in 9.2.0. The section describes API changes in CloudAPI versions. +## 9.15.0 + +- Added support for creating VPCs and VPC networks. + ## 9.14.0 - Expose Feed of Machines Changes [#68](https://github.com/joyent/sdc-cloudapi/pull/68). @@ -917,7 +921,7 @@ across reprovisions. connect to the VNC console of a HVM. - Support for pre-signed URLs for authentication (needed to make WebSockets useable with browsers). - + ## 9.8.6 - Allow `affinity` and `tags` to [Volume](#Volumes) creation, which can be used @@ -8822,6 +8826,7 @@ id | UUID | Unique id for this network name | String | The network name public | Boolean | Whether this a public or private (rfc1918) network fabric | Boolean | Whether this network is created on a fabric +vpc | Boolean | Whether this network is created on a VPC description | String | Description of this network (optional) subnet | String | A CIDR formatted string that describes the network provision_start_ip| String | The first IP on the network that may be assigned @@ -8864,6 +8869,7 @@ ResourceNotFound | If `:login` or `:vlan_id` does not exist "name": "default", "public": false, "fabric": true, + "vpc": false, "gateway": "192.168.128.1", "internet_nat": true, "provision_end_ip": "192.168.131.250", @@ -8924,6 +8930,7 @@ id | UUID | Unique id for this network name | String | The network name public | Boolean | Whether this a public or private (rfc1918) network fabric | Boolean | Whether this network is created on a fabric +vpc | Boolean | Whether this network is created on a VPC description | String | Description of this network (optional) subnet | String | A CIDR formatted string that describes the network provision_start_ip| String | The first IP on the network that may be assigned @@ -8981,6 +8988,7 @@ ResourceNotFound | If `:login` does not exist "name": "newnet", "public": false, "fabric": true, + "vpc": false, "gateway": "10.50.1.1", "internet_nat": true, "provision_end_ip": "10.50.1.20", @@ -9021,6 +9029,7 @@ id | UUID | Unique id for this network name | String | The network name public | Boolean | Whether this a public or private (rfc1918) network fabric | Boolean | Whether this network is created on a fabric +vpc | Boolean | Whether this network is created on a VPC description | String | Description of this network (optional) subnet | String | A CIDR formatted string that describes the network provision_start_ip| String | The first IP on the network that may be assigned @@ -9063,6 +9072,7 @@ ResourceNotFound | If `:login`, `:vlan_id` or `:id` does not exist "name": "newnet", "public": false, "fabric": true, + "vpc": false, "gateway": "10.50.1.1", "internet_nat": true, "provision_end_ip": "10.50.1.20", @@ -9104,6 +9114,7 @@ id | UUID | Unique id for this network name | String | The network name public | Boolean | Whether this a public or private (rfc1918) network fabric | Boolean | Whether this network is created on a fabric +vpc | Boolean | Whether this network is created on a VPC description | String | Description of this network (optional) subnet | String | A CIDR formatted string that describes the network provision_start_ip| String | The first IP on the network that may be assigned @@ -9156,6 +9167,7 @@ ResourceNotFound | If `:login` does not exist "name": "newnet", "public": false, "fabric": true, + "vpc": false, "description": "updated description", "gateway": "192.168.128.1", "internet_nat": true, @@ -9213,7 +9225,621 @@ InUseError | The VLAN currently has active networks on it Api-Version: 7.3.0 +# VPCs + +VPCs are the next iteration of fabrics. Some notable differences between +fabrics and VPCs: + +- A user (subject to quota limits) can have multiple VPCs, each VPC is isolated + from any other VPC. +- Each VPC must have a CIDR block that contains all of the networks (and IPs) + allocated in those networks. That is, all networks created in a VPC must + fit within the CIDR block used to create the VPC. +- VPC Networks automatically route traffic between each other without having to + create any dedicated router instances. +- VPC Networks do not have a vlan id. While used internally, these values are + automatically assigned as required. + + +## ListVPCs (GET /:login/vpc/) + +### Inputs + +* None + +### Returns + +An array of A VPC objects. + +**Field** | **Type** | **Description** +----------- | -------- | --------------- +vpc_id | UUID | Unique id for the network +name | String | A unique name to identify the VPC +cidr | String | The CIDR block for networks in this VPC. +description | String | Description of the VPC (optional) + +### Errors + +For all possible errors, see [CloudAPI HTTP Responses](#cloudapi-http-responses). + +**Error Code** | **Description** +-------------- | --------------- +ResourceNotFound | If `:login` does not exist + +#### Example Request + + GET /login/vpc HTTP/1.1 + Authorization: Basic ... + Host: api.example.com + Accept: application/json + Accept-version: ~9.13 + +#### Example Response + + HTTP/1.1 200 OK + Content-Type: application/json + Server: Joyent Triton 9.13.0 + Api-Version: 9.13.0 + + [ + { + "vpc_id": "ee8e7430-a607-4f79-b36a-798031b842aa", + "name": "MyVPC", + "cidr": "192.168.0.0/16", + "description": "My First VPC Network" + } + ] + + +## CreateVPC (POST /:login/vpc) + +Creates a new VPC. + +### Inputs + +**Field** | **Type** | **Description** +----------- | -------- | --------------- +name | String | A unique name to identify the VPC +cidr | String | The CIDR block (address/mask) for this VPC. +description | String | An optional description of the VPC. + +### Returns + +A VPC Object. + + +**Field** | **Type** | **Description** +----------- | -------- | --------------- +vpc_id | UUID | The unique id of the VPC. +name | String | A unique name to identify the VPC +cidr | String | The CIDR block (address/mask) for this VPC. +description | String | An optional description of the VPC. + +### Errors + +**Error Code** | **Description** +---------------- | --------------- +ResourceNotFound | If `:login` does not exist +MissingParameter | If you didn't send a required field +InvalidArgument | `vlan_id` or `name` are in use, or `vlan_id` is outside the valid range +QuotaExceeded | The user has already created the maximum number of allowed VPCs. + +#### Example Request + + POST /login/vpc + Authorization: Basic ... + Host: api.example.com + Accept: application/json + Accept-version: ~9.13 + + { + "name": "MyVPC", + "description": "My First VPC", + "cidr": "10.100.0.0/16" + } + +#### Example Response + + HTTP/1.1 201 Created + Content-Type: application/json + Server: Joyent Triton 9.13.0 + Api-Version: 9.13.0 + + { + "vpc_id": "ee8e7430-a607-4f79-b36a-798031b842aa", + "name": "MyVPC", + "description": "My First VPC", + "cidr": "10.100.0.0/16" + } + +## GetVPC (GET /:login/vpc/:vpc_id) + +### Inputs + +* None + +### Returns + +A VPC object + +**Field** | **Type** | **Description** +----------- | -------- | --------------- +vpc_id | UUID | Unique id for the network +name | String | A unique name to identify the VPC +cidr | String | The CIDR block for networks in this VPC. +description | String | Description of the VPC (optional) + +### Errors + +For all possible errors, see [CloudAPI HTTP Responses](#cloudapi-http-responses). + +**Error Code** | **Description** +-------------- | --------------- +ResourceNotFound | If `:login` does not exist + +#### Example Request + + GET /login/vpc/ee8e7430-a607-4f79-b36a-798031b842aa HTTP/1.1 + Authorization: Basic ... + Host: api.example.com + Accept: application/json + Accept-version: ~9.13 + +#### Example Response + + HTTP/1.1 200 OK + Content-Type: application/json + Server: Joyent Triton 9.13.0 + Api-Version: 9.13.0 + + [ + { + "vpc_id": "ee8e7430-a607-4f79-b36a-798031b842aa", + "name": "MyVPC", + "cidr": "192.168.0.0/16", + "description": "My First VPC Network" + } + ] + +## UpdateVPC (PUT /:login/vpc/:vpc_id) + +Updates a VPC. + +### Inputs + +All inputs are optional. + +**Field** | **Type** | **Description** +----------- | -------- | --------------- +name | String | A unique name to identify the VPC +description | String | Description of the VPC (optional) + +### Returns + +**Field** | **Type** | **Description** +----------- | -------- | --------------- +vpc_id | UUID | Unique id for the network +name | String | A unique name to identify the VPC +cidr | String | The CIDR block for networks in this VPC. +description | String | Description of the VPC (optional) + +### Errors + +**Error Code** | **Description** +---------------- | --------------- +ResourceNotFound | If `:login` or `:vpc_id` does not exist + +#### Example Request + + POST /login/vpcs/ee8e7430-a607-4f79-b36a-798031b842aa HTTP/1.1 + Authorization: Basic ... + Host: api.example.com + Accept: application/json + Accept-version: ~9.13 + + { + "description": "new description" + } + +#### Example Response + + HTTP/1.1 202 Accepted + Content-Type: application/json + Server: Joyent Triton 9.13.0 + Api-Version: 9.13.0 + + { + "vpc_id": "ee8e7430-a607-4f79-b36a-798031b842aa", + "name": "MyVPC", + "description": "new description", + "cidr": "192.168.0.0/16", + } + +## DeleteVPC (DELETE /:login/vpc/:vpc_id) + +Deletes the specified VPC. Note that there must be no networks on that VPC in +order for the VPC to be deleted. + +### Inputs + +* None + +### Returns + +* None + +### Errors + +For all possible errors, see [CloudAPI HTTP Responses](#cloudapi-http-responses). + +**Error Code** | **Description** +---------------- | --------------- +ResourceNotFound | If `:login` or `:vpc_id` does not exist +InUseError | The VPC currently has active networks on it + +#### Example Request + + DELETE /login/vpc/ee8e7430-a607-4f79-b36a-798031b842aa HTTP/1.1 + Authorization: Basic ... + Host: api.example.com + Accept: application/json + Accept-version: ~9.13 + +#### Example Response + + HTTP/1.1 204 No Content + Content-Type: application/json + Server: Joyent Triton 9.13.0 + Api-Version: 9.13.0 + +## ListVPCNetworks (GET /:login/vpc/:vpc_id/networks) + +Lists all of the networks in the VPC specified by `:vpc_id`. + +### Inputs + +* None + +### Returns + +Returns an array of Network Objects. Each network object has the following +information: + +**Field** | **Type** | **Description** +------------ | -------- | --------------- +id | UUID | Unique id for this network +name | String | The network name +public | Boolean | Whether this a public or private (rfc1918) network +fabric | Boolean | Whether this network is created on a fabric +vpc | Boolean | Whether this network is created on a VPC +description | String | Description of this network (optional) +subnet | String | A CIDR formatted string that describes the network +provision_start_ip| String | The first IP on the network that may be assigned +provision_end_ip | String | The last IP on the network that may be assigned +gateway | String | Optional Gateway IP address +resolvers | String | Resolver IP addresses +routes | Routes Object| Optional Static routes for hosts on this network + +### Errors + +For all possible errors, see [CloudAPI HTTP Responses](#cloudapi-http-responses). + +**Error Code** | **Description** +---------------- | --------------- +ResourceNotFound | If `:login` or `:vpc_id` does not exist + +#### Example Request + + GET /login/vpc/ee8e7430-a607-4f79-b36a-798031b842aa/networks HTTP/1.1 + Authorization: Basic ... + Host: api.example.com + Accept: application/json + Accept-version: ~9.13 + +#### Example Response + + HTTP/1.1 200 OK + Content-Type: application/json + Server: Joyent Triton 9.13.0 + Api-Version: 9.13.0 + + [ + { + "id": "8ecf7f1c-e69f-4264-b94b-57774aab71e6", + "name": "default", + "public": false, + "fabric": false, + "vpc": true, + "gateway": "192.168.128.1", + "internet_nat": true, + "provision_end_ip": "192.168.131.250", + "provision_start_ip": "192.168.128.5", + "resolvers": [ + "8.8.8.8", + "8.8.4.4" + ], + "routes": { + "192.168.0.0/16": "192.168.128.1" + }, + "subnet": "192.168.128.0/22", + }, + { + "id": "ad2ae2e7-2ec8-4d64-9528-aa7fb69352cb", + "name": "newnet", + "public": false, + "fabric": false, + "vpc": true, + "gateway": "192.168.5.1", + "internet_nat": true, + "provision_end_ip": "192.168.5.20", + "provision_start_ip": "192.168.5.2", + "resolvers": [ + "8.8.8.8", + "8.8.4.4" + ], + "routes": { + "192.168.0.0/16": "192.168.5.1" + }, + "subnet": "192.168.5.0/24", + } + ] + +## CreateVPCNetwork (POST /:login/vpc/:vpc_id/networks) + +Creates a network on the VPC specified by `:vpc_id`. The subnet must be a +subset of the CIDR block for `:vpc_id`. + +**Field** | **Type** | **Description** +------------ | -------- | --------------- +name | String | The network name; must be unique +description | String | Description of this network (optional) +subnet | String | A CIDR formatted string that describes the network +provision_start_ip| String | The first IP on the network that may be assigned +provision_end_ip | String | The last IP on the network that may be assigned +gateway | String | Gateway IP address +resolvers | String | Optional resolver IP addresses +routes | Routes Object| Optional Static routes for hosts on this network. +internet_nat | Boolean | Provision internet NAT zone on gateway address, default is true + +If the gateway address is not specified, the first IP in the subnet is used. +This may adjust the provision_start_ip value if it is the first IP in the +subnet. + +If no routes are given, a default route entry with the VPC CIDR block and +a next hop of the gateway IP are added. If no routes are desired for the +network, an empty routes object should be given. + +Network Object: + +**Field** | **Type** | **Description** +------------ | -------- | --------------- +id | UUID | Unique id for this network +name | String | The network name +public | Boolean | Whether this a public or private (rfc1918) network +fabric | Boolean | Whether this network is created on a fabric +vpc | Boolean | Whether this network is created on a VPC +description | String | Description of this network (optional) +subnet | String | A CIDR formatted string that describes the network +provision_start_ip| String | The first IP on the network that may be assigned +provision_end_ip | String | The last IP on the network that may be assigned +gateway | String | Gateway IP address +resolvers | String | Resolver IP addresses +routes | Routes Object| Optional Static routes for hosts on this network +internet_nat | Boolean | Provision internet NAT zone on gateway address + +### Errors + +For all possible errors, see [CloudAPI HTTP Responses](#cloudapi-http-responses). + +**Error Code** | **Description** +---------------- | --------------- +ResourceNotFound | If `:login` or `:vpc_id` do not exist + +#### Example Request + + POST /login/vpc/8e7430-a607-4f79-b36a-798031b842aa/networks HTTP/1.1 + Authorization: Basic ... + Host: api.example.com + Accept: application/json + Accept-version: ~9.13 + + { + "name": "newnet", + "provision_end_ip": "192.168.1.20", + "provision_start_ip": "192.168.1.5", + "resolvers": [ + "8.8.8.8", + "8.8.4.4" + ], + "subnet": "10.50.1.0/24" + } + +#### Example Response + + HTTP/1.1 201 Created + Content-Type: application/json + Server: Joyent Triton 9.13.0 + Api-Version: 9.13.0 + + { + "id": "fe5813d7-b897-4538-91ae-5fc2490fbafe", + "name": "newnet", + "public": false, + "fabric": false, + "vpc": true, + "gateway": "192.168.1.1", + "internet_nat": true, + "provision_end_ip": "192.168.1.20", + "provision_start_ip": "192.168.1.5", + "resolvers": [ + "8.8.8.8", + "8.8.4.4" + ], + "routes": { + "192.168.0.0/16": "192.168.1.1" + }, + "subnet": "192.168.1.0/24", + } + +## GetVPCNetwork (GET /:login/vpc/:vpc_id/networks/:id) + +### Inputs + +* None + +### Returns + +The details of the network object: + +**Field** | **Type** | **Description** +------------ | -------- | --------------- +id | UUID | Unique id for this network +name | String | The network name +public | Boolean | Whether this a public or private (rfc1918) network +fabric | Boolean | Whether this network is created on a fabric +vpc | Boolean | Whether this network is created on a VPC +description | String | Description of this network (optional) +subnet | String | A CIDR formatted string that describes the network +provision_start_ip| String | The first IP on the network that may be assigned +provision_end_ip | String | The last IP on the network that may be assigned +gateway | String | Optional Gateway IP address +resolvers | String | Resolver IP addresses +routes | Routes Object| Optional Static routes for hosts on this network +internet_nat | Boolean | Provision internet NAT zone on gateway address + +### Errors + +For all possible errors, see [CloudAPI HTTP Responses](#cloudapi-http-responses). + +**Error Code** | **Description** +---------------- | --------------- +ResourceNotFound | If `:login`, `:vpc_id` or `:id` does not exist + +## UpdateVPCNetwork (PUT /:login/vpc/:vpc_id/networks/:id) + +### Inputs + +**Field** | **Type** | **Description** +------------ | -------- | --------------- +name | String | The network name; must be unique (optional) +description | String | Description of this network (optional) +provision_start_ip| String | The first IP on the network that may be assigned (optional) +provision_end_ip | String | The last IP on the network that may be assigned (optional) +resolvers | String | Resolver IP addresses (optional) +routes | Routes Object| Static routes for hosts on this network (optional) + +### Returns + +Network Object: + +**Field** | **Type** | **Description** +------------ | -------- | --------------- +id | UUID | Unique id for this network +name | String | The network name +public | Boolean | Whether this a public or private (rfc1918) network +fabric | Boolean | Whether this network is created on a fabric +vpc | Boolean | Whether this network is created on a VPC +description | String | Description of this network (optional) +subnet | String | A CIDR formatted string that describes the network +provision_start_ip| String | The first IP on the network that may be assigned +provision_end_ip | String | The last IP on the network that may be assigned +gateway | String | Optional Gateway IP address +resolvers | String | Resolver IP addresses +routes | Routes Object| Optional Static routes for hosts on this network +internet_nat | Boolean | Provision internet NAT zone on gateway address + +### Errors + +For all possible errors, see [CloudAPI HTTP Responses](#cloudapi-http-responses). + +**Error Code** | **Description** +---------------- | --------------- +ResourceNotFound | If `:login` does not exist + +#### Example Request + + PUT /login/vpc/8e7430-a607-4f79-b36a-798031b842aa/networks/8f6372d0-4988-4aab-8072-9f62d5c6210b HTTP/1.1 + Authorization: Basic ... + Host: api.example.com + Accept: application/json + Accept-version: ~9.13 + + { + "name": "updatenet", + "description": "updated description", + "resolvers": [ + "8.8.8.8" + ], + "routes": { + "192.168.128.0/16": "192.168.128.1", + "172.16.10.0/24": "192.16.128.1" + } + } + +#### Example Response + + HTTP/1.1 201 Created + Content-Type: application/json + Server: Joyent Triton 7.3.0 + Api-Version: 7.3.0 + + { + "id": "8f6372d0-4988-4aab-8072-9f62d5c6210b", + "name": "updatenet", + "public": false, + "fabric": false, + "vpc": true, + "description": "updated description", + "gateway": "192.168.128.1", + "internet_nat": true, + "provision_end_ip": "192.168.131.250", + "provision_start_ip": "192.168.128.5", + "resolvers": [ + "8.8.8.8" + ], + "routes": { + "192.168.0.0/16": "192.168.128.1", + "172.16.10.1/24": "192.168.128.1" + }, + "subnet": "192.168.128.0/22" + } + +## DeleteVPCNetwork (DELETE /:login/vpc/:vpc_id/networks/:id) +Deletes the specified Network. Note that no instances may be provisioned on the +Network. + +### Inputs + +* None + +### Returns + +* None + +### Errors + +For all possible errors, see [CloudAPI HTTP Responses](#cloudapi-http-responses). + +**Error Code** | **Description** +---------------- | --------------- +ResourceNotFound | If `:login`, `:vpc_id` or `:id` does not exist +InUseError | The VPC network currently has provisioned instances on it + +#### Example Request + + DELETE /login/vpc/networks/f5008d60-470a-4c96-8b6b-2c8f55fc40e4 HTTP/1.1 + Authorization: Basic ... + Host: api.example.com + Accept: application/json + Accept-version: ~9.13 + +#### Example Response + + HTTP/1.1 204 No Content + Content-Type: application/json + Server: Joyent Triton 9.13.0 + Api-Version: 9.13.0 # Networks From a90dee150d5fd17cfab56481e6064e9cd165f768 Mon Sep 17 00:00:00 2001 From: Jason King Date: Wed, 9 Dec 2020 17:09:53 -0600 Subject: [PATCH 2/6] VPC Support --- lib/endpoints/networks.js | 435 ++++++++++++++++++++++++++++++- package.json | 4 +- sapi_manifests/cloudapi/template | 7 + 3 files changed, 442 insertions(+), 4 deletions(-) diff --git a/lib/endpoints/networks.js b/lib/endpoints/networks.js index 89059d72..01eca49b 100644 --- a/lib/endpoints/networks.js +++ b/lib/endpoints/networks.js @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2019, Joyent, Inc. + * Copyright 2020 Joyent, Inc. */ var assert = require('assert-plus'); @@ -29,6 +29,12 @@ var FABRIC_VLAN_FIELDS = ['description', 'name', 'vlan_id']; var FABRIC_NETWORK_FIELDS = ['description', 'fabric', 'gateway', 'internet_nat', 'name', 'provision_end_ip', 'provision_start_ip', 'resolvers', 'routes', 'subnet', 'uuid', 'vlan_id']; + +var VPC_FIELDS = ['description', 'name', 'cidr', 'uuid']; +var VPC_NETWORK_FIELDS = ['description', 'vpc', 'gateway', + 'name', 'provision_end_ip', 'provision_start_ip', + 'resolvers', 'routes', 'subnet', 'uuid']; + // Fields that are IPv4 addresses: var IP_FIELDS = ['gateway', 'provision_end_ip', 'provision_start_ip', 'resolvers', 'resolvers[0]', 'resolvers[1]', 'resolvers[2]', @@ -50,6 +56,18 @@ function ensureFabricsEnabled(req, res, next) { return next(); } +/* + * Similarly, return an error if VPCs aren't enabled in this datacenter + */ +function ensureVPCEnabled(req, res, next) { + if (!req.config.vpc_enabled) { + return next(new restify.NotImplementedError( + 'VPC not enabled for this datacenter')); + } + + return next(); +} + /* * Return an error if the network is not valid for the user */ @@ -135,7 +153,8 @@ function translateErr(err) { } -// Note here "net" can be a network, fabric network or network_pool from NAPI +// Note here "net" can be a network, fabric network, vpc network, or +// network_pool from NAPI function translateNetwork(net) { assert.object(net, 'net'); @@ -149,6 +168,8 @@ function translateNetwork(net) { isPublic = net['public']; } else if (net.fabric) { isPublic = false; + } else if (net.vpc) { + isPublic = false; } else { isPublic = netconfig.isNetExternal(net); } @@ -165,6 +186,16 @@ function translateNetwork(net) { return; } + if (net.hasOwnProperty(p)) { + obj[p] = net[p]; + } + }); + } else if (net.vpc) { + VPC_NETWORK_FIELDS.forEach(function (p) { + if (p === 'uuid') { + return; + } + if (net.hasOwnProperty(p)) { obj[p] = net[p]; } @@ -831,6 +862,318 @@ function deleteFabricNetwork(req, res, next) { }); } +function listVPCs(req, res, next) { + assert.ok(req.account); + assert.ok(req.sdc.napi); + + var params = { + fields: VPC_FIELDS + }; + + return req.sdc.napi.listVPCs(req.account.uuid, params, reqOpts(req), + function onList(err, vpcs) { + if (err) { + next(translateErr(err)); + return; + } + + req.log.debug({ + vpcs: vpcs, + account: req.account.login + }, 'ListVPCs done'); + + res.send(vpcs); + next(); + return; + }); +} + +function createVPC(req, res, next) { + assert.ok(req.account); + assert.ok(req.sdc.napi); + + var params; + + try { + params = schemaValidate(schemas.CreateVPC, req); + } catch (schemaErr) { + next(schemaErr); + return; + } + + params.fields = VPC_FIELDS; + params.owner_uuid = req.account.uuid; + + req.sdc.napi.createVPC(params, reqOpts(req), function onCreate(err, vpc) { + if (err) { + next(translateErr(err)); + return; + } + + res.send(201, vpc); + next(); + }); +} + +function getVPC(req, res, next) { + assert.ok(req.account); + assert.ok(req.sdc.napi); + + var params; + + try { + params = schemaValidate(schemas.GetVPC, req); + } catch (schemaErr) { + next(schemaErr); + return; + } + + params.fields = VPC_FIELDS; + params.owner_uuid = req.account.uuid; + + req.sdc.napi.getVPC(params.vpc_id, params, reqOpts(req), + function onGet(err, vpc) { + if (err) { + next(translateErr(err)); + return; + } + + res.send(201, vpc); + next(); + }); +} + +function updateVPC(req, res, next) { + assert.ok(req.account); + assert.ok(req.sdc.napi); + + var params; + var vpcId; + + try { + params = schemaValidate(schemas.UpdateVPC, req); + } catch (schemaErr) { + next(schemaErr); + return; + } + + vpcId = params.vpc_id; + delete params.vpc_id; + + params.fields = VPC_FIELDS; + params.owner_uuid = req.account.uuid; + + req.sdc.napi.updateVPC(vpcId, params, reqOpts(req), + function onUpdate(err, vpc) { + if (err) { + next(translateErr(err)); + return; + } + + res.send(202, vpc); + next(); + }); +} + +function deleteVPC(req, res, next) { + assert.ok(req.account); + assert.ok(req.sdc.napi); + + var params; + + try { + params = schemaValidate(schemas.DeleteVPC, req); + } catch (schemaErr) { + next(schemaErr); + return; + } + + params.owner_uuid = req.account.uuid; + + req.sdc.napi.deleteVPC(params.vpc_id, params, reqOpts(req), + function onDel(err) { + if (err) { + next(translateErr(err)); + return; + } + res.send(204); + next(); + }); +} + +function listVPCNetworks(req, res, next) { + var params; + + assert.ok(req.account); + assert.ok(req.sdc.napi); + + try { + params = schemaValidate(schemas.ListVPCNetworks, req); + } catch (schemaErr) { + next(schemaErr); + return; + } + + params.owner_uuid = req.account.uuid; + + req.sdc.napi.listVPCNetworks(params.vpc_id, {}, reqOpts(req), + function onList(err, networks) { + if (err) { + next(translateErr(err)); + return; + } + + res.send(networks.map(function _translateNetwork(network) { + assert.object(network, 'network'); + return translateNetwork(network); + })); + + next(); + }); +} + +function createVPCNetwork(req, res, next) { + var params; + var vpcId; + + assert.ok(req.account); + assert.ok(req.sdc.napi); + + try { + params = schemaValidate(schemas.CreateVPCNetwork, req); + } catch (schemaErr) { + next(schemaErr); + return; + } + + if (params.resolvers && params.resolvers.length > MAX_RESOLVERS) { + next(new InvalidArgumentError(format( + 'property "resolvers": maximum of %d resolvers', + MAX_RESOLVERS))); + return; + } + + vpcId = params.vpc_id; + delete params.vpc_id; + + params.fields = VPC_NETWORK_FIELDS; + params.owner_uuid = req.account.uuid; + + req.sdc.napi.createVPCNetwork(vpcId, params, reqOpts(req), + function onCreate(err, network) { + if (err) { + next(translateErr(err)); + return; + } + + res.send(201, translateNetwork(network)); + next(); + }); +} + +function getVPCNetwork(req, res, next) { + assert.ok(req.account); + assert.ok(req.sdc.napi); + + var params; + + try { + params = schemaValidate(schemas.GetVPCNetwork, req); + } catch (schemaErr) { + next(schemaErr); + return; + } + + params.owner_uuid = req.account.uuid; + + req.sdc.napi.getVPCNetwork(params.vpc_id, params.id, + { fields: FABRIC_NETWORK_FIELDS }, reqOpts(req), + function onGet(err, network) { + if (err) { + next(translateErr(err)); + return; + } + + res.send(translateNetwork(network)); + next(); + }); +} + +function updateVPCNetwork(req, res, next) { + assert.ok(req.account); + assert.ok(req.sdc.napi); + + var params; + var vpcId; + var id; + + if (req.body && req.body.id) { + next(new InvalidArgumentError(format( + 'property "id": cannot be set'))); + return; + } + + try { + params = schemaValidate(schemas.UpdateVPCNetwork, req); + } catch (schemaErr) { + next(schemaErr); + return; + } + + if (params.resolvers && params.resolvers.length > MAX_RESOLVERS) { + next(new InvalidArgumentError(format( + 'property "resolvers": maximum of %d resolvers', + MAX_RESOLVERS))); + return; + } + + vpcId = params.vpc_id; + delete params.vpc_id; + + id = params.id; + delete params.id; + + params.fields = FABRIC_NETWORK_FIELDS; + params.owner_uuid = req.account.uuid; + + req.sdc.napi.updateVPCNetwork(vpcId, id, params, + function onUpdate(err, network) { + if (err) { + next(translateErr(err)); + return; + } + + res.send(translateNetwork(network)); + next(); + }); +} + +function deleteVPCNetwork(req, res, next) { + assert.ok(req.account); + assert.ok(req.sdc.napi); + + var params; + + try { + params = schemaValidate(schemas.DeleteVPCNetwork, req); + } catch (schemaErr) { + next(schemaErr); + return; + } + + params.owner_uuid = req.account.uuid; + + req.sdc.napi.deleteVPCNetwork(params.vpc_id, params.id, params, + reqOpts(req), function onDel(err) { + if (err) { + next(translateErr(err)); + return; + } + + res.send(204); + next(); + }); +} function mountNetworks(server, before, pre) { assert.object(server, 'server'); @@ -927,6 +1270,94 @@ function mountNetworks(server, before, pre) { version: [ '7.3.0', '8.0.0', '9.0.0' ] }, before, ensureFabricsEnabled, pre, deleteFabricNetwork); + // --- VPCs + + server.get({ + path: '/:account/vpc', + name: 'ListVPCs', + version: [ '9.13.0'] + }, before, ensureVPCEnabled, pre, listVPCs); + + server.head({ + path: '/:account/vpc', + name: 'HeadVPCs', + version: [ '9.13.0'] + }, before, ensureVPCEnabled, pre, listVPCs); + + server.post({ + path: '/:account/vpc', + name: 'CreateVPC', + version: ['9.13.0'] + }, before, ensureVPCEnabled, pre, createVPC); + + server.get({ + path: '/:account/vpc/:id', + name: 'GetVPC', + version: ['9.13.0'] + }, before, ensureVPCEnabled, pre, getVPC); + + server.head({ + path: '/:account/vpc/:id', + name: 'GetVPC', + version: ['9.13.0'] + }, before, ensureVPCEnabled, pre, getVPC); + + server.put({ + path: '/:account/vpc/:id', + name: 'UpdateVPC', + version: ['9.13.0'] + }, before, ensureVPCEnabled, pre, updateVPC); + + server.del({ + path: '/:account/vpc/:id', + name: 'DeleteVPC', + version: ['9.13.0'] + }, before, ensureVPCEnabled, pre, deleteVPC); + + // --- VPC Networks + + server.get({ + path: '/:account/vpc/:vpc_id/networks', + name: 'ListVPCNetworks', + version: [ '9.13.0'] + }, before, ensureVPCEnabled, pre, listVPCNetworks); + + server.head({ + path: '/:account/vpc/:vpc_id/networks', + name: 'HeadVPCNetworks', + version: [ '9.13.0'] + }, before, ensureVPCEnabled, pre, listVPCNetworks); + + server.post({ + path: '/:account/vpc/:vpc_id/networks', + name: 'CreateVPCNetwork', + version: ['9.13.0'] + }, before, ensureVPCEnabled, pre, createVPCNetwork); + + server.get({ + path: '/:account/vpc/:vpc_id/networks/:id', + name: 'GetVPCNetwork', + version: ['9.13.0'] + }, before, ensureVPCEnabled, pre, getVPCNetwork); + + server.head({ + path: '/:account/vpc/:vpc_id/networks/:id', + name: 'GetVPCNetwork', + version: ['9.13.0'] + }, before, ensureVPCEnabled, pre, getVPCNetwork); + + server.put({ + path: '/:account/vpc/:vpc_id/networks/:id', + name: 'UpdateVPCNetwork', + version: ['9.13.0'] + }, before, ensureVPCEnabled, pre, updateVPCNetwork); + + server.del({ + path: '/:account/vpc/:vpc_id/networks/:id', + name: 'DeleteVPCNetwork', + version: ['9.13.0'] + }, before, ensureVPCEnabled, pre, deleteVPCNetwork); + // --- Networks (non-fabric) server.get({ diff --git a/package.json b/package.json index 1433ae34..5d5712d7 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "cueball": "2.1.1", "filed": "0.0.7", "http-signature": "1.1.0", - "joyent-schemas": "git+https://github.com/joyent/schemas.git#dd1c3cbfae6e2aa6ceafbe003bd95fb5e579e748", + "joyent-schemas": "git+https://github.com/joyent/schemas.git#vpc", "jsprim": "2.0.0", "kang": "1.1.0", "keyapi": "git+https://github.com/joyent/keyapi.git#e14b3d582e1d9d338b7082d61f34ba8d1bbc540a", @@ -36,7 +36,7 @@ "nodemailer": "0.7.1", "nopt": "2.0.0", "restify": "4.3.3", - "sdc-clients": "git+https://github.com/joyent/node-sdc-clients.git#01b9136799e4c76e26c59dd266cd5435cccde534", + "sdc-clients": "git+https://github.com/joyent/node-sdc-clients.git#vpc", "semver": "5.4.1", "triton-metrics": "0.1.0", "triton-netconfig": "1.3.0", diff --git a/sapi_manifests/cloudapi/template b/sapi_manifests/cloudapi/template index fa1fc9a3..b4837068 100644 --- a/sapi_manifests/cloudapi/template +++ b/sapi_manifests/cloudapi/template @@ -172,6 +172,13 @@ "fabric_package_prefixes": {{{"CLOUDAPI_FABRIC_PACKAGE_PREFIXES}}}, {{/"CLOUDAPI_FABRIC_PACKAGE_PREFIXES}} + {{^vpc_cfg}} + "vpc_enabled": false, + {{/vpc_cfg}} + {{#vpc_cfg}} + "vpc_enabled": true, + {{/vpc_enabled}} + {{^CLOUDAPI_DEFAULT_CONTAINER_BRAND}} "default_container_brand": "joyent", {{/CLOUDAPI_DEFAULT_CONTAINER_BRAND}} From 8d85dea4bac9d4c711b7b409c3794056dd3df3d1 Mon Sep 17 00:00:00 2001 From: Jason King Date: Wed, 9 Dec 2020 23:01:16 -0600 Subject: [PATCH 3/6] VPC quotas --- lib/endpoints/networks.js | 30 +++++++++++++++++++++++++----- lib/networks.js | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/lib/endpoints/networks.js b/lib/endpoints/networks.js index 01eca49b..9b3bdcae 100644 --- a/lib/endpoints/networks.js +++ b/lib/endpoints/networks.js @@ -904,14 +904,34 @@ function createVPC(req, res, next) { params.fields = VPC_FIELDS; params.owner_uuid = req.account.uuid; - req.sdc.napi.createVPC(params, reqOpts(req), function onCreate(err, vpc) { - if (err) { - next(translateErr(err)); + modNetworks.getVPCQuotaForUser(req.sdc.ufds, + req.config.datacenter_name, req.account, { + log: req.log + }, function _afterGetConf(getVPCQuotaErr, vpcQuota) { + if (getVPCQuotaErr) { + next(getVPCQuotaErr); return; } - res.send(201, vpc); - next(); + if (vpcQuota) { + assert.number(vpcQuota.quota); + params.quota = vpcQuota.quota; + } + + req.log.info({ + vpcQuota: vpcQuota.quota + }, 'User VPC quota'); + + req.sdc.napi.createVPC(params, reqOpts(req), + function onCreate(err, vpc) { + if (err) { + next(translateErr(err)); + return; + } + + res.send(201, vpc); + next(); + }); }); } diff --git a/lib/networks.js b/lib/networks.js index caacdfa6..8d05a27f 100644 --- a/lib/networks.js +++ b/lib/networks.js @@ -102,8 +102,43 @@ function getDefaultFabricNetworkForUser(ufdsClient, dataCenterName, account, }); } +function getVPCQuotaForUser(ufdsClient, dataCenterName, accountUuid, + options, cb) { + assert.object(ufdsClient, 'ufdsClient'); + assert.string(dataCenterName, 'dataCenterName'); + assert.object(accountUuid, 'accountUuid'); + assert.object(options, 'options'); + assert.object(options.log, 'options.log'); + assert.func(cb, 'cb'); + + modConfig.getAccountDcConfigFromUFDS(ufdsClient, accountUuid, + dataCenterName, { + log: options.log + }, function onGetDcLocalConfig(getDcLocalConfigErr, conf) { + if (getDcLocalConfigErr) { + cb(getDcLocalConfigErr); + return; + } + options.log.info({conf: conf}, 'config'); + + if (!conf) { + cb(new Error('Could not get user VPC quota')); + return; + } + + // No quota set on the user implies we use the default + // A quota of '0' means no quota. + if (conf.vpcquota) { + cb(null, {quota: conf.vpcquota}); + return; + } + + cb(null, null); + }); +} module.exports = { checkFabricNetworks: checkFabricNetworks, - getDefaultFabricNetworkForUser: getDefaultFabricNetworkForUser + getDefaultFabricNetworkForUser: getDefaultFabricNetworkForUser, + getVPCQuotaForUser: getVPCQuotaForUser }; From 6978715bdbe6f69f30b067547d4534b23b7021b3 Mon Sep 17 00:00:00 2001 From: Jason King Date: Mon, 11 Jan 2021 21:21:40 -0600 Subject: [PATCH 4/6] version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d5712d7..39219541 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cloudapi", "description": "Triton CloudAPI", - "version": "9.14.0", + "version": "9.15.0", "author": "Joyent (joyent.com)", "private": true, "engines": { From c207c515dc99b21c18ad766558d889d11ac323f0 Mon Sep 17 00:00:00 2001 From: Jason King Date: Tue, 26 Jan 2021 11:23:45 -0500 Subject: [PATCH 5/6] Fix placeholder for VPC operations --- lib/endpoints/networks.js | 10 +++++----- lib/networks.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/endpoints/networks.js b/lib/endpoints/networks.js index 9b3bdcae..e2bbc0a5 100644 --- a/lib/endpoints/networks.js +++ b/lib/endpoints/networks.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2020 Joyent, Inc. + * Copyright 2021 Joyent, Inc. */ var assert = require('assert-plus'); @@ -1311,25 +1311,25 @@ function mountNetworks(server, before, pre) { }, before, ensureVPCEnabled, pre, createVPC); server.get({ - path: '/:account/vpc/:id', + path: '/:account/vpc/:vpc_id', name: 'GetVPC', version: ['9.13.0'] }, before, ensureVPCEnabled, pre, getVPC); server.head({ - path: '/:account/vpc/:id', + path: '/:account/vpc/:vpc_id', name: 'GetVPC', version: ['9.13.0'] }, before, ensureVPCEnabled, pre, getVPC); server.put({ - path: '/:account/vpc/:id', + path: '/:account/vpc/:vpc_id', name: 'UpdateVPC', version: ['9.13.0'] }, before, ensureVPCEnabled, pre, updateVPC); server.del({ - path: '/:account/vpc/:id', + path: '/:account/vpc/:vpc_id', name: 'DeleteVPC', version: ['9.13.0'] }, before, ensureVPCEnabled, pre, deleteVPC); diff --git a/lib/networks.js b/lib/networks.js index 8d05a27f..c8fba721 100644 --- a/lib/networks.js +++ b/lib/networks.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2020 Joyent, Inc. + * Copyright 2021 Joyent, Inc. */ var assert = require('assert-plus'); From d15b5abe9f1914128c9df726253d9aef22548c67 Mon Sep 17 00:00:00 2001 From: Jason King Date: Tue, 26 Jan 2021 11:25:18 -0500 Subject: [PATCH 6/6] Update schemas branch --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 39219541..7f887899 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "cueball": "2.1.1", "filed": "0.0.7", "http-signature": "1.1.0", - "joyent-schemas": "git+https://github.com/joyent/schemas.git#vpc", + "joyent-schemas": "git+https://github.com/joyent/schemas.git#vpc2", "jsprim": "2.0.0", "kang": "1.1.0", "keyapi": "git+https://github.com/joyent/keyapi.git#e14b3d582e1d9d338b7082d61f34ba8d1bbc540a", @@ -36,7 +36,7 @@ "nodemailer": "0.7.1", "nopt": "2.0.0", "restify": "4.3.3", - "sdc-clients": "git+https://github.com/joyent/node-sdc-clients.git#vpc", + "sdc-clients": "git+https://github.com/joyent/node-sdc-clients.git#vpc2", "semver": "5.4.1", "triton-metrics": "0.1.0", "triton-netconfig": "1.3.0",