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