blob: e4f3e92e09419ec854c9b3176d2831ddf3666f13 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Lorenz Brun1d801752020-04-02 09:24:51 +02002// SPDX-License-Identifier: Apache-2.0
Lorenz Brun1d801752020-04-02 09:24:51 +02003
Serge Bazanski216fe7b2021-05-21 18:36:16 +02004// Package fsquota provides a simplified interface to interact with Linux's
5// filesystem qouta API. It only supports setting quotas on directories, not
6// groups or users. Quotas need to be already enabled on the filesystem to be
7// able to use them using this package. See the quotactl package if you intend
8// to use this on a filesystem where quotas need to be enabled manually.
Lorenz Brun1d801752020-04-02 09:24:51 +02009package fsquota
10
11import (
Tim Windelschmidtd5f851b2024-04-23 14:59:37 +020012 "errors"
Lorenz Brun1d801752020-04-02 09:24:51 +020013 "fmt"
Lorenz Brun1d801752020-04-02 09:24:51 +020014 "os"
15
Lorenz Brun1d801752020-04-02 09:24:51 +020016 "golang.org/x/sys/unix"
Serge Bazanski77cb6c52020-12-19 00:09:22 +010017
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020018 "source.monogon.dev/osbase/fsquota/fsxattrs"
19 "source.monogon.dev/osbase/fsquota/quotactl"
Lorenz Brun1d801752020-04-02 09:24:51 +020020)
21
Serge Bazanski216fe7b2021-05-21 18:36:16 +020022// SetQuota sets the quota of bytes and/or inodes in a given path. To not set a
23// limit, set the corresponding argument to zero. Setting both arguments to
24// zero removes the quota entirely. This function can only be called on an
25// empty directory. It can't be used to create a quota below a directory which
26// already has a quota since Linux doesn't offer hierarchical quotas.
Lorenz Brun1d801752020-04-02 09:24:51 +020027func SetQuota(path string, maxBytes uint64, maxInodes uint64) error {
28 dir, err := os.Open(path)
29 if err != nil {
30 return err
31 }
32 defer dir.Close()
Lorenz Brun1d801752020-04-02 09:24:51 +020033 var valid uint32
34 if maxBytes > 0 {
35 valid |= quotactl.FlagBLimitsValid
36 }
37 if maxInodes > 0 {
38 valid |= quotactl.FlagILimitsValid
39 }
40
41 attrs, err := fsxattrs.Get(dir)
42 if err != nil {
43 return err
44 }
45
Tim Windelschmidt5e460a92024-04-11 01:33:09 +020046 var lastID = attrs.ProjectID
Lorenz Brun1d801752020-04-02 09:24:51 +020047 if lastID == 0 {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020048 // No project/quota exists for this directory, assign a new project
49 // quota.
50 // TODO(lorenz): This is racy, but the kernel does not support
51 // atomically assigning quotas. So this needs to be added to the
52 // kernels setquota interface. Due to the short time window and
53 // infrequent calls this should not be an immediate issue.
Lorenz Brun1d801752020-04-02 09:24:51 +020054 for {
Lorenz Brun531e2c22021-11-17 20:00:05 +010055 quota, err := quotactl.GetNextQuota(dir, quotactl.QuotaTypeProject, lastID)
Tim Windelschmidtd5f851b2024-04-23 14:59:37 +020056 if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ESRCH) {
Lorenz Brun1d801752020-04-02 09:24:51 +020057 // We have enumerated all quotas, nothing exists here
58 break
59 } else if err != nil {
60 return fmt.Errorf("failed to call GetNextQuota: %w", err)
61 }
62 if quota.ID > lastID+1 {
63 // Take the first ID in the quota ID gap
64 lastID++
65 break
66 }
67 lastID++
68 }
69 }
70
71 // If both limits are zero, this is a delete operation, process it as such
72 if maxBytes == 0 && maxInodes == 0 {
73 valid = quotactl.FlagBLimitsValid | quotactl.FlagILimitsValid
74 attrs.ProjectID = 0
75 attrs.Flags &= ^fsxattrs.FlagProjectInherit
76 } else {
77 attrs.ProjectID = lastID
78 attrs.Flags |= fsxattrs.FlagProjectInherit
79 }
80
81 if err := fsxattrs.Set(dir, attrs); err != nil {
82 return err
83 }
84
85 // Always round up to the nearest block size
Jan Schäre817a2a2025-03-06 17:46:25 +010086 bytesLimitBlocks := maxBytes / 1024
87 if bytesLimitBlocks*1024 < maxBytes {
88 bytesLimitBlocks += 1
89 }
Lorenz Brun1d801752020-04-02 09:24:51 +020090
Lorenz Brun531e2c22021-11-17 20:00:05 +010091 return quotactl.SetQuota(dir, quotactl.QuotaTypeProject, lastID, &quotactl.Quota{
Lorenz Brun1d801752020-04-02 09:24:51 +020092 BHardLimit: bytesLimitBlocks,
93 BSoftLimit: bytesLimitBlocks,
94 IHardLimit: maxInodes,
95 ISoftLimit: maxInodes,
96 Valid: valid,
97 })
98}
99
100type Quota struct {
101 Bytes uint64
102 BytesUsed uint64
103 Inodes uint64
104 InodesUsed uint64
105}
106
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200107// GetQuota returns the current active quota and its utilization at the given
108// path
Lorenz Brun1d801752020-04-02 09:24:51 +0200109func GetQuota(path string) (*Quota, error) {
110 dir, err := os.Open(path)
111 if err != nil {
112 return nil, err
113 }
114 defer dir.Close()
Lorenz Brun1d801752020-04-02 09:24:51 +0200115 attrs, err := fsxattrs.Get(dir)
116 if err != nil {
117 return nil, err
118 }
119 if attrs.ProjectID == 0 {
120 return nil, os.ErrNotExist
121 }
Lorenz Brun531e2c22021-11-17 20:00:05 +0100122 quota, err := quotactl.GetQuota(dir, quotactl.QuotaTypeProject, attrs.ProjectID)
Lorenz Brun1d801752020-04-02 09:24:51 +0200123 if err != nil {
124 return nil, err
125 }
126 return &Quota{
Lorenz Brun547b33f2020-04-23 15:27:06 +0200127 Bytes: quota.BHardLimit * 1024,
Lorenz Brun1d801752020-04-02 09:24:51 +0200128 BytesUsed: quota.CurSpace,
129 Inodes: quota.IHardLimit,
130 InodesUsed: quota.CurInodes,
131 }, nil
132}