mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
dynamic host volumes quotas (#24871)
Allow users to configure a host volumes quota in MB. This will be enforced at the time of provisioning via create/register RPCs. This changeset is the CE version of ENT/2114. Ref: https://github.com/hashicorp/nomad-enterprise/pull/2114 Ref: https://hashicorp.atlassian.net/browse/NET-11549
This commit is contained in:
@@ -157,6 +157,11 @@ type QuotaStorageResources struct {
|
||||
// Variable.EncryptedData, in megabytes (2^20 bytes). A value of zero is
|
||||
// treated as unlimited and a negative value is treated as fully disallowed.
|
||||
VariablesMB int `hcl:"variables"`
|
||||
|
||||
// HostVolumesMB is the maximum provisioned size of all dynamic host
|
||||
// volumes, in megabytes (2^20 bytes). A value of zero is treated as
|
||||
// unlimited and a negative value is treated as fully disallowed.
|
||||
HostVolumesMB int `hcl:"host_volumes"`
|
||||
}
|
||||
|
||||
// QuotaUsage is the resource usage of a Quota
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
multierror "github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/hcl"
|
||||
"github.com/hashicorp/hcl/hcl/ast"
|
||||
@@ -319,19 +320,44 @@ func parseStorageResource(storageBlocks *ast.ObjectList) (*api.QuotaStorageResou
|
||||
return nil, errors.New("only one storage block is allowed")
|
||||
}
|
||||
block := storageBlocks.Items[0]
|
||||
valid := []string{"variables"}
|
||||
valid := []string{"variables", "host_volumes"}
|
||||
if err := helper.CheckHCLKeys(block.Val, valid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var storage api.QuotaStorageResources
|
||||
var m map[string]interface{}
|
||||
if err := hcl.DecodeObject(&storage, block.Val); err != nil {
|
||||
|
||||
var m map[string]any
|
||||
if err := hcl.DecodeObject(&m, block.Val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mapstructure.WeakDecode(m, &storage); err != nil {
|
||||
return nil, err
|
||||
|
||||
variablesLimit, err := parseQuotaMegabytes(m["variables"])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid variables limit: %v", err)
|
||||
}
|
||||
hostVolumesLimit, err := parseQuotaMegabytes(m["host_volumes"])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid host_volumes limit: %v", err)
|
||||
}
|
||||
|
||||
return &api.QuotaStorageResources{
|
||||
VariablesMB: variablesLimit,
|
||||
HostVolumesMB: hostVolumesLimit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseQuotaMegabytes(raw any) (int, error) {
|
||||
switch val := raw.(type) {
|
||||
case string:
|
||||
b, err := humanize.ParseBytes(val)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("could not parse value as bytes: %v", err)
|
||||
}
|
||||
return int(b >> 20), nil
|
||||
case int:
|
||||
return val, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid type %T", raw)
|
||||
}
|
||||
return &storage, nil
|
||||
}
|
||||
|
||||
func parseDeviceResource(result *[]*api.RequestedDevice, list *ast.ObjectList) error {
|
||||
|
||||
@@ -8,7 +8,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/cli"
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func TestQuotaApplyCommand_Implements(t *testing.T) {
|
||||
@@ -38,3 +41,53 @@ func TestQuotaApplyCommand_Fails(t *testing.T) {
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
}
|
||||
|
||||
func TestQuotaParse(t *testing.T) {
|
||||
|
||||
in := []byte(`
|
||||
name = "default-quota"
|
||||
description = "Limit the shared default namespace"
|
||||
|
||||
limit {
|
||||
region = "global"
|
||||
region_limit {
|
||||
cores = 0
|
||||
cpu = 2500
|
||||
memory = 1000
|
||||
memory_max = 1000
|
||||
device "nvidia/gpu/1080ti" {
|
||||
count = 1
|
||||
}
|
||||
storage {
|
||||
variables = 1000 # in MB
|
||||
host_volumes = "100 GiB"
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
spec, err := parseQuotaSpec(in)
|
||||
must.NoError(t, err)
|
||||
|
||||
must.Eq(t, &api.QuotaSpec{
|
||||
Name: "default-quota",
|
||||
Description: "Limit the shared default namespace",
|
||||
Limits: []*api.QuotaLimit{{
|
||||
Region: "global",
|
||||
RegionLimit: &api.QuotaResources{
|
||||
CPU: pointer.Of(2500),
|
||||
Cores: pointer.Of(0),
|
||||
MemoryMB: pointer.Of(1000),
|
||||
MemoryMaxMB: pointer.Of(1000),
|
||||
Devices: []*api.RequestedDevice{{
|
||||
Name: "nvidia/gpu/1080ti",
|
||||
Count: pointer.Of(uint64(1)),
|
||||
}},
|
||||
Storage: &api.QuotaStorageResources{
|
||||
VariablesMB: 1000,
|
||||
HostVolumesMB: 102_400,
|
||||
},
|
||||
},
|
||||
}},
|
||||
}, spec)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,8 @@ limit {
|
||||
count = 1
|
||||
}
|
||||
storage {
|
||||
variables = 1000
|
||||
variables = 1000 # in MB
|
||||
host_volumes = 100000 # in MB
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,9 +152,10 @@ var defaultJsonQuotaSpec = strings.TrimSpace(`
|
||||
"Count": 1
|
||||
}
|
||||
],
|
||||
"Storage": {
|
||||
"Variables": 1000
|
||||
}
|
||||
"Storage": {
|
||||
"Variables": 1000,
|
||||
"HostVolumes": 100000
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -66,14 +66,20 @@ func (s *StateStore) UpsertHostVolume(index uint64, vol *structs.HostVolume) err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var old *structs.HostVolume
|
||||
if obj != nil {
|
||||
old := obj.(*structs.HostVolume)
|
||||
old = obj.(*structs.HostVolume)
|
||||
vol.CreateIndex = old.CreateIndex
|
||||
vol.CreateTime = old.CreateTime
|
||||
} else {
|
||||
vol.CreateIndex = index
|
||||
}
|
||||
|
||||
err = s.enforceHostVolumeQuotaTxn(txn, index, vol, old, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the fingerprint is written from the node before the create RPC handler
|
||||
// completes, we'll never update from the initial pending, so reconcile that
|
||||
// here
|
||||
|
||||
16
nomad/state/state_store_host_volumes_ce.go
Normal file
16
nomad/state/state_store_host_volumes_ce.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !ent
|
||||
|
||||
package state
|
||||
|
||||
import "github.com/hashicorp/nomad/nomad/structs"
|
||||
|
||||
func (s *StateStore) EnforceHostVolumeQuota(_ *structs.HostVolume, _ *structs.HostVolume) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StateStore) enforceHostVolumeQuotaTxn(_ Txn, _ uint64, _ *structs.HostVolume, _ *structs.HostVolume, _ bool) error {
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user