...

Source file src/k8s.io/kubernetes/pkg/volume/util/fsquota/common/quota_common_linux_impl.go

Documentation: k8s.io/kubernetes/pkg/volume/util/fsquota/common

     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  }
   287  

View as plain text