1
2
3
4
19
20 package common
21
22 import (
23 "bufio"
24 "fmt"
25 "os"
26 "os/exec"
27 "regexp"
28 "strconv"
29 "strings"
30 "sync"
31 "syscall"
32
33 "k8s.io/klog/v2"
34 )
35
36 var quotaCmd string
37 var quotaCmdInitialized bool
38 var quotaCmdLock sync.RWMutex
39
40
41
42
43 type linuxFilesystemType struct {
44 name string
45 typeMagic int64
46 maxQuota int64
47 allowEmptyOutput bool
48 }
49
50 const (
51 bitsPerWord = 32 << (^uint(0) >> 63)
52 )
53
54 var (
55 linuxSupportedFilesystems = []linuxFilesystemType{
56 {
57 name: "XFS",
58 typeMagic: 0x58465342,
59 maxQuota: 1<<(bitsPerWord-1) - 1,
60 allowEmptyOutput: true,
61 }, {
62 name: "ext4fs",
63 typeMagic: 0xef53,
64 maxQuota: (1<<(bitsPerWord-1) - 1) & (1<<58 - 1),
65 allowEmptyOutput: false,
66 },
67 }
68 )
69
70
71 type VolumeProvider struct {
72 }
73
74 var quotaCmds = []string{"/sbin/xfs_quota",
75 "/usr/sbin/xfs_quota",
76 "/bin/xfs_quota"}
77
78 var quotaParseRegexp = regexp.MustCompilePOSIX("^[^ \t]*[ \t]*([0-9]+)")
79
80 var lsattrCmd = "/usr/bin/lsattr"
81 var lsattrParseRegexp = regexp.MustCompilePOSIX("^ *([0-9]+) [^ ]+ (.*)$")
82
83
84
85 func (*VolumeProvider) GetQuotaApplier(mountpoint string, backingDev string) LinuxVolumeQuotaApplier {
86 for _, fsType := range linuxSupportedFilesystems {
87 if isFilesystemOfType(mountpoint, backingDev, fsType.typeMagic) {
88 return linuxVolumeQuotaApplier{mountpoint: mountpoint,
89 maxQuota: fsType.maxQuota,
90 allowEmptyOutput: fsType.allowEmptyOutput,
91 }
92 }
93 }
94 return nil
95 }
96
97 type linuxVolumeQuotaApplier struct {
98 mountpoint string
99 maxQuota int64
100 allowEmptyOutput bool
101 }
102
103 func getXFSQuotaCmd() (string, error) {
104 quotaCmdLock.Lock()
105 defer quotaCmdLock.Unlock()
106 if quotaCmdInitialized {
107 return quotaCmd, nil
108 }
109 for _, program := range quotaCmds {
110 fileinfo, err := os.Stat(program)
111 if err == nil && ((fileinfo.Mode().Perm() & (1 << 6)) != 0) {
112 klog.V(3).Infof("Found xfs_quota program %s", program)
113 quotaCmd = program
114 quotaCmdInitialized = true
115 return quotaCmd, nil
116 }
117 }
118 quotaCmdInitialized = true
119 return "", fmt.Errorf("no xfs_quota program found")
120 }
121
122 func doRunXFSQuotaCommand(mountpoint string, mountsFile, command string) (string, error) {
123 quotaCmd, err := getXFSQuotaCmd()
124 if err != nil {
125 return "", err
126 }
127
128
129 klog.V(4).Infof("runXFSQuotaCommand %s -t %s -P/dev/null -D/dev/null -x -f %s -c %s", quotaCmd, mountsFile, mountpoint, command)
130 cmd := exec.Command(quotaCmd, "-t", mountsFile, "-P/dev/null", "-D/dev/null", "-x", "-f", mountpoint, "-c", command)
131
132 data, err := cmd.Output()
133 if err != nil {
134 return "", err
135 }
136 klog.V(4).Infof("runXFSQuotaCommand output %q", string(data))
137 return string(data), nil
138 }
139
140
141
142
143
144
145 func runXFSQuotaCommand(mountpoint string, command string) (string, error) {
146 tmpMounts, err := os.CreateTemp("", "mounts")
147 if err != nil {
148 return "", fmt.Errorf("cannot create temporary mount file: %v", err)
149 }
150 tmpMountsFileName := tmpMounts.Name()
151 defer tmpMounts.Close()
152 defer os.Remove(tmpMountsFileName)
153
154 mounts, err := os.Open(MountsFile)
155 if err != nil {
156 return "", fmt.Errorf("cannot open mounts file %s: %v", MountsFile, err)
157 }
158 defer mounts.Close()
159
160 scanner := bufio.NewScanner(mounts)
161 for scanner.Scan() {
162 match := MountParseRegexp.FindStringSubmatch(scanner.Text())
163 if match != nil {
164 mount := match[2]
165 if mount == mountpoint {
166 if _, err := tmpMounts.WriteString(fmt.Sprintf("%s\n", scanner.Text())); err != nil {
167 return "", fmt.Errorf("cannot write temporary mounts file: %v", err)
168 }
169 if err := tmpMounts.Sync(); err != nil {
170 return "", fmt.Errorf("cannot sync temporary mounts file: %v", err)
171 }
172 return doRunXFSQuotaCommand(mountpoint, tmpMountsFileName, command)
173 }
174 }
175 }
176 return "", fmt.Errorf("cannot run xfs_quota: cannot find mount point %s in %s", mountpoint, MountsFile)
177 }
178
179
180 func SupportsQuotas(mountpoint string, qType QuotaType) (bool, error) {
181 data, err := runXFSQuotaCommand(mountpoint, "state -p")
182 if err != nil {
183 return false, err
184 }
185 if qType == FSQuotaEnforcing {
186 return strings.Contains(data, "Enforcement: ON"), nil
187 }
188 return strings.Contains(data, "Accounting: ON"), nil
189 }
190
191 func isFilesystemOfType(mountpoint string, backingDev string, typeMagic int64) bool {
192 var buf syscall.Statfs_t
193 err := syscall.Statfs(mountpoint, &buf)
194 if err != nil {
195 klog.Warningf("Warning: Unable to statfs %s: %v", mountpoint, err)
196 return false
197 }
198 if int64(buf.Type) != typeMagic {
199 return false
200 }
201 if answer, _ := SupportsQuotas(mountpoint, FSQuotaAccounting); answer {
202 return true
203 }
204 return false
205 }
206
207
208
209
210 func (v linuxVolumeQuotaApplier) GetQuotaOnDir(path string) (QuotaID, error) {
211 cmd := exec.Command(lsattrCmd, "-pd", path)
212 data, err := cmd.Output()
213 if err != nil {
214 return BadQuotaID, fmt.Errorf("cannot run lsattr: %v", err)
215 }
216 match := lsattrParseRegexp.FindStringSubmatch(string(data))
217 if match == nil {
218 return BadQuotaID, fmt.Errorf("unable to parse lsattr -pd %s output %s", path, string(data))
219 }
220 if match[2] != path {
221 return BadQuotaID, fmt.Errorf("mismatch between supplied and returned path (%s != %s)", path, match[2])
222 }
223 projid, err := strconv.ParseInt(match[1], 10, 32)
224 if err != nil {
225 return BadQuotaID, fmt.Errorf("unable to parse project ID from %s (%v)", match[1], err)
226 }
227 return QuotaID(projid), nil
228 }
229
230
231 func (v linuxVolumeQuotaApplier) SetQuotaOnDir(path string, id QuotaID, bytes int64) error {
232 if bytes < 0 || bytes > v.maxQuota {
233 bytes = v.maxQuota
234 }
235 _, err := runXFSQuotaCommand(v.mountpoint, fmt.Sprintf("limit -p bhard=%v bsoft=%v %v", bytes, bytes, id))
236 if err != nil {
237 return err
238 }
239
240 _, err = runXFSQuotaCommand(v.mountpoint, fmt.Sprintf("project -s -p %s %v", path, id))
241 return err
242 }
243
244 func getQuantity(mountpoint string, id QuotaID, xfsQuotaArg string, multiplier int64, allowEmptyOutput bool) (int64, error) {
245 data, err := runXFSQuotaCommand(mountpoint, fmt.Sprintf("quota -p -N -n -v %s %v", xfsQuotaArg, id))
246 if err != nil {
247 return 0, fmt.Errorf("unable to run xfs_quota: %v", err)
248 }
249 if data == "" && allowEmptyOutput {
250 return 0, nil
251 }
252 match := quotaParseRegexp.FindStringSubmatch(data)
253 if match == nil {
254 return 0, fmt.Errorf("unable to parse quota output '%s'", data)
255 }
256 size, err := strconv.ParseInt(match[1], 10, 64)
257 if err != nil {
258 return 0, fmt.Errorf("unable to parse data size '%s' from '%s': %v", match[1], data, err)
259 }
260 klog.V(4).Infof("getQuantity %s %d %s %d => %d %v", mountpoint, id, xfsQuotaArg, multiplier, size, err)
261 return size * multiplier, nil
262 }
263
264
265 func (v linuxVolumeQuotaApplier) GetConsumption(_ string, id QuotaID) (int64, error) {
266 return getQuantity(v.mountpoint, id, "-b", 1024, v.allowEmptyOutput)
267 }
268
269
270 func (v linuxVolumeQuotaApplier) GetInodes(_ string, id QuotaID) (int64, error) {
271 return getQuantity(v.mountpoint, id, "-i", 1, v.allowEmptyOutput)
272 }
273
274
275
276 func (v linuxVolumeQuotaApplier) QuotaIDIsInUse(id QuotaID) (bool, error) {
277 bytes, err := v.GetConsumption(v.mountpoint, id)
278 if err != nil {
279 return false, err
280 }
281 if bytes > 0 {
282 return true, nil
283 }
284 inodes, err := v.GetInodes(v.mountpoint, id)
285 return inodes > 0, err
286 }
287
View as plain text