[lxc-devel] [lxd/master] lxd: project limits.containers and limits.virtual-machines support
rcash on Github
lxc-bot at linuxcontainers.org
Wed Dec 11 20:45:13 UTC 2019
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 446 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20191211/6937b071/attachment-0001.bin>
-------------- next part --------------
From 8c7c50a369c5f40e52a7cec8d9840fb19932331a Mon Sep 17 00:00:00 2001
From: Rowdy <rowdycash at gmail.com>
Date: Tue, 3 Dec 2019 18:22:26 -0600
Subject: [PATCH] limits.containers and limits.virtual-machines support
Signed-off-by: Rowdy <rowdycash at gmail.com>
---
lxd/api_project.go | 91 +++++++++++++++++++++++++-
lxd/containers_post.go | 134 +++++++++++++++++++++++++++++++++++++++
lxd/db/cluster/open.go | 2 +
lxd/db/cluster/update.go | 2 +
lxd/db/containers.go | 10 +++
lxd/db/migration.go | 2 +
lxd/db/projects.go | 22 +++++++
7 files changed, 262 insertions(+), 1 deletion(-)
diff --git a/lxd/api_project.go b/lxd/api_project.go
index 30633beaed..3c5091a7ba 100644
--- a/lxd/api_project.go
+++ b/lxd/api_project.go
@@ -4,8 +4,10 @@ import (
"bytes"
"encoding/json"
"fmt"
+ "github.com/lxc/lxd/shared/logger"
"io/ioutil"
"net/http"
+ "strconv"
"strings"
"github.com/gorilla/mux"
@@ -206,6 +208,8 @@ func projectGet(d *Daemon, r *http.Request) response.Response {
project.Description,
project.Config["features.images"],
project.Config["features.profiles"],
+ project.Config["limits.containers"],
+ project.Config["limits.virtual-machines"],
}
return response.SyncResponseETag(true, project, etag)
@@ -235,6 +239,8 @@ func projectPut(d *Daemon, r *http.Request) response.Response {
project.Description,
project.Config["features.images"],
project.Config["features.profiles"],
+ project.Config["limits.containers"],
+ project.Config["limits.virtual-machines"],
}
err = util.EtagCheck(r, etag)
if err != nil {
@@ -276,6 +282,8 @@ func projectPatch(d *Daemon, r *http.Request) response.Response {
project.Description,
project.Config["features.images"],
project.Config["features.profiles"],
+ project.Config["limits.containers"],
+ project.Config["limits.virtual-machines"],
}
err = util.EtagCheck(r, etag)
if err != nil {
@@ -316,19 +324,36 @@ func projectPatch(d *Daemon, r *http.Request) response.Response {
req.Config["features.images"] = project.Config["features.profiles"]
}
+ _, err = reqRaw.GetString("limits.containers")
+ if err != nil {
+ req.Config["limits.containers"] = project.Config["limits.containers"]
+ }
+
+ _, err = reqRaw.GetString("limits.virtual-machines")
+ if err != nil {
+ req.Config["limits.virtual-machines"] = project.Config["limits.virtual-machines"]
+ }
+
return projectChange(d, project, req)
}
-
// Common logic between PUT and PATCH.
func projectChange(d *Daemon, project *api.Project, req api.ProjectPut) response.Response {
// Flag indicating if any feature has changed.
featuresChanged := req.Config["features.images"] != project.Config["features.images"] || req.Config["features.profiles"] != project.Config["features.profiles"]
+ // Flag indicating if any limit has changed.
+ containerLimitChanged := req.Config["limits.containers"] != project.Config["limits.containers"]
+ vmLimitChanged := req.Config["limits.virtual-machines"] != project.Config["limits.virtual-machines"]
+ limitsChanged := containerLimitChanged || vmLimitChanged
// Sanity checks
if project.Name == "default" && featuresChanged {
return response.BadRequest(fmt.Errorf("You can't change the features of the default project"))
}
+ if project.Name == "default" && limitsChanged {
+ return response.BadRequest(fmt.Errorf("You can't change the limits of the default project"))
+ }
+
if !projectIsEmpty(project) && featuresChanged {
return response.BadRequest(fmt.Errorf("Features can only be changed on empty projects"))
}
@@ -339,6 +364,56 @@ func projectChange(d *Daemon, project *api.Project, req api.ProjectPut) response
return response.BadRequest(err)
}
+ if containerLimitChanged {
+ var names []string
+ err := d.cluster.Transaction(func(tx *db.ClusterTx) error {
+ var err error
+ names, err = tx.ContainerNames(project.Name)
+ return err
+ })
+ if err != nil {
+ logger.Errorf("Failed to grab container names for current project")
+ }
+ numberOfContainers := uint64(len(names))
+
+ v, ok := req.Config["limits.containers"]
+ if ok && v != "" {
+ requestedContainerLimit, err := strconv.ParseUint(v, 10, 0)
+ if err != nil {
+ return response.SmartError(err)
+ }
+ if requestedContainerLimit < numberOfContainers {
+ return response.BadRequest(fmt.Errorf("You can't change the project container limit to less " +
+ "than the current number of containers"))
+ }
+ }
+ }
+
+ if vmLimitChanged {
+ var names []string
+ err := d.cluster.Transaction(func(tx *db.ClusterTx) error {
+ var err error
+ names, err = tx.VMNames(project.Name)
+ return err
+ })
+ if err != nil {
+ logger.Errorf("Failed to grab virtual-machine names for current project")
+ }
+ numberOfVMs := uint64(len(names))
+
+ v, ok := req.Config["limits.virtual-machines"]
+ if ok && v != "" {
+ requestedVMLimit, err := strconv.ParseUint(v, 10, 0)
+ if err != nil {
+ return response.SmartError(err)
+ }
+ if requestedVMLimit < numberOfVMs {
+ return response.BadRequest(fmt.Errorf("You can't change the project virtual machine limit to " +
+ "less than the current number of containers"))
+ }
+ }
+ }
+
// Update the database entry
err = d.cluster.Transaction(func(tx *db.ClusterTx) error {
err := tx.ProjectUpdate(project.Name, req)
@@ -495,6 +570,20 @@ func projectIsEmpty(project *api.Project) bool {
var projectConfigKeys = map[string]func(value string) error{
"features.profiles": shared.IsBool,
"features.images": shared.IsBool,
+ "limits.containers": func(value string) error {
+ if value == "" {
+ return nil
+ }
+ _, err := strconv.ParseUint(value, 10, 0)
+ return err
+ },
+ "limits.virtual-machines": func(value string) error {
+ if value == "" {
+ return nil
+ }
+ _, err := strconv.ParseUint(value, 10, 0)
+ return err
+ },
}
func projectValidateConfig(config map[string]string) error {
diff --git a/lxd/containers_post.go b/lxd/containers_post.go
index 5da87077af..23af64bc8d 100644
--- a/lxd/containers_post.go
+++ b/lxd/containers_post.go
@@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
"os"
+ "strconv"
"strings"
"github.com/dustinkirkland/golang-petname"
@@ -41,6 +42,65 @@ func createFromImage(d *Daemon, project string, req *api.InstancesPost) response
return response.BadRequest(err)
}
+ if req.Source.Fingerprint != "" {
+ hash = req.Source.Fingerprint
+ } else if req.Source.Alias != "" {
+ if req.Source.Server != "" {
+ hash = req.Source.Alias
+ } else {
+ _, alias, err := d.cluster.ImageAliasGet(project, req.Source.Alias, true)
+ if err != nil {
+ return response.SmartError(err)
+ }
+
+ hash = alias.Target
+ }
+ } else if req.Source.Properties != nil {
+ if req.Source.Server != "" {
+ return response.BadRequest(fmt.Errorf("Property match is only supported for local images"))
+ }
+
+ hashes, err := d.cluster.ImagesGet(project, false)
+ if err != nil {
+ return response.SmartError(err)
+ }
+
+ var image *api.Image
+
+ for _, imageHash := range hashes {
+ _, img, err := d.cluster.ImageGet(project, imageHash, false, true)
+ if err != nil {
+ continue
+ }
+
+ if image != nil && img.CreatedAt.Before(image.CreatedAt) {
+ continue
+ }
+
+ match := true
+ for key, value := range req.Source.Properties {
+ if img.Properties[key] != value {
+ match = false
+ break
+ }
+ }
+
+ if !match {
+ continue
+ }
+
+ image = img
+ }
+
+ if image == nil {
+ return response.BadRequest(fmt.Errorf("No matching image could be found"))
+ }
+
+ hash = image.Fingerprint
+ } else {
+ return response.BadRequest(fmt.Errorf("Must specify one of alias, fingerprint or properties for init from image"))
+ }
+
dbType, err := instancetype.New(string(req.Type))
if err != nil {
return response.BadRequest(err)
@@ -590,6 +650,7 @@ func createFromCopy(d *Daemon, project string, req *api.InstancesPost) response.
}
func createFromBackup(d *Daemon, project string, data io.Reader, pool string) response.Response {
+
// Create temporary file to store uploaded backup data.
backupFile, err := ioutil.TempFile("", "lxd_backup_")
if err != nil {
@@ -766,6 +827,15 @@ func containersPost(d *Daemon, r *http.Request) response.Response {
return response.BadRequest(err)
}
+ lessInstancesThanLimit, err := instancesLessThanLimit(d, project, req.Type)
+ if err != nil {
+ return response.SmartError(err)
+ }
+ if !lessInstancesThanLimit {
+ return response.BadRequest(fmt.Errorf("Instance limit reached, unable to create new container/VM"))
+ }
+
+
targetNode := queryParam(r, "target")
if targetNode == "" {
// If no target node was specified, pick the node with the
@@ -936,6 +1006,70 @@ func containerFindStoragePool(d *Daemon, project string, req *api.InstancesPost)
return storagePool, storagePoolProfile, localRootDiskDeviceKey, localRootDiskDevice, nil
}
+//Check instance limit for project, if it exists
+func instancesLessThanLimit(d *Daemon, project string, instance api.InstanceType) (bool, error) {
+ if instance == api.InstanceTypeAny {
+ err := errors.New("Any is not currently supported for this method!")
+ return false, err
+ }
+
+ var names []string
+
+ if instance == api.InstanceTypeContainer {
+ err := d.cluster.Transaction(func(tx *db.ClusterTx) error {
+ var err error
+ names, err = tx.ContainerNames(project)
+ return err
+ })
+ if err != nil {
+ logger.Errorf("Failed to grab container names for given project")
+ }
+ } else if instance == api.InstanceTypeVM {
+ err := d.cluster.Transaction(func(tx *db.ClusterTx) error {
+ var err error
+ names, err = tx.VMNames(project)
+ return err
+ })
+ if err != nil {
+ logger.Errorf("Failed to grab vm names for given project")
+ }
+ }
+
+ numberOfSpecifiedInstances := uint64(len(names))
+
+ var projectInstanceLimit string
+ if instance == api.InstanceTypeContainer {
+ err := d.cluster.Transaction(func(tx *db.ClusterTx) error {
+ var err error
+ projectInstanceLimit, err = tx.ProjectGetContainerLimit(project)
+ return err
+ })
+ if err != nil {
+ return false, err
+ }
+ } else if instance == api.InstanceTypeVM {
+ err := d.cluster.Transaction(func(tx *db.ClusterTx) error {
+ var err error
+ projectInstanceLimit, err = tx.ProjectGetVMLimit(project)
+ return err
+ })
+ if err != nil {
+ return false, err
+ }
+ }
+
+ if projectInstanceLimit != "" {
+ limit, err := strconv.ParseUint(projectInstanceLimit, 10, 0)
+ if err != nil {
+ logger.Errorf("Failed to parse uint %s", err)
+ return false, err
+ }
+ return numberOfSpecifiedInstances < limit, nil
+ }
+
+ //project container limit not set therefore return true
+ return true, nil
+}
func clusterCopyContainerInternal(d *Daemon, source instance.Instance, project string, req *api.InstancesPost) response.Response {
name := req.Source.Source
diff --git a/lxd/db/cluster/open.go b/lxd/db/cluster/open.go
index 0ce55f6a0b..7f29db25c8 100644
--- a/lxd/db/cluster/open.go
+++ b/lxd/db/cluster/open.go
@@ -184,6 +184,8 @@ INSERT INTO nodes(id, name, address, schema, api_extensions, arch) VALUES(1, 'no
INSERT INTO projects (name, description) VALUES ('default', 'Default LXD project');
INSERT INTO projects_config (project_id, key, value) VALUES (1, 'features.images', 'true');
INSERT INTO projects_config (project_id, key, value) VALUES (1, 'features.profiles', 'true');
+INSERT INTO projects_config (project_id, key, value) VALUES (1, 'limits.containers', '');
+INSERT INTO projects_config (project_id, key, value) VALUES (1, 'limits.virtual-machines', '');
`
_, err = tx.Exec(stmt)
if err != nil {
diff --git a/lxd/db/cluster/update.go b/lxd/db/cluster/update.go
index 5e2114197c..2310dfb391 100644
--- a/lxd/db/cluster/update.go
+++ b/lxd/db/cluster/update.go
@@ -754,6 +754,8 @@ CREATE VIEW projects_config_ref (name, key, value) AS
INSERT INTO projects (name, description) VALUES ('default', 'Default LXD project');
INSERT INTO projects_config (project_id, key, value) VALUES (1, 'features.images', 'true');
INSERT INTO projects_config (project_id, key, value) VALUES (1, 'features.profiles', 'true');
+INSERT INTO projects_config (project_id, key, value) VALUES (1, 'limits.containers', '');
+INSERT INTO projects_config (project_id, key, value) VALUES (1, 'limits.virtual-machines', '');
-- Add a project_id column to all tables that need to be project-scoped.
-- The column is added without the FOREIGN KEY constraint
diff --git a/lxd/db/containers.go b/lxd/db/containers.go
index dd9b8e4644..fe1eb8d895 100644
--- a/lxd/db/containers.go
+++ b/lxd/db/containers.go
@@ -180,6 +180,16 @@ SELECT instances.name FROM instances
return query.SelectStrings(c.tx, stmt, project, instancetype.Container)
}
+// vmNames returns the names of all virtual machines the given project
+func (c *ClusterTx) VMNames(project string) ([]string, error) {
+ stmt := `
+SELECT instances.name FROM instances
+ JOIN projects ON projects.id = instances.project_id
+ WHERE projects.name = ? AND instances.type = ?
+`
+ return query.SelectStrings(c.tx, stmt, project, instancetype.VM)
+}
+
// ContainerNodeAddress returns the address of the node hosting the container
// with the given name in the given project.
//
diff --git a/lxd/db/migration.go b/lxd/db/migration.go
index 12d4624da0..40a301761d 100644
--- a/lxd/db/migration.go
+++ b/lxd/db/migration.go
@@ -125,6 +125,8 @@ INSERT INTO nodes(id, name, address, schema, api_extensions) VALUES(1, 'none', '
INSERT INTO projects (name, description) VALUES ('default', 'Default LXD project');
INSERT INTO projects_config (project_id, key, value) VALUES (1, 'features.images', 'true');
INSERT INTO projects_config (project_id, key, value) VALUES (1, 'features.profiles', 'true');
+INSERT INTO projects_config (project_id, key, value) VALUES (1, 'limits.containers', '');
+INSERT INTO projects_config (project_id, key, value) VALUES (1, 'limits.virtual-machines', '');
`
_, err = tx.Exec(stmt)
if err != nil {
diff --git a/lxd/db/projects.go b/lxd/db/projects.go
index 39bde7bfd9..5a1e34bdac 100644
--- a/lxd/db/projects.go
+++ b/lxd/db/projects.go
@@ -128,6 +128,28 @@ func (c *ClusterTx) ProjectHasImages(name string) (bool, error) {
return enabled, nil
}
+func (c* ClusterTx) ProjectGetContainerLimit(name string) (string, error) {
+ project, err := c.ProjectGet(name)
+ if err != nil {
+ return "", errors.Wrap(err, "fetch project")
+ }
+
+ containerlimit := project.Config["limits.containers"]
+
+ return containerlimit, nil
+}
+
+func (c* ClusterTx) ProjectGetVMLimit(name string) (string, error) {
+ project, err := c.ProjectGet(name)
+ if err != nil {
+ return "", errors.Wrap(err, "fetch project")
+ }
+
+ vmLimit := project.Config["limits.virtual-machines"]
+
+ return vmLimit, nil
+}
+
// ProjectUpdate updates the project matching the given key parameters.
func (c *ClusterTx) ProjectUpdate(name string, object api.ProjectPut) error {
stmt := c.stmt(projectUpdate)
More information about the lxc-devel
mailing list