github.com/kunnos/engine@v1.13.1/daemon/graphdriver/quota/projectquota.go (about)

     1  // +build linux
     2  
     3  //
     4  // projectquota.go - implements XFS project quota controls
     5  // for setting quota limits on a newly created directory.
     6  // It currently supports the legacy XFS specific ioctls.
     7  //
     8  // TODO: use generic quota control ioctl FS_IOC_FS{GET,SET}XATTR
     9  //       for both xfs/ext4 for kernel version >= v4.5
    10  //
    11  
    12  package quota
    13  
    14  /*
    15  #include <stdlib.h>
    16  #include <dirent.h>
    17  #include <linux/fs.h>
    18  #include <linux/quota.h>
    19  #include <linux/dqblk_xfs.h>
    20  
    21  #ifndef FS_XFLAG_PROJINHERIT
    22  struct fsxattr {
    23  	__u32		fsx_xflags;
    24  	__u32		fsx_extsize;
    25  	__u32		fsx_nextents;
    26  	__u32		fsx_projid;
    27  	unsigned char	fsx_pad[12];
    28  };
    29  #define FS_XFLAG_PROJINHERIT	0x00000200
    30  #endif
    31  #ifndef FS_IOC_FSGETXATTR
    32  #define FS_IOC_FSGETXATTR		_IOR ('X', 31, struct fsxattr)
    33  #endif
    34  #ifndef FS_IOC_FSSETXATTR
    35  #define FS_IOC_FSSETXATTR		_IOW ('X', 32, struct fsxattr)
    36  #endif
    37  
    38  #ifndef PRJQUOTA
    39  #define PRJQUOTA	2
    40  #endif
    41  #ifndef XFS_PROJ_QUOTA
    42  #define XFS_PROJ_QUOTA	2
    43  #endif
    44  #ifndef Q_XSETPQLIM
    45  #define Q_XSETPQLIM QCMD(Q_XSETQLIM, PRJQUOTA)
    46  #endif
    47  #ifndef Q_XGETPQUOTA
    48  #define Q_XGETPQUOTA QCMD(Q_XGETQUOTA, PRJQUOTA)
    49  #endif
    50  */
    51  import "C"
    52  import (
    53  	"fmt"
    54  	"io/ioutil"
    55  	"os"
    56  	"path"
    57  	"path/filepath"
    58  	"syscall"
    59  	"unsafe"
    60  
    61  	"github.com/sirupsen/logrus"
    62  )
    63  
    64  // Quota limit params - currently we only control blocks hard limit
    65  type Quota struct {
    66  	Size uint64
    67  }
    68  
    69  // Control - Context to be used by storage driver (e.g. overlay)
    70  // who wants to apply project quotas to container dirs
    71  type Control struct {
    72  	backingFsBlockDev string
    73  	nextProjectID     uint32
    74  	quotas            map[string]uint32
    75  }
    76  
    77  // NewControl - initialize project quota support.
    78  // Test to make sure that quota can be set on a test dir and find
    79  // the first project id to be used for the next container create.
    80  //
    81  // Returns nil (and error) if project quota is not supported.
    82  //
    83  // First get the project id of the home directory.
    84  // This test will fail if the backing fs is not xfs.
    85  //
    86  // xfs_quota tool can be used to assign a project id to the driver home directory, e.g.:
    87  //    echo 999:/var/lib/docker/overlay2 >> /etc/projects
    88  //    echo docker:999 >> /etc/projid
    89  //    xfs_quota -x -c 'project -s docker' /<xfs mount point>
    90  //
    91  // In that case, the home directory project id will be used as a "start offset"
    92  // and all containers will be assigned larger project ids (e.g. >= 1000).
    93  // This is a way to prevent xfs_quota management from conflicting with docker.
    94  //
    95  // Then try to create a test directory with the next project id and set a quota
    96  // on it. If that works, continue to scan existing containers to map allocated
    97  // project ids.
    98  //
    99  func NewControl(basePath string) (*Control, error) {
   100  	//
   101  	// Get project id of parent dir as minimal id to be used by driver
   102  	//
   103  	minProjectID, err := getProjectID(basePath)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  	minProjectID++
   108  
   109  	//
   110  	// create backing filesystem device node
   111  	//
   112  	backingFsBlockDev, err := makeBackingFsDev(basePath)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	//
   118  	// Test if filesystem supports project quotas by trying to set
   119  	// a quota on the first available project id
   120  	//
   121  	quota := Quota{
   122  		Size: 0,
   123  	}
   124  	if err := setProjectQuota(backingFsBlockDev, minProjectID, quota); err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	q := Control{
   129  		backingFsBlockDev: backingFsBlockDev,
   130  		nextProjectID:     minProjectID + 1,
   131  		quotas:            make(map[string]uint32),
   132  	}
   133  
   134  	//
   135  	// get first project id to be used for next container
   136  	//
   137  	err = q.findNextProjectID(basePath)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	logrus.Debugf("NewControl(%s): nextProjectID = %d", basePath, q.nextProjectID)
   143  	return &q, nil
   144  }
   145  
   146  // SetQuota - assign a unique project id to directory and set the quota limits
   147  // for that project id
   148  func (q *Control) SetQuota(targetPath string, quota Quota) error {
   149  
   150  	projectID, ok := q.quotas[targetPath]
   151  	if !ok {
   152  		projectID = q.nextProjectID
   153  
   154  		//
   155  		// assign project id to new container directory
   156  		//
   157  		err := setProjectID(targetPath, projectID)
   158  		if err != nil {
   159  			return err
   160  		}
   161  
   162  		q.quotas[targetPath] = projectID
   163  		q.nextProjectID++
   164  	}
   165  
   166  	//
   167  	// set the quota limit for the container's project id
   168  	//
   169  	logrus.Debugf("SetQuota(%s, %d): projectID=%d", targetPath, quota.Size, projectID)
   170  	return setProjectQuota(q.backingFsBlockDev, projectID, quota)
   171  }
   172  
   173  // setProjectQuota - set the quota for project id on xfs block device
   174  func setProjectQuota(backingFsBlockDev string, projectID uint32, quota Quota) error {
   175  	var d C.fs_disk_quota_t
   176  	d.d_version = C.FS_DQUOT_VERSION
   177  	d.d_id = C.__u32(projectID)
   178  	d.d_flags = C.XFS_PROJ_QUOTA
   179  
   180  	d.d_fieldmask = C.FS_DQ_BHARD | C.FS_DQ_BSOFT
   181  	d.d_blk_hardlimit = C.__u64(quota.Size / 512)
   182  	d.d_blk_softlimit = d.d_blk_hardlimit
   183  
   184  	var cs = C.CString(backingFsBlockDev)
   185  	defer C.free(unsafe.Pointer(cs))
   186  
   187  	_, _, errno := syscall.Syscall6(syscall.SYS_QUOTACTL, C.Q_XSETPQLIM,
   188  		uintptr(unsafe.Pointer(cs)), uintptr(d.d_id),
   189  		uintptr(unsafe.Pointer(&d)), 0, 0)
   190  	if errno != 0 {
   191  		return fmt.Errorf("Failed to set quota limit for projid %d on %s: %v",
   192  			projectID, backingFsBlockDev, errno.Error())
   193  	}
   194  
   195  	return nil
   196  }
   197  
   198  // GetQuota - get the quota limits of a directory that was configured with SetQuota
   199  func (q *Control) GetQuota(targetPath string, quota *Quota) error {
   200  
   201  	projectID, ok := q.quotas[targetPath]
   202  	if !ok {
   203  		return fmt.Errorf("quota not found for path : %s", targetPath)
   204  	}
   205  
   206  	//
   207  	// get the quota limit for the container's project id
   208  	//
   209  	var d C.fs_disk_quota_t
   210  
   211  	var cs = C.CString(q.backingFsBlockDev)
   212  	defer C.free(unsafe.Pointer(cs))
   213  
   214  	_, _, errno := syscall.Syscall6(syscall.SYS_QUOTACTL, C.Q_XGETPQUOTA,
   215  		uintptr(unsafe.Pointer(cs)), uintptr(C.__u32(projectID)),
   216  		uintptr(unsafe.Pointer(&d)), 0, 0)
   217  	if errno != 0 {
   218  		return fmt.Errorf("Failed to get quota limit for projid %d on %s: %v",
   219  			projectID, q.backingFsBlockDev, errno.Error())
   220  	}
   221  	quota.Size = uint64(d.d_blk_hardlimit) * 512
   222  
   223  	return nil
   224  }
   225  
   226  // getProjectID - get the project id of path on xfs
   227  func getProjectID(targetPath string) (uint32, error) {
   228  	dir, err := openDir(targetPath)
   229  	if err != nil {
   230  		return 0, err
   231  	}
   232  	defer closeDir(dir)
   233  
   234  	var fsx C.struct_fsxattr
   235  	_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR,
   236  		uintptr(unsafe.Pointer(&fsx)))
   237  	if errno != 0 {
   238  		return 0, fmt.Errorf("Failed to get projid for %s: %v", targetPath, errno.Error())
   239  	}
   240  
   241  	return uint32(fsx.fsx_projid), nil
   242  }
   243  
   244  // setProjectID - set the project id of path on xfs
   245  func setProjectID(targetPath string, projectID uint32) error {
   246  	dir, err := openDir(targetPath)
   247  	if err != nil {
   248  		return err
   249  	}
   250  	defer closeDir(dir)
   251  
   252  	var fsx C.struct_fsxattr
   253  	_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR,
   254  		uintptr(unsafe.Pointer(&fsx)))
   255  	if errno != 0 {
   256  		return fmt.Errorf("Failed to get projid for %s: %v", targetPath, errno.Error())
   257  	}
   258  	fsx.fsx_projid = C.__u32(projectID)
   259  	fsx.fsx_xflags |= C.FS_XFLAG_PROJINHERIT
   260  	_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSSETXATTR,
   261  		uintptr(unsafe.Pointer(&fsx)))
   262  	if errno != 0 {
   263  		return fmt.Errorf("Failed to set projid for %s: %v", targetPath, errno.Error())
   264  	}
   265  
   266  	return nil
   267  }
   268  
   269  // findNextProjectID - find the next project id to be used for containers
   270  // by scanning driver home directory to find used project ids
   271  func (q *Control) findNextProjectID(home string) error {
   272  	files, err := ioutil.ReadDir(home)
   273  	if err != nil {
   274  		return fmt.Errorf("read directory failed : %s", home)
   275  	}
   276  	for _, file := range files {
   277  		if !file.IsDir() {
   278  			continue
   279  		}
   280  		path := filepath.Join(home, file.Name())
   281  		projid, err := getProjectID(path)
   282  		if err != nil {
   283  			return err
   284  		}
   285  		if projid > 0 {
   286  			q.quotas[path] = projid
   287  		}
   288  		if q.nextProjectID <= projid {
   289  			q.nextProjectID = projid + 1
   290  		}
   291  	}
   292  
   293  	return nil
   294  }
   295  
   296  func free(p *C.char) {
   297  	C.free(unsafe.Pointer(p))
   298  }
   299  
   300  func openDir(path string) (*C.DIR, error) {
   301  	Cpath := C.CString(path)
   302  	defer free(Cpath)
   303  
   304  	dir := C.opendir(Cpath)
   305  	if dir == nil {
   306  		return nil, fmt.Errorf("Can't open dir")
   307  	}
   308  	return dir, nil
   309  }
   310  
   311  func closeDir(dir *C.DIR) {
   312  	if dir != nil {
   313  		C.closedir(dir)
   314  	}
   315  }
   316  
   317  func getDirFd(dir *C.DIR) uintptr {
   318  	return uintptr(C.dirfd(dir))
   319  }
   320  
   321  // Get the backing block device of the driver home directory
   322  // and create a block device node under the home directory
   323  // to be used by quotactl commands
   324  func makeBackingFsDev(home string) (string, error) {
   325  	fileinfo, err := os.Stat(home)
   326  	if err != nil {
   327  		return "", err
   328  	}
   329  
   330  	backingFsBlockDev := path.Join(home, "backingFsBlockDev")
   331  	// Re-create just in case comeone copied the home directory over to a new device
   332  	syscall.Unlink(backingFsBlockDev)
   333  	stat := fileinfo.Sys().(*syscall.Stat_t)
   334  	if err := syscall.Mknod(backingFsBlockDev, syscall.S_IFBLK|0600, int(stat.Dev)); err != nil {
   335  		return "", fmt.Errorf("Failed to mknod %s: %v", backingFsBlockDev, err)
   336  	}
   337  
   338  	return backingFsBlockDev, nil
   339  }