k8s.io/kubernetes@v1.29.3/pkg/volume/util/fsquota/common/quota_common_linux_impl.go (about) 1 //go:build linux 2 // +build linux 3 4 /* 5 Copyright 2018 The Kubernetes Authors. 6 7 Licensed under the Apache License, Version 2.0 (the "License"); 8 you may not use this file except in compliance with the License. 9 You may obtain a copy of the License at 10 11 http://www.apache.org/licenses/LICENSE-2.0 12 13 Unless required by applicable law or agreed to in writing, software 14 distributed under the License is distributed on an "AS IS" BASIS, 15 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 See the License for the specific language governing permissions and 17 limitations under the License. 18 */ 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 // If we later get a filesystem that uses project quota semantics other than 41 // XFS, we'll need to change this. 42 // Higher levels don't need to know what's inside 43 type linuxFilesystemType struct { 44 name string 45 typeMagic int64 // Filesystem magic number, per statfs(2) 46 maxQuota int64 47 allowEmptyOutput bool // Accept empty output from "quota" command 48 } 49 50 const ( 51 bitsPerWord = 32 << (^uint(0) >> 63) // either 32 or 64 52 ) 53 54 var ( 55 linuxSupportedFilesystems = []linuxFilesystemType{ 56 { 57 name: "XFS", 58 typeMagic: 0x58465342, 59 maxQuota: 1<<(bitsPerWord-1) - 1, 60 allowEmptyOutput: true, // XFS filesystems report nothing if a quota is not present 61 }, { 62 name: "ext4fs", 63 typeMagic: 0xef53, 64 maxQuota: (1<<(bitsPerWord-1) - 1) & (1<<58 - 1), 65 allowEmptyOutput: false, // ext4 filesystems always report something even if a quota is not present 66 }, 67 } 68 ) 69 70 // VolumeProvider supplies a quota applier to the generic code. 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 // GetQuotaApplier -- does this backing device support quotas that 84 // can be applied to directories? 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 // We're using numeric project IDs directly; no need to scan 128 // /etc/projects or /etc/projid 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 // Extract the mountpoint we care about into a temporary mounts file so that xfs_quota does 141 // not attempt to scan every mount on the filesystem, which could hang if e. g. 142 // a stuck NFS mount is present. 143 // See https://bugzilla.redhat.com/show_bug.cgi?id=237120 for an example 144 // of the problem that could be caused if this were to happen. 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 // SupportsQuotas determines whether the filesystem supports quotas. 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 // GetQuotaOnDir retrieves the quota ID (if any) associated with the specified directory 208 // If we can't make system calls, all we can say is that we don't know whether 209 // it has a quota, and higher levels have to make the call. 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 // SetQuotaOnDir applies a quota to the specified directory under the specified mountpoint. 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 // GetConsumption returns the consumption in bytes if available via quotas 265 func (v linuxVolumeQuotaApplier) GetConsumption(_ string, id QuotaID) (int64, error) { 266 return getQuantity(v.mountpoint, id, "-b", 1024, v.allowEmptyOutput) 267 } 268 269 // GetInodes returns the inodes in use if available via quotas 270 func (v linuxVolumeQuotaApplier) GetInodes(_ string, id QuotaID) (int64, error) { 271 return getQuantity(v.mountpoint, id, "-i", 1, v.allowEmptyOutput) 272 } 273 274 // QuotaIDIsInUse checks whether the specified quota ID is in use on the specified 275 // filesystem 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 }