[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