github.com/kinvolk/docker@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 }