blob: f702d236ceb147399d58657a411c1904cabcc183 [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
Lorenz Brun1d801752020-04-02 09:24:51 +020029 "golang.org/x/sys/unix"
Serge Bazanski77cb6c52020-12-19 00:09:22 +010030
31 "git.monogon.dev/source/nexantic.git/metropolis/node/common/fsquota/fsxattrs"
32 "git.monogon.dev/source/nexantic.git/metropolis/node/common/fsquota/quotactl"
Lorenz Brun1d801752020-04-02 09:24:51 +020033)
34
35// SetQuota sets the quota of bytes and/or inodes in a given path. To not set a limit, set the
36// corresponding argument to zero. Setting both arguments to zero removes the quota entirely.
37// This function can only be called on an empty directory. It can't be used to create a quota
38// below a directory which already has a quota since Linux doesn't offer hierarchical quotas.
39func SetQuota(path string, maxBytes uint64, maxInodes uint64) error {
40 dir, err := os.Open(path)
41 if err != nil {
42 return err
43 }
44 defer dir.Close()
45 source, err := fsinfoGetSource(dir)
46 if err != nil {
47 return err
48 }
49 var valid uint32
50 if maxBytes > 0 {
51 valid |= quotactl.FlagBLimitsValid
52 }
53 if maxInodes > 0 {
54 valid |= quotactl.FlagILimitsValid
55 }
56
57 attrs, err := fsxattrs.Get(dir)
58 if err != nil {
59 return err
60 }
61
62 var lastID uint32 = attrs.ProjectID
63 if lastID == 0 {
64 // No project/quota exists for this directory, assign a new project quota
65 // TODO(lorenz): This is racy, but the kernel does not support atomically assigning
66 // quotas. So this needs to be added to the kernels setquota interface. Due to the short
67 // time window and infrequent calls this should not be an immediate issue.
68 for {
69 quota, err := quotactl.GetNextQuota(source, quotactl.QuotaTypeProject, lastID)
70 if err == unix.ENOENT || err == unix.ESRCH {
71 // We have enumerated all quotas, nothing exists here
72 break
73 } else if err != nil {
74 return fmt.Errorf("failed to call GetNextQuota: %w", err)
75 }
76 if quota.ID > lastID+1 {
77 // Take the first ID in the quota ID gap
78 lastID++
79 break
80 }
81 lastID++
82 }
83 }
84
85 // If both limits are zero, this is a delete operation, process it as such
86 if maxBytes == 0 && maxInodes == 0 {
87 valid = quotactl.FlagBLimitsValid | quotactl.FlagILimitsValid
88 attrs.ProjectID = 0
89 attrs.Flags &= ^fsxattrs.FlagProjectInherit
90 } else {
91 attrs.ProjectID = lastID
92 attrs.Flags |= fsxattrs.FlagProjectInherit
93 }
94
95 if err := fsxattrs.Set(dir, attrs); err != nil {
96 return err
97 }
98
99 // Always round up to the nearest block size
100 bytesLimitBlocks := uint64(math.Ceil(float64(maxBytes) / float64(1024)))
101
102 return quotactl.SetQuota(source, quotactl.QuotaTypeProject, lastID, &quotactl.Quota{
103 BHardLimit: bytesLimitBlocks,
104 BSoftLimit: bytesLimitBlocks,
105 IHardLimit: maxInodes,
106 ISoftLimit: maxInodes,
107 Valid: valid,
108 })
109}
110
111type Quota struct {
112 Bytes uint64
113 BytesUsed uint64
114 Inodes uint64
115 InodesUsed uint64
116}
117
118// GetQuota returns the current active quota and its utilization at the given path
119func GetQuota(path string) (*Quota, error) {
120 dir, err := os.Open(path)
121 if err != nil {
122 return nil, err
123 }
124 defer dir.Close()
125 source, err := fsinfoGetSource(dir)
126 if err != nil {
127 return nil, err
128 }
129 attrs, err := fsxattrs.Get(dir)
130 if err != nil {
131 return nil, err
132 }
133 if attrs.ProjectID == 0 {
134 return nil, os.ErrNotExist
135 }
136 quota, err := quotactl.GetQuota(source, quotactl.QuotaTypeProject, attrs.ProjectID)
137 if err != nil {
138 return nil, err
139 }
140 return &Quota{
Lorenz Brun547b33f2020-04-23 15:27:06 +0200141 Bytes: quota.BHardLimit * 1024,
Lorenz Brun1d801752020-04-02 09:24:51 +0200142 BytesUsed: quota.CurSpace,
143 Inodes: quota.IHardLimit,
144 InodesUsed: quota.CurInodes,
145 }, nil
146}