blob: f4f4050b6ee495e55e205fcf161624c7bc4fb871 [file] [log] [blame]
Lorenz Brun1d801752020-04-02 09:24:51 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17// Package fsquota provides a simplified interface to interact with Linux's filesystem qouta API.
18// It only supports setting quotas on directories, not groups or users.
19// Quotas need to be already enabled on the filesystem to be able to use them using this package.
20// See the quotactl package if you intend to use this on a filesystem where quotas need to be
21// enabled manually.
22package fsquota
23
24import (
25 "fmt"
26 "math"
27 "os"
28
29 "git.monogon.dev/source/nexantic.git/core/pkg/fsquota/fsxattrs"
30 "git.monogon.dev/source/nexantic.git/core/pkg/fsquota/quotactl"
31 "golang.org/x/sys/unix"
32)
33
34// SetQuota sets the quota of bytes and/or inodes in a given path. To not set a limit, set the
35// corresponding argument to zero. Setting both arguments to zero removes the quota entirely.
36// This function can only be called on an empty directory. It can't be used to create a quota
37// below a directory which already has a quota since Linux doesn't offer hierarchical quotas.
38func SetQuota(path string, maxBytes uint64, maxInodes uint64) error {
39 dir, err := os.Open(path)
40 if err != nil {
41 return err
42 }
43 defer dir.Close()
44 source, err := fsinfoGetSource(dir)
45 if err != nil {
46 return err
47 }
48 var valid uint32
49 if maxBytes > 0 {
50 valid |= quotactl.FlagBLimitsValid
51 }
52 if maxInodes > 0 {
53 valid |= quotactl.FlagILimitsValid
54 }
55
56 attrs, err := fsxattrs.Get(dir)
57 if err != nil {
58 return err
59 }
60
61 var lastID uint32 = attrs.ProjectID
62 if lastID == 0 {
63 // No project/quota exists for this directory, assign a new project quota
64 // TODO(lorenz): This is racy, but the kernel does not support atomically assigning
65 // quotas. So this needs to be added to the kernels setquota interface. Due to the short
66 // time window and infrequent calls this should not be an immediate issue.
67 for {
68 quota, err := quotactl.GetNextQuota(source, quotactl.QuotaTypeProject, lastID)
69 if err == unix.ENOENT || err == unix.ESRCH {
70 // We have enumerated all quotas, nothing exists here
71 break
72 } else if err != nil {
73 return fmt.Errorf("failed to call GetNextQuota: %w", err)
74 }
75 if quota.ID > lastID+1 {
76 // Take the first ID in the quota ID gap
77 lastID++
78 break
79 }
80 lastID++
81 }
82 }
83
84 // If both limits are zero, this is a delete operation, process it as such
85 if maxBytes == 0 && maxInodes == 0 {
86 valid = quotactl.FlagBLimitsValid | quotactl.FlagILimitsValid
87 attrs.ProjectID = 0
88 attrs.Flags &= ^fsxattrs.FlagProjectInherit
89 } else {
90 attrs.ProjectID = lastID
91 attrs.Flags |= fsxattrs.FlagProjectInherit
92 }
93
94 if err := fsxattrs.Set(dir, attrs); err != nil {
95 return err
96 }
97
98 // Always round up to the nearest block size
99 bytesLimitBlocks := uint64(math.Ceil(float64(maxBytes) / float64(1024)))
100
101 return quotactl.SetQuota(source, quotactl.QuotaTypeProject, lastID, &quotactl.Quota{
102 BHardLimit: bytesLimitBlocks,
103 BSoftLimit: bytesLimitBlocks,
104 IHardLimit: maxInodes,
105 ISoftLimit: maxInodes,
106 Valid: valid,
107 })
108}
109
110type Quota struct {
111 Bytes uint64
112 BytesUsed uint64
113 Inodes uint64
114 InodesUsed uint64
115}
116
117// GetQuota returns the current active quota and its utilization at the given path
118func GetQuota(path string) (*Quota, error) {
119 dir, err := os.Open(path)
120 if err != nil {
121 return nil, err
122 }
123 defer dir.Close()
124 source, err := fsinfoGetSource(dir)
125 if err != nil {
126 return nil, err
127 }
128 attrs, err := fsxattrs.Get(dir)
129 if err != nil {
130 return nil, err
131 }
132 if attrs.ProjectID == 0 {
133 return nil, os.ErrNotExist
134 }
135 quota, err := quotactl.GetQuota(source, quotactl.QuotaTypeProject, attrs.ProjectID)
136 if err != nil {
137 return nil, err
138 }
139 return &Quota{
140 Bytes: quota.BHardLimit,
141 BytesUsed: quota.CurSpace,
142 Inodes: quota.IHardLimit,
143 InodesUsed: quota.CurInodes,
144 }, nil
145}