blob: f4f4050b6ee495e55e205fcf161624c7bc4fb871 [file] [log] [blame]
// Copyright 2020 The Monogon Project Authors.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package fsquota provides a simplified interface to interact with Linux's filesystem qouta API.
// It only supports setting quotas on directories, not groups or users.
// Quotas need to be already enabled on the filesystem to be able to use them using this package.
// See the quotactl package if you intend to use this on a filesystem where quotas need to be
// enabled manually.
package fsquota
import (
"fmt"
"math"
"os"
"git.monogon.dev/source/nexantic.git/core/pkg/fsquota/fsxattrs"
"git.monogon.dev/source/nexantic.git/core/pkg/fsquota/quotactl"
"golang.org/x/sys/unix"
)
// SetQuota sets the quota of bytes and/or inodes in a given path. To not set a limit, set the
// corresponding argument to zero. Setting both arguments to zero removes the quota entirely.
// This function can only be called on an empty directory. It can't be used to create a quota
// below a directory which already has a quota since Linux doesn't offer hierarchical quotas.
func SetQuota(path string, maxBytes uint64, maxInodes uint64) error {
dir, err := os.Open(path)
if err != nil {
return err
}
defer dir.Close()
source, err := fsinfoGetSource(dir)
if err != nil {
return err
}
var valid uint32
if maxBytes > 0 {
valid |= quotactl.FlagBLimitsValid
}
if maxInodes > 0 {
valid |= quotactl.FlagILimitsValid
}
attrs, err := fsxattrs.Get(dir)
if err != nil {
return err
}
var lastID uint32 = attrs.ProjectID
if lastID == 0 {
// No project/quota exists for this directory, assign a new project quota
// TODO(lorenz): This is racy, but the kernel does not support atomically assigning
// quotas. So this needs to be added to the kernels setquota interface. Due to the short
// time window and infrequent calls this should not be an immediate issue.
for {
quota, err := quotactl.GetNextQuota(source, quotactl.QuotaTypeProject, lastID)
if err == unix.ENOENT || err == unix.ESRCH {
// We have enumerated all quotas, nothing exists here
break
} else if err != nil {
return fmt.Errorf("failed to call GetNextQuota: %w", err)
}
if quota.ID > lastID+1 {
// Take the first ID in the quota ID gap
lastID++
break
}
lastID++
}
}
// If both limits are zero, this is a delete operation, process it as such
if maxBytes == 0 && maxInodes == 0 {
valid = quotactl.FlagBLimitsValid | quotactl.FlagILimitsValid
attrs.ProjectID = 0
attrs.Flags &= ^fsxattrs.FlagProjectInherit
} else {
attrs.ProjectID = lastID
attrs.Flags |= fsxattrs.FlagProjectInherit
}
if err := fsxattrs.Set(dir, attrs); err != nil {
return err
}
// Always round up to the nearest block size
bytesLimitBlocks := uint64(math.Ceil(float64(maxBytes) / float64(1024)))
return quotactl.SetQuota(source, quotactl.QuotaTypeProject, lastID, &quotactl.Quota{
BHardLimit: bytesLimitBlocks,
BSoftLimit: bytesLimitBlocks,
IHardLimit: maxInodes,
ISoftLimit: maxInodes,
Valid: valid,
})
}
type Quota struct {
Bytes uint64
BytesUsed uint64
Inodes uint64
InodesUsed uint64
}
// GetQuota returns the current active quota and its utilization at the given path
func GetQuota(path string) (*Quota, error) {
dir, err := os.Open(path)
if err != nil {
return nil, err
}
defer dir.Close()
source, err := fsinfoGetSource(dir)
if err != nil {
return nil, err
}
attrs, err := fsxattrs.Get(dir)
if err != nil {
return nil, err
}
if attrs.ProjectID == 0 {
return nil, os.ErrNotExist
}
quota, err := quotactl.GetQuota(source, quotactl.QuotaTypeProject, attrs.ProjectID)
if err != nil {
return nil, err
}
return &Quota{
Bytes: quota.BHardLimit,
BytesUsed: quota.CurSpace,
Inodes: quota.IHardLimit,
InodesUsed: quota.CurInodes,
}, nil
}