|  | // 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" | 
|  |  | 
|  | "golang.org/x/sys/unix" | 
|  |  | 
|  | "source.monogon.dev/metropolis/pkg/fsquota/fsxattrs" | 
|  | "source.monogon.dev/metropolis/pkg/fsquota/quotactl" | 
|  | ) | 
|  |  | 
|  | // 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() | 
|  | 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(dir, 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(dir, quotactl.QuotaTypeProject, lastID, "actl.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() | 
|  | attrs, err := fsxattrs.Get(dir) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | if attrs.ProjectID == 0 { | 
|  | return nil, os.ErrNotExist | 
|  | } | 
|  | quota, err := quotactl.GetQuota(dir, quotactl.QuotaTypeProject, attrs.ProjectID) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | return &Quota{ | 
|  | Bytes:      quota.BHardLimit * 1024, | 
|  | BytesUsed:  quota.CurSpace, | 
|  | Inodes:     quota.IHardLimit, | 
|  | InodesUsed: quota.CurInodes, | 
|  | }, nil | 
|  | } |