Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Per-project uplink IP quotas #14631

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2538,3 +2538,8 @@ Adds a new {config:option}`device-unix-hotplug-device-conf:subsystem` configurat

## `storage_ceph_osd_pool_size`
This introduces the configuration keys {config:option}`storage-ceph-pool-conf:ceph.osd.pool_size`, and {config:option}`storage-cephfs-pool-conf:cephfs.osd_pool_size` to be used when adding or updating a `ceph` or `cephfs` storage pool to instruct LXD to create set the replication size for the underlying OSD pools.

## `projects_limits_uplink_ips`

Introduces per-network project uplink IP limits, adding a `limits.networks.uplink_ips.NETWORK_NAME` configuration key for projects with `features.networks` enabled.
This key defines the maximum value of IPs made available on a network named NETWORK_NAME to be assigned as uplink IPs for entities inside a certain project. These entities can be other networks, network forwards or load balancers.
9 changes: 9 additions & 0 deletions doc/metadata.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4053,6 +4053,15 @@ The value is the maximum value for the sum of the individual {config:option}`ins

```

```{config:option} limits.networks.uplink_ips.NETWORK_NAME project-limits
:shortdesc: "Quota of IPs on a certain network used by entities on this project"
:type: "string"
This represents the maximum value for IPs made available on a network
named NETWORK_NAME to be assigned as uplink addresses for entities inside
a specific project.

```

```{config:option} limits.processes project-limits
:shortdesc: "Maximum number of processes within the project"
:type: "integer"
Expand Down
2 changes: 1 addition & 1 deletion doc/rest-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3257,7 +3257,7 @@ definitions:
$ref: '#/definitions/NetworkForwardPort'
type: array
x-go-name: Ports
title: NetworkForward used for displaying an network address forward.
title: NetworkForward used for displaying a network address forward.
type: object
x-go-package: github.com/canonical/lxd/shared/api
NetworkForwardPort:
Expand Down
58 changes: 55 additions & 3 deletions lxd/api_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/gorilla/mux"
Expand Down Expand Up @@ -294,7 +295,7 @@ func projectsPost(d *Daemon, r *http.Request) response.Response {
}

// Validate the configuration.
err = projectValidateConfig(s, project.Config)
err = projectValidateConfig(s, project.Config, project.Name)
if err != nil {
return response.BadRequest(err)
}
Expand Down Expand Up @@ -682,7 +683,7 @@ func projectChange(s *state.State, project *api.Project, req api.ProjectPut) res
}

// Validate the configuration.
err := projectValidateConfig(s, req.Config)
err := projectValidateConfig(s, req.Config, project.Name)
if err != nil {
return response.BadRequest(err)
}
Expand Down Expand Up @@ -952,6 +953,35 @@ func projectStateGet(d *Daemon, r *http.Request) response.Response {
return response.SyncResponse(true, &state)
}

// uplinkIPLimitValidator creates a validator function that checks whether the provided value for uplink IP limits is valid.
// This check can be expensive so we only do the most expensive checks when actually validating a provided config key.
func uplinkIPLimitValidator(s *state.State, projectName string, networkName string) func(string) error {
return func(value string) error {
// Perform cheaper checks on the value first.
providedLimit, err := strconv.Atoi(value)
if err != nil {
return err
}

if providedLimit < 0 {
return fmt.Errorf("Value must be non-negative")
}

// Check if the provided value is equal or lower to the number of uplink addresses currently in use
// on the provided project and in the specified network.
invalidQuota, err := network.ProjectUplinkAddressThresholdExceeded(s, projectName, networkName, providedLimit)
if err != nil {
return err
}

if invalidQuota {
return fmt.Errorf("Value %s is below current number of used uplink addresses", value)
}

return nil
}
}

// Check if a project is empty.
func projectIsEmpty(ctx context.Context, project *cluster.Project, tx *db.ClusterTx) (bool, error) {
instances, err := cluster.GetInstances(ctx, tx.Tx(), cluster.InstanceFilter{Project: &project.Name})
Expand Down Expand Up @@ -1024,7 +1054,7 @@ func isEitherAllowOrBlockOrManaged(value string) error {
return validate.Optional(validate.IsOneOf("block", "allow", "managed"))(value)
}

func projectValidateConfig(s *state.State, config map[string]string) error {
func projectValidateConfig(s *state.State, config map[string]string, projectName string) error {
// Validate the project configuration.
projectConfigKeys := map[string]func(value string) error{
// lxdmeta:generate(entities=project; group=specific; key=backups.compression_algorithm)
Expand Down Expand Up @@ -1414,6 +1444,28 @@ func projectValidateConfig(s *state.State, config map[string]string) error {
return fmt.Errorf("Failed loading storage pool names: %w", err)
}

// Per-network project limits for uplink IPs only make sense for projects with their own networks.
if shared.IsTrue(config["features.networks"]) {
// Get networks that are allowed to be used as uplinks by this project.
allowedUplinkNetworks, err := network.AllowedUplinkNetworks(s, config)
if err != nil {
return nil
}

// Add network-specific config keys.
for _, networkName := range allowedUplinkNetworks {
// lxdmeta:generate(entities=project; group=limits; key=limits.networks.uplink_ips.NETWORK_NAME)
// This represents the maximum value for IPs made available on a network
// named NETWORK_NAME to be assigned as uplink addresses for entities inside
// a specific project.
//
// ---
// type: string
// shortdesc: Quota of IPs on a certain network used by entities on this project
projectConfigKeys[fmt.Sprintf("limits.networks.uplink_ips.%s", networkName)] = uplinkIPLimitValidator(s, projectName, networkName)
}
}

for k, v := range config {
key := k

Expand Down
7 changes: 7 additions & 0 deletions lxd/metadata/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -4603,6 +4603,13 @@
"type": "integer"
}
},
{
"limits.networks.uplink_ips.NETWORK_NAME": {
"longdesc": "This represents the maximum value for IPs made available on a network\nnamed NETWORK_NAME to be assigned as uplink addresses for entities inside\na specific project.\n",
"shortdesc": "Quota of IPs on a certain network used by entities on this project",
"type": "string"
}
},
{
"limits.processes": {
"longdesc": "This value is the maximum value for the sum of the individual {config:option}`instance-resource-limits:limits.processes` configurations set on the instances of the project.",
Expand Down
Loading
Loading