blob: 4919841e0f7f261fb1c6bad5eeec9859fcabaf9b [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"
14 "math"
15 "os"
16
Lorenz Brun1d801752020-04-02 09:24:51 +020017 "golang.org/x/sys/unix"
Serge Bazanski77cb6c52020-12-19 00:09:22 +010018
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020019 "source.monogon.dev/osbase/fsquota/fsxattrs"
20 "source.monogon.dev/osbase/fsquota/quotactl"
Lorenz Brun1d801752020-04-02 09:24:51 +020021)
22
Serge Bazanski216fe7b2021-05-21 18:36:16 +020023// SetQuota sets the quota of bytes and/or inodes in a given path. To not set a
24// limit, set the corresponding argument to zero. Setting both arguments to
25// zero removes the quota entirely. This function can only be called on an
26// empty directory. It can't be used to create a quota below a directory which
27// already has a quota since Linux doesn't offer hierarchical quotas.
Lorenz Brun1d801752020-04-02 09:24:51 +020028func SetQuota(path string, maxBytes uint64, maxInodes uint64) error {
29 dir, err := os.Open(path)
30 if err != nil {
31 return err
32 }
33 defer dir.Close()
Lorenz Brun1d801752020-04-02 09:24:51 +020034 var valid uint32
35 if maxBytes > 0 {
36 valid |= quotactl.FlagBLimitsValid
37 }
38 if maxInodes > 0 {
39 valid |= quotactl.FlagILimitsValid
40 }
41
42 attrs, err := fsxattrs.Get(dir)
43 if err != nil {
44 return err
45 }
46
Tim Windelschmidt5e460a92024-04-11 01:33:09 +020047 var lastID = attrs.ProjectID
Lorenz Brun1d801752020-04-02 09:24:51 +020048 if lastID == 0 {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020049 // No project/quota exists for this directory, assign a new project
50 // quota.
51 // TODO(lorenz): This is racy, but the kernel does not support
52 // atomically assigning quotas. So this needs to be added to the
53 // kernels setquota interface. Due to the short time window and
54 // infrequent calls this should not be an immediate issue.
Lorenz Brun1d801752020-04-02 09:24:51 +020055 for {
Lorenz Brun531e2c22021-11-17 20:00:05 +010056 quota, err := quotactl.GetNextQuota(dir, quotactl.QuotaTypeProject, lastID)
Tim Windelschmidtd5f851b2024-04-23 14:59:37 +020057 if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ESRCH) {
Lorenz Brun1d801752020-04-02 09:24:51 +020058 // We have enumerated all quotas, nothing exists here
59 break
60 } else if err != nil {
61 return fmt.Errorf("failed to call GetNextQuota: %w", err)
62 }
63 if quota.ID > lastID+1 {
64 // Take the first ID in the quota ID gap
65 lastID++
66 break
67 }
68 lastID++
69 }
70 }
71
72 // If both limits are zero, this is a delete operation, process it as such
73 if maxBytes == 0 && maxInodes == 0 {
74 valid = quotactl.FlagBLimitsValid | quotactl.FlagILimitsValid
75 attrs.ProjectID = 0
76 attrs.Flags &= ^fsxattrs.FlagProjectInherit
77 } else {
78 attrs.ProjectID = lastID
79 attrs.Flags |= fsxattrs.FlagProjectInherit
80 }
81
82 if err := fsxattrs.Set(dir, attrs); err != nil {
83 return err
84 }
85
86 // Always round up to the nearest block size
87 bytesLimitBlocks := uint64(math.Ceil(float64(maxBytes) / float64(1024)))
88
Lorenz Brun531e2c22021-11-17 20:00:05 +010089 return quotactl.SetQuota(dir, quotactl.QuotaTypeProject, lastID, &quotactl.Quota{
Lorenz Brun1d801752020-04-02 09:24:51 +020090 BHardLimit: bytesLimitBlocks,
91 BSoftLimit: bytesLimitBlocks,
92 IHardLimit: maxInodes,
93 ISoftLimit: maxInodes,
94 Valid: valid,
95 })
96}
97
98type Quota struct {
99 Bytes uint64
100 BytesUsed uint64
101 Inodes uint64
102 InodesUsed uint64
103}
104
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200105// GetQuota returns the current active quota and its utilization at the given
106// path
Lorenz Brun1d801752020-04-02 09:24:51 +0200107func GetQuota(path string) (*Quota, error) {
108 dir, err := os.Open(path)
109 if err != nil {
110 return nil, err
111 }
112 defer dir.Close()
Lorenz Brun1d801752020-04-02 09:24:51 +0200113 attrs, err := fsxattrs.Get(dir)
114 if err != nil {
115 return nil, err
116 }
117 if attrs.ProjectID == 0 {
118 return nil, os.ErrNotExist
119 }
Lorenz Brun531e2c22021-11-17 20:00:05 +0100120 quota, err := quotactl.GetQuota(dir, quotactl.QuotaTypeProject, attrs.ProjectID)
Lorenz Brun1d801752020-04-02 09:24:51 +0200121 if err != nil {
122 return nil, err
123 }
124 return &Quota{
Lorenz Brun547b33f2020-04-23 15:27:06 +0200125 Bytes: quota.BHardLimit * 1024,
Lorenz Brun1d801752020-04-02 09:24:51 +0200126 BytesUsed: quota.CurSpace,
127 Inodes: quota.IHardLimit,
128 InodesUsed: quota.CurInodes,
129 }, nil
130}