...

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

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

     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 fsquota
    21  
    22  import (
    23  	"bufio"
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  	"regexp"
    28  	"strconv"
    29  	"sync"
    30  
    31  	"golang.org/x/sys/unix"
    32  	"k8s.io/kubernetes/pkg/volume/util/fsquota/common"
    33  )
    34  
    35  var projectsFile = "/etc/projects"
    36  var projidFile = "/etc/projid"
    37  
    38  var projectsParseRegexp = regexp.MustCompilePOSIX("^([[:digit:]]+):(.*)$")
    39  var projidParseRegexp = regexp.MustCompilePOSIX("^([^#][^:]*):([[:digit:]]+)$")
    40  
    41  var quotaIDLock sync.RWMutex
    42  
    43  const maxUnusedQuotasToSearch = 128 // Don't go into an infinite loop searching for an unused quota
    44  
    45  type projectType struct {
    46  	isValid bool // False if we need to remove this line
    47  	id      common.QuotaID
    48  	data    string // Project name (projid) or directory (projects)
    49  	line    string
    50  }
    51  
    52  type projectsList struct {
    53  	projects []projectType
    54  	projid   []projectType
    55  }
    56  
    57  func projFilesAreOK() error {
    58  	if sf, err := os.Lstat(projectsFile); err != nil || sf.Mode().IsRegular() {
    59  		if sf, err := os.Lstat(projidFile); err != nil || sf.Mode().IsRegular() {
    60  			return nil
    61  		}
    62  		return fmt.Errorf("%s exists but is not a plain file, cannot continue", projidFile)
    63  	}
    64  	return fmt.Errorf("%s exists but is not a plain file, cannot continue", projectsFile)
    65  }
    66  
    67  func lockFile(file *os.File) error {
    68  	return unix.Flock(int(file.Fd()), unix.LOCK_EX)
    69  }
    70  
    71  func unlockFile(file *os.File) error {
    72  	return unix.Flock(int(file.Fd()), unix.LOCK_UN)
    73  }
    74  
    75  // openAndLockProjectFiles opens /etc/projects and /etc/projid locked.
    76  // Creates them if they don't exist
    77  func openAndLockProjectFiles() (*os.File, *os.File, error) {
    78  	// Make sure neither project-related file is a symlink!
    79  	if err := projFilesAreOK(); err != nil {
    80  		return nil, nil, fmt.Errorf("system project files failed verification: %v", err)
    81  	}
    82  	// We don't actually modify the original files; we create temporaries and
    83  	// move them over the originals
    84  	fProjects, err := os.OpenFile(projectsFile, os.O_RDONLY|os.O_CREATE, 0644)
    85  	if err != nil {
    86  		err = fmt.Errorf("unable to open %s: %v", projectsFile, err)
    87  		return nil, nil, err
    88  	}
    89  	fProjid, err := os.OpenFile(projidFile, os.O_RDONLY|os.O_CREATE, 0644)
    90  	if err == nil {
    91  		// Check once more, to ensure nothing got changed out from under us
    92  		if err = projFilesAreOK(); err == nil {
    93  			err = lockFile(fProjects)
    94  			if err == nil {
    95  				err = lockFile(fProjid)
    96  				if err == nil {
    97  					return fProjects, fProjid, nil
    98  				}
    99  				// Nothing useful we can do if we get an error here
   100  				err = fmt.Errorf("unable to lock %s: %v", projidFile, err)
   101  				unlockFile(fProjects)
   102  			} else {
   103  				err = fmt.Errorf("unable to lock %s: %v", projectsFile, err)
   104  			}
   105  		} else {
   106  			err = fmt.Errorf("system project files failed re-verification: %v", err)
   107  		}
   108  		fProjid.Close()
   109  	} else {
   110  		err = fmt.Errorf("unable to open %s: %v", projidFile, err)
   111  	}
   112  	fProjects.Close()
   113  	return nil, nil, err
   114  }
   115  
   116  func closeProjectFiles(fProjects *os.File, fProjid *os.File) error {
   117  	// Nothing useful we can do if either of these fail,
   118  	// but we have to close (and thereby unlock) the files anyway.
   119  	var err error
   120  	var err1 error
   121  	if fProjid != nil {
   122  		err = fProjid.Close()
   123  	}
   124  	if fProjects != nil {
   125  		err1 = fProjects.Close()
   126  	}
   127  	if err == nil {
   128  		return err1
   129  	}
   130  	return err
   131  }
   132  
   133  func parseProject(l string) projectType {
   134  	if match := projectsParseRegexp.FindStringSubmatch(l); match != nil {
   135  		i, err := strconv.Atoi(match[1])
   136  		if err == nil {
   137  			return projectType{true, common.QuotaID(i), match[2], l}
   138  		}
   139  	}
   140  	return projectType{true, common.BadQuotaID, "", l}
   141  }
   142  
   143  func parseProjid(l string) projectType {
   144  	if match := projidParseRegexp.FindStringSubmatch(l); match != nil {
   145  		i, err := strconv.Atoi(match[2])
   146  		if err == nil {
   147  			return projectType{true, common.QuotaID(i), match[1], l}
   148  		}
   149  	}
   150  	return projectType{true, common.BadQuotaID, "", l}
   151  }
   152  
   153  func parseProjFile(f *os.File, parser func(l string) projectType) []projectType {
   154  	var answer []projectType
   155  	scanner := bufio.NewScanner(f)
   156  	for scanner.Scan() {
   157  		answer = append(answer, parser(scanner.Text()))
   158  	}
   159  	return answer
   160  }
   161  
   162  func readProjectFiles(projects *os.File, projid *os.File) projectsList {
   163  	return projectsList{parseProjFile(projects, parseProject), parseProjFile(projid, parseProjid)}
   164  }
   165  
   166  // findAvailableQuota finds the next available quota from the FirstQuota
   167  // it returns error if QuotaIDIsInUse returns error when getting quota id in use;
   168  // it searches at most maxUnusedQuotasToSearch(128) time
   169  func findAvailableQuota(path string, idMap map[common.QuotaID]bool) (common.QuotaID, error) {
   170  	unusedQuotasSearched := 0
   171  	for id := common.FirstQuota; true; id++ {
   172  		if _, ok := idMap[id]; !ok {
   173  			isInUse, err := getApplier(path).QuotaIDIsInUse(id)
   174  			if err != nil {
   175  				return common.BadQuotaID, err
   176  			} else if !isInUse {
   177  				return id, nil
   178  			}
   179  			unusedQuotasSearched++
   180  			if unusedQuotasSearched > maxUnusedQuotasToSearch {
   181  				break
   182  			}
   183  		}
   184  	}
   185  	return common.BadQuotaID, fmt.Errorf("cannot find available quota ID")
   186  }
   187  
   188  func addDirToProject(path string, id common.QuotaID, list *projectsList) (common.QuotaID, bool, error) {
   189  	idMap := make(map[common.QuotaID]bool)
   190  	for _, project := range list.projects {
   191  		if project.data == path {
   192  			if id != common.BadQuotaID && id != project.id {
   193  				return common.BadQuotaID, false, fmt.Errorf("attempt to reassign project ID for %s", path)
   194  			}
   195  			// Trying to reassign a directory to the project it's
   196  			// already in.  Maybe this should be an error, but for
   197  			// now treat it as an idempotent operation
   198  			return project.id, false, nil
   199  		}
   200  		idMap[project.id] = true
   201  	}
   202  	var needToAddProjid = true
   203  	for _, projid := range list.projid {
   204  		idMap[projid.id] = true
   205  		if projid.id == id && id != common.BadQuotaID {
   206  			needToAddProjid = false
   207  		}
   208  	}
   209  	var err error
   210  	if id == common.BadQuotaID {
   211  		id, err = findAvailableQuota(path, idMap)
   212  		if err != nil {
   213  			return common.BadQuotaID, false, err
   214  		}
   215  		needToAddProjid = true
   216  	}
   217  	if needToAddProjid {
   218  		name := fmt.Sprintf("volume%v", id)
   219  		line := fmt.Sprintf("%s:%v", name, id)
   220  		list.projid = append(list.projid, projectType{true, id, name, line})
   221  	}
   222  	line := fmt.Sprintf("%v:%s", id, path)
   223  	list.projects = append(list.projects, projectType{true, id, path, line})
   224  	return id, needToAddProjid, nil
   225  }
   226  
   227  func removeDirFromProject(path string, id common.QuotaID, list *projectsList) (bool, error) {
   228  	if id == common.BadQuotaID {
   229  		return false, fmt.Errorf("attempt to remove invalid quota ID from %s", path)
   230  	}
   231  	foundAt := -1
   232  	countByID := make(map[common.QuotaID]int)
   233  	for i, project := range list.projects {
   234  		if project.data == path {
   235  			if id != project.id {
   236  				return false, fmt.Errorf("attempting to remove quota ID %v from path %s, but expecting ID %v", id, path, project.id)
   237  			} else if foundAt != -1 {
   238  				return false, fmt.Errorf("found multiple quota IDs for path %s", path)
   239  			}
   240  			// Faster and easier than deleting an element
   241  			list.projects[i].isValid = false
   242  			foundAt = i
   243  		}
   244  		countByID[project.id]++
   245  	}
   246  	if foundAt == -1 {
   247  		return false, fmt.Errorf("cannot find quota associated with path %s", path)
   248  	}
   249  	if countByID[id] <= 1 {
   250  		// Removing the last entry means that we're no longer using
   251  		// the quota ID, so remove that as well
   252  		for i, projid := range list.projid {
   253  			if projid.id == id {
   254  				list.projid[i].isValid = false
   255  			}
   256  		}
   257  		return true, nil
   258  	}
   259  	return false, nil
   260  }
   261  
   262  func writeProjectFile(base *os.File, projects []projectType) (string, error) {
   263  	oname := base.Name()
   264  	stat, err := base.Stat()
   265  	if err != nil {
   266  		return "", err
   267  	}
   268  	mode := stat.Mode() & os.ModePerm
   269  	f, err := os.CreateTemp(filepath.Dir(oname), filepath.Base(oname))
   270  	if err != nil {
   271  		return "", err
   272  	}
   273  	filename := f.Name()
   274  	if err := os.Chmod(filename, mode); err != nil {
   275  		return "", err
   276  	}
   277  	for _, proj := range projects {
   278  		if proj.isValid {
   279  			if _, err := f.WriteString(fmt.Sprintf("%s\n", proj.line)); err != nil {
   280  				f.Close()
   281  				os.Remove(filename)
   282  				return "", err
   283  			}
   284  		}
   285  	}
   286  	if err := f.Close(); err != nil {
   287  		os.Remove(filename)
   288  		return "", err
   289  	}
   290  	return filename, nil
   291  }
   292  
   293  func writeProjectFiles(fProjects *os.File, fProjid *os.File, writeProjid bool, list projectsList) error {
   294  	tmpProjects, err := writeProjectFile(fProjects, list.projects)
   295  	if err == nil {
   296  		// Ensure that both files are written before we try to rename either.
   297  		if writeProjid {
   298  			tmpProjid, err := writeProjectFile(fProjid, list.projid)
   299  			if err == nil {
   300  				err = os.Rename(tmpProjid, fProjid.Name())
   301  				if err != nil {
   302  					os.Remove(tmpProjid)
   303  				}
   304  			}
   305  		}
   306  		if err == nil {
   307  			err = os.Rename(tmpProjects, fProjects.Name())
   308  			if err == nil {
   309  				return nil
   310  			}
   311  			// We're in a bit of trouble here; at this
   312  			// point we've successfully renamed tmpProjid
   313  			// to the real thing, but renaming tmpProject
   314  			// to the real file failed.  There's not much we
   315  			// can do in this position.  Anything we could do
   316  			// to try to undo it would itself be likely to fail.
   317  		}
   318  		os.Remove(tmpProjects)
   319  	}
   320  	return fmt.Errorf("unable to write project files: %v", err)
   321  }
   322  
   323  // if ID is common.BadQuotaID, generate new project id if the dir is not in a project
   324  func createProjectID(path string, ID common.QuotaID) (common.QuotaID, error) {
   325  	quotaIDLock.Lock()
   326  	defer quotaIDLock.Unlock()
   327  	fProjects, fProjid, err := openAndLockProjectFiles()
   328  	if err == nil {
   329  		defer closeProjectFiles(fProjects, fProjid)
   330  		list := readProjectFiles(fProjects, fProjid)
   331  		var writeProjid bool
   332  		ID, writeProjid, err = addDirToProject(path, ID, &list)
   333  		if err == nil && ID != common.BadQuotaID {
   334  			if err = writeProjectFiles(fProjects, fProjid, writeProjid, list); err == nil {
   335  				return ID, nil
   336  			}
   337  		}
   338  	}
   339  	return common.BadQuotaID, fmt.Errorf("createProjectID %s %v failed %v", path, ID, err)
   340  }
   341  
   342  func removeProjectID(path string, ID common.QuotaID) error {
   343  	if ID == common.BadQuotaID {
   344  		return fmt.Errorf("attempting to remove invalid quota ID %v", ID)
   345  	}
   346  	quotaIDLock.Lock()
   347  	defer quotaIDLock.Unlock()
   348  	fProjects, fProjid, err := openAndLockProjectFiles()
   349  	if err == nil {
   350  		defer closeProjectFiles(fProjects, fProjid)
   351  		list := readProjectFiles(fProjects, fProjid)
   352  		var writeProjid bool
   353  		writeProjid, err = removeDirFromProject(path, ID, &list)
   354  		if err == nil {
   355  			if err = writeProjectFiles(fProjects, fProjid, writeProjid, list); err == nil {
   356  				return nil
   357  			}
   358  		}
   359  	}
   360  	return fmt.Errorf("removeProjectID %s %v failed %v", path, ID, err)
   361  }
   362  

View as plain text