Signed-off-by: Timo Rothenpieler <timo@rothenpieler.org>
| ... | ... |
@@ -13,8 +13,8 @@ import ( |
| 13 | 13 |
"unsafe" |
| 14 | 14 |
|
| 15 | 15 |
"github.com/docker/docker/daemon/graphdriver" |
| 16 |
- "github.com/docker/docker/daemon/graphdriver/quota" |
|
| 17 | 16 |
"github.com/docker/docker/pkg/stringid" |
| 17 |
+ "github.com/docker/docker/quota" |
|
| 18 | 18 |
units "github.com/docker/go-units" |
| 19 | 19 |
"golang.org/x/sys/unix" |
| 20 | 20 |
"gotest.tools/v3/assert" |
| ... | ... |
@@ -18,7 +18,6 @@ import ( |
| 18 | 18 |
"github.com/containerd/containerd/sys" |
| 19 | 19 |
"github.com/docker/docker/daemon/graphdriver" |
| 20 | 20 |
"github.com/docker/docker/daemon/graphdriver/overlayutils" |
| 21 |
- "github.com/docker/docker/daemon/graphdriver/quota" |
|
| 22 | 21 |
"github.com/docker/docker/pkg/archive" |
| 23 | 22 |
"github.com/docker/docker/pkg/chrootarchive" |
| 24 | 23 |
"github.com/docker/docker/pkg/containerfs" |
| ... | ... |
@@ -27,6 +26,7 @@ import ( |
| 27 | 27 |
"github.com/docker/docker/pkg/idtools" |
| 28 | 28 |
"github.com/docker/docker/pkg/parsers" |
| 29 | 29 |
"github.com/docker/docker/pkg/system" |
| 30 |
+ "github.com/docker/docker/quota" |
|
| 30 | 31 |
units "github.com/docker/go-units" |
| 31 | 32 |
"github.com/moby/locker" |
| 32 | 33 |
"github.com/moby/sys/mount" |
| 33 | 34 |
deleted file mode 100644 |
| ... | ... |
@@ -1,19 +0,0 @@ |
| 1 |
-package quota // import "github.com/docker/docker/daemon/graphdriver/quota" |
|
| 2 |
- |
|
| 3 |
-import "github.com/docker/docker/errdefs" |
|
| 4 |
- |
|
| 5 |
-var ( |
|
| 6 |
- _ errdefs.ErrNotImplemented = (*errQuotaNotSupported)(nil) |
|
| 7 |
-) |
|
| 8 |
- |
|
| 9 |
-// ErrQuotaNotSupported indicates if were found the FS didn't have projects quotas available |
|
| 10 |
-var ErrQuotaNotSupported = errQuotaNotSupported{}
|
|
| 11 |
- |
|
| 12 |
-type errQuotaNotSupported struct {
|
|
| 13 |
-} |
|
| 14 |
- |
|
| 15 |
-func (e errQuotaNotSupported) NotImplemented() {}
|
|
| 16 |
- |
|
| 17 |
-func (e errQuotaNotSupported) Error() string {
|
|
| 18 |
- return "Filesystem does not support, or has not enabled quotas" |
|
| 19 |
-} |
| 20 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,442 +0,0 @@ |
| 1 |
-// +build linux,!exclude_disk_quota,cgo |
|
| 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 // import "github.com/docker/docker/daemon/graphdriver/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 |
-const int Q_XGETQSTAT_PRJQUOTA = QCMD(Q_XGETQSTAT, PRJQUOTA); |
|
| 52 |
-*/ |
|
| 53 |
-import "C" |
|
| 54 |
-import ( |
|
| 55 |
- "io/ioutil" |
|
| 56 |
- "path" |
|
| 57 |
- "path/filepath" |
|
| 58 |
- "sync" |
|
| 59 |
- "unsafe" |
|
| 60 |
- |
|
| 61 |
- "github.com/containerd/containerd/sys" |
|
| 62 |
- "github.com/pkg/errors" |
|
| 63 |
- "github.com/sirupsen/logrus" |
|
| 64 |
- "golang.org/x/sys/unix" |
|
| 65 |
-) |
|
| 66 |
- |
|
| 67 |
-type pquotaState struct {
|
|
| 68 |
- sync.Mutex |
|
| 69 |
- nextProjectID uint32 |
|
| 70 |
-} |
|
| 71 |
- |
|
| 72 |
-var pquotaStateInst *pquotaState |
|
| 73 |
-var pquotaStateOnce sync.Once |
|
| 74 |
- |
|
| 75 |
-// getPquotaState - get global pquota state tracker instance |
|
| 76 |
-func getPquotaState() *pquotaState {
|
|
| 77 |
- pquotaStateOnce.Do(func() {
|
|
| 78 |
- pquotaStateInst = &pquotaState{
|
|
| 79 |
- nextProjectID: 1, |
|
| 80 |
- } |
|
| 81 |
- }) |
|
| 82 |
- return pquotaStateInst |
|
| 83 |
-} |
|
| 84 |
- |
|
| 85 |
-// registerBasePath - register a new base path and update nextProjectID |
|
| 86 |
-func (state *pquotaState) updateMinProjID(minProjectID uint32) {
|
|
| 87 |
- state.Lock() |
|
| 88 |
- defer state.Unlock() |
|
| 89 |
- if state.nextProjectID <= minProjectID {
|
|
| 90 |
- state.nextProjectID = minProjectID + 1 |
|
| 91 |
- } |
|
| 92 |
-} |
|
| 93 |
- |
|
| 94 |
-// NewControl - initialize project quota support. |
|
| 95 |
-// Test to make sure that quota can be set on a test dir and find |
|
| 96 |
-// the first project id to be used for the next container create. |
|
| 97 |
-// |
|
| 98 |
-// Returns nil (and error) if project quota is not supported. |
|
| 99 |
-// |
|
| 100 |
-// First get the project id of the home directory. |
|
| 101 |
-// This test will fail if the backing fs is not xfs. |
|
| 102 |
-// |
|
| 103 |
-// xfs_quota tool can be used to assign a project id to the driver home directory, e.g.: |
|
| 104 |
-// echo 999:/var/lib/docker/overlay2 >> /etc/projects |
|
| 105 |
-// echo docker:999 >> /etc/projid |
|
| 106 |
-// xfs_quota -x -c 'project -s docker' /<xfs mount point> |
|
| 107 |
-// |
|
| 108 |
-// In that case, the home directory project id will be used as a "start offset" |
|
| 109 |
-// and all containers will be assigned larger project ids (e.g. >= 1000). |
|
| 110 |
-// This is a way to prevent xfs_quota management from conflicting with docker. |
|
| 111 |
-// |
|
| 112 |
-// Then try to create a test directory with the next project id and set a quota |
|
| 113 |
-// on it. If that works, continue to scan existing containers to map allocated |
|
| 114 |
-// project ids. |
|
| 115 |
-// |
|
| 116 |
-func NewControl(basePath string) (*Control, error) {
|
|
| 117 |
- // |
|
| 118 |
- // If we are running in a user namespace quota won't be supported for |
|
| 119 |
- // now since makeBackingFsDev() will try to mknod(). |
|
| 120 |
- // |
|
| 121 |
- if sys.RunningInUserNS() {
|
|
| 122 |
- return nil, ErrQuotaNotSupported |
|
| 123 |
- } |
|
| 124 |
- |
|
| 125 |
- // |
|
| 126 |
- // create backing filesystem device node |
|
| 127 |
- // |
|
| 128 |
- backingFsBlockDev, err := makeBackingFsDev(basePath) |
|
| 129 |
- if err != nil {
|
|
| 130 |
- return nil, err |
|
| 131 |
- } |
|
| 132 |
- |
|
| 133 |
- // check if we can call quotactl with project quotas |
|
| 134 |
- // as a mechanism to determine (early) if we have support |
|
| 135 |
- hasQuotaSupport, err := hasQuotaSupport(backingFsBlockDev) |
|
| 136 |
- if err != nil {
|
|
| 137 |
- return nil, err |
|
| 138 |
- } |
|
| 139 |
- if !hasQuotaSupport {
|
|
| 140 |
- return nil, ErrQuotaNotSupported |
|
| 141 |
- } |
|
| 142 |
- |
|
| 143 |
- // |
|
| 144 |
- // Get project id of parent dir as minimal id to be used by driver |
|
| 145 |
- // |
|
| 146 |
- baseProjectID, err := getProjectID(basePath) |
|
| 147 |
- if err != nil {
|
|
| 148 |
- return nil, err |
|
| 149 |
- } |
|
| 150 |
- minProjectID := baseProjectID + 1 |
|
| 151 |
- |
|
| 152 |
- // |
|
| 153 |
- // Test if filesystem supports project quotas by trying to set |
|
| 154 |
- // a quota on the first available project id |
|
| 155 |
- // |
|
| 156 |
- quota := Quota{
|
|
| 157 |
- Size: 0, |
|
| 158 |
- } |
|
| 159 |
- if err := setProjectQuota(backingFsBlockDev, minProjectID, quota); err != nil {
|
|
| 160 |
- return nil, err |
|
| 161 |
- } |
|
| 162 |
- |
|
| 163 |
- q := Control{
|
|
| 164 |
- backingFsBlockDev: backingFsBlockDev, |
|
| 165 |
- quotas: make(map[string]uint32), |
|
| 166 |
- } |
|
| 167 |
- |
|
| 168 |
- // |
|
| 169 |
- // update minimum project ID |
|
| 170 |
- // |
|
| 171 |
- state := getPquotaState() |
|
| 172 |
- state.updateMinProjID(minProjectID) |
|
| 173 |
- |
|
| 174 |
- // |
|
| 175 |
- // get first project id to be used for next container |
|
| 176 |
- // |
|
| 177 |
- err = q.findNextProjectID(basePath, baseProjectID) |
|
| 178 |
- if err != nil {
|
|
| 179 |
- return nil, err |
|
| 180 |
- } |
|
| 181 |
- |
|
| 182 |
- logrus.Debugf("NewControl(%s): nextProjectID = %d", basePath, state.nextProjectID)
|
|
| 183 |
- return &q, nil |
|
| 184 |
-} |
|
| 185 |
- |
|
| 186 |
-// SetQuota - assign a unique project id to directory and set the quota limits |
|
| 187 |
-// for that project id |
|
| 188 |
-func (q *Control) SetQuota(targetPath string, quota Quota) error {
|
|
| 189 |
- q.RLock() |
|
| 190 |
- projectID, ok := q.quotas[targetPath] |
|
| 191 |
- q.RUnlock() |
|
| 192 |
- if !ok {
|
|
| 193 |
- state := getPquotaState() |
|
| 194 |
- state.Lock() |
|
| 195 |
- projectID = state.nextProjectID |
|
| 196 |
- |
|
| 197 |
- // |
|
| 198 |
- // assign project id to new container directory |
|
| 199 |
- // |
|
| 200 |
- err := setProjectID(targetPath, projectID) |
|
| 201 |
- if err != nil {
|
|
| 202 |
- state.Unlock() |
|
| 203 |
- return err |
|
| 204 |
- } |
|
| 205 |
- |
|
| 206 |
- state.nextProjectID++ |
|
| 207 |
- state.Unlock() |
|
| 208 |
- |
|
| 209 |
- q.Lock() |
|
| 210 |
- q.quotas[targetPath] = projectID |
|
| 211 |
- q.Unlock() |
|
| 212 |
- } |
|
| 213 |
- |
|
| 214 |
- // |
|
| 215 |
- // set the quota limit for the container's project id |
|
| 216 |
- // |
|
| 217 |
- logrus.Debugf("SetQuota(%s, %d): projectID=%d", targetPath, quota.Size, projectID)
|
|
| 218 |
- return setProjectQuota(q.backingFsBlockDev, projectID, quota) |
|
| 219 |
-} |
|
| 220 |
- |
|
| 221 |
-// setProjectQuota - set the quota for project id on xfs block device |
|
| 222 |
-func setProjectQuota(backingFsBlockDev string, projectID uint32, quota Quota) error {
|
|
| 223 |
- var d C.fs_disk_quota_t |
|
| 224 |
- d.d_version = C.FS_DQUOT_VERSION |
|
| 225 |
- d.d_id = C.__u32(projectID) |
|
| 226 |
- d.d_flags = C.XFS_PROJ_QUOTA |
|
| 227 |
- |
|
| 228 |
- d.d_fieldmask = C.FS_DQ_BHARD | C.FS_DQ_BSOFT |
|
| 229 |
- d.d_blk_hardlimit = C.__u64(quota.Size / 512) |
|
| 230 |
- d.d_blk_softlimit = d.d_blk_hardlimit |
|
| 231 |
- |
|
| 232 |
- var cs = C.CString(backingFsBlockDev) |
|
| 233 |
- defer C.free(unsafe.Pointer(cs)) |
|
| 234 |
- |
|
| 235 |
- _, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XSETPQLIM, |
|
| 236 |
- uintptr(unsafe.Pointer(cs)), uintptr(d.d_id), |
|
| 237 |
- uintptr(unsafe.Pointer(&d)), 0, 0) |
|
| 238 |
- if errno != 0 {
|
|
| 239 |
- return errors.Wrapf(errno, "failed to set quota limit for projid %d on %s", |
|
| 240 |
- projectID, backingFsBlockDev) |
|
| 241 |
- } |
|
| 242 |
- |
|
| 243 |
- return nil |
|
| 244 |
-} |
|
| 245 |
- |
|
| 246 |
-// GetQuota - get the quota limits of a directory that was configured with SetQuota |
|
| 247 |
-func (q *Control) GetQuota(targetPath string, quota *Quota) error {
|
|
| 248 |
- q.RLock() |
|
| 249 |
- projectID, ok := q.quotas[targetPath] |
|
| 250 |
- q.RUnlock() |
|
| 251 |
- if !ok {
|
|
| 252 |
- return errors.Errorf("quota not found for path: %s", targetPath)
|
|
| 253 |
- } |
|
| 254 |
- |
|
| 255 |
- // |
|
| 256 |
- // get the quota limit for the container's project id |
|
| 257 |
- // |
|
| 258 |
- var d C.fs_disk_quota_t |
|
| 259 |
- |
|
| 260 |
- var cs = C.CString(q.backingFsBlockDev) |
|
| 261 |
- defer C.free(unsafe.Pointer(cs)) |
|
| 262 |
- |
|
| 263 |
- _, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XGETPQUOTA, |
|
| 264 |
- uintptr(unsafe.Pointer(cs)), uintptr(C.__u32(projectID)), |
|
| 265 |
- uintptr(unsafe.Pointer(&d)), 0, 0) |
|
| 266 |
- if errno != 0 {
|
|
| 267 |
- return errors.Wrapf(errno, "Failed to get quota limit for projid %d on %s", |
|
| 268 |
- projectID, q.backingFsBlockDev) |
|
| 269 |
- } |
|
| 270 |
- quota.Size = uint64(d.d_blk_hardlimit) * 512 |
|
| 271 |
- |
|
| 272 |
- return nil |
|
| 273 |
-} |
|
| 274 |
- |
|
| 275 |
-// getProjectID - get the project id of path on xfs |
|
| 276 |
-func getProjectID(targetPath string) (uint32, error) {
|
|
| 277 |
- dir, err := openDir(targetPath) |
|
| 278 |
- if err != nil {
|
|
| 279 |
- return 0, err |
|
| 280 |
- } |
|
| 281 |
- defer closeDir(dir) |
|
| 282 |
- |
|
| 283 |
- var fsx C.struct_fsxattr |
|
| 284 |
- _, _, errno := unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR, |
|
| 285 |
- uintptr(unsafe.Pointer(&fsx))) |
|
| 286 |
- if errno != 0 {
|
|
| 287 |
- return 0, errors.Wrapf(errno, "failed to get projid for %s", targetPath) |
|
| 288 |
- } |
|
| 289 |
- |
|
| 290 |
- return uint32(fsx.fsx_projid), nil |
|
| 291 |
-} |
|
| 292 |
- |
|
| 293 |
-// setProjectID - set the project id of path on xfs |
|
| 294 |
-func setProjectID(targetPath string, projectID uint32) error {
|
|
| 295 |
- dir, err := openDir(targetPath) |
|
| 296 |
- if err != nil {
|
|
| 297 |
- return err |
|
| 298 |
- } |
|
| 299 |
- defer closeDir(dir) |
|
| 300 |
- |
|
| 301 |
- var fsx C.struct_fsxattr |
|
| 302 |
- _, _, errno := unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR, |
|
| 303 |
- uintptr(unsafe.Pointer(&fsx))) |
|
| 304 |
- if errno != 0 {
|
|
| 305 |
- return errors.Wrapf(errno, "failed to get projid for %s", targetPath) |
|
| 306 |
- } |
|
| 307 |
- fsx.fsx_projid = C.__u32(projectID) |
|
| 308 |
- fsx.fsx_xflags |= C.FS_XFLAG_PROJINHERIT |
|
| 309 |
- _, _, errno = unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSSETXATTR, |
|
| 310 |
- uintptr(unsafe.Pointer(&fsx))) |
|
| 311 |
- if errno != 0 {
|
|
| 312 |
- return errors.Wrapf(errno, "failed to set projid for %s", targetPath) |
|
| 313 |
- } |
|
| 314 |
- |
|
| 315 |
- return nil |
|
| 316 |
-} |
|
| 317 |
- |
|
| 318 |
-// findNextProjectID - find the next project id to be used for containers |
|
| 319 |
-// by scanning driver home directory to find used project ids |
|
| 320 |
-func (q *Control) findNextProjectID(home string, baseID uint32) error {
|
|
| 321 |
- state := getPquotaState() |
|
| 322 |
- state.Lock() |
|
| 323 |
- defer state.Unlock() |
|
| 324 |
- |
|
| 325 |
- checkProjID := func(path string) (uint32, error) {
|
|
| 326 |
- projid, err := getProjectID(path) |
|
| 327 |
- if err != nil {
|
|
| 328 |
- return projid, err |
|
| 329 |
- } |
|
| 330 |
- if projid > 0 {
|
|
| 331 |
- q.quotas[path] = projid |
|
| 332 |
- } |
|
| 333 |
- if state.nextProjectID <= projid {
|
|
| 334 |
- state.nextProjectID = projid + 1 |
|
| 335 |
- } |
|
| 336 |
- return projid, nil |
|
| 337 |
- } |
|
| 338 |
- |
|
| 339 |
- files, err := ioutil.ReadDir(home) |
|
| 340 |
- if err != nil {
|
|
| 341 |
- return errors.Errorf("read directory failed: %s", home)
|
|
| 342 |
- } |
|
| 343 |
- for _, file := range files {
|
|
| 344 |
- if !file.IsDir() {
|
|
| 345 |
- continue |
|
| 346 |
- } |
|
| 347 |
- path := filepath.Join(home, file.Name()) |
|
| 348 |
- projid, err := checkProjID(path) |
|
| 349 |
- if err != nil {
|
|
| 350 |
- return err |
|
| 351 |
- } |
|
| 352 |
- if projid > 0 && projid != baseID {
|
|
| 353 |
- continue |
|
| 354 |
- } |
|
| 355 |
- subfiles, err := ioutil.ReadDir(path) |
|
| 356 |
- if err != nil {
|
|
| 357 |
- return errors.Errorf("read directory failed: %s", path)
|
|
| 358 |
- } |
|
| 359 |
- for _, subfile := range subfiles {
|
|
| 360 |
- if !subfile.IsDir() {
|
|
| 361 |
- continue |
|
| 362 |
- } |
|
| 363 |
- subpath := filepath.Join(path, subfile.Name()) |
|
| 364 |
- _, err := checkProjID(subpath) |
|
| 365 |
- if err != nil {
|
|
| 366 |
- return err |
|
| 367 |
- } |
|
| 368 |
- } |
|
| 369 |
- } |
|
| 370 |
- |
|
| 371 |
- return nil |
|
| 372 |
-} |
|
| 373 |
- |
|
| 374 |
-func free(p *C.char) {
|
|
| 375 |
- C.free(unsafe.Pointer(p)) |
|
| 376 |
-} |
|
| 377 |
- |
|
| 378 |
-func openDir(path string) (*C.DIR, error) {
|
|
| 379 |
- Cpath := C.CString(path) |
|
| 380 |
- defer free(Cpath) |
|
| 381 |
- |
|
| 382 |
- dir := C.opendir(Cpath) |
|
| 383 |
- if dir == nil {
|
|
| 384 |
- return nil, errors.Errorf("failed to open dir: %s", path)
|
|
| 385 |
- } |
|
| 386 |
- return dir, nil |
|
| 387 |
-} |
|
| 388 |
- |
|
| 389 |
-func closeDir(dir *C.DIR) {
|
|
| 390 |
- if dir != nil {
|
|
| 391 |
- C.closedir(dir) |
|
| 392 |
- } |
|
| 393 |
-} |
|
| 394 |
- |
|
| 395 |
-func getDirFd(dir *C.DIR) uintptr {
|
|
| 396 |
- return uintptr(C.dirfd(dir)) |
|
| 397 |
-} |
|
| 398 |
- |
|
| 399 |
-// Get the backing block device of the driver home directory |
|
| 400 |
-// and create a block device node under the home directory |
|
| 401 |
-// to be used by quotactl commands |
|
| 402 |
-func makeBackingFsDev(home string) (string, error) {
|
|
| 403 |
- var stat unix.Stat_t |
|
| 404 |
- if err := unix.Stat(home, &stat); err != nil {
|
|
| 405 |
- return "", err |
|
| 406 |
- } |
|
| 407 |
- |
|
| 408 |
- backingFsBlockDev := path.Join(home, "backingFsBlockDev") |
|
| 409 |
- // Re-create just in case someone copied the home directory over to a new device |
|
| 410 |
- unix.Unlink(backingFsBlockDev) |
|
| 411 |
- err := unix.Mknod(backingFsBlockDev, unix.S_IFBLK|0600, int(stat.Dev)) |
|
| 412 |
- switch err {
|
|
| 413 |
- case nil: |
|
| 414 |
- return backingFsBlockDev, nil |
|
| 415 |
- |
|
| 416 |
- case unix.ENOSYS, unix.EPERM: |
|
| 417 |
- return "", ErrQuotaNotSupported |
|
| 418 |
- |
|
| 419 |
- default: |
|
| 420 |
- return "", errors.Wrapf(err, "failed to mknod %s", backingFsBlockDev) |
|
| 421 |
- } |
|
| 422 |
-} |
|
| 423 |
- |
|
| 424 |
-func hasQuotaSupport(backingFsBlockDev string) (bool, error) {
|
|
| 425 |
- var cs = C.CString(backingFsBlockDev) |
|
| 426 |
- defer free(cs) |
|
| 427 |
- var qstat C.fs_quota_stat_t |
|
| 428 |
- |
|
| 429 |
- _, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, uintptr(C.Q_XGETQSTAT_PRJQUOTA), uintptr(unsafe.Pointer(cs)), 0, uintptr(unsafe.Pointer(&qstat)), 0, 0) |
|
| 430 |
- if errno == 0 && qstat.qs_flags&C.FS_QUOTA_PDQ_ENFD > 0 && qstat.qs_flags&C.FS_QUOTA_PDQ_ACCT > 0 {
|
|
| 431 |
- return true, nil |
|
| 432 |
- } |
|
| 433 |
- |
|
| 434 |
- switch errno {
|
|
| 435 |
- // These are the known fatal errors, consider all other errors (ENOTTY, etc.. not supporting quota) |
|
| 436 |
- case unix.EFAULT, unix.ENOENT, unix.ENOTBLK, unix.EPERM: |
|
| 437 |
- default: |
|
| 438 |
- return false, nil |
|
| 439 |
- } |
|
| 440 |
- |
|
| 441 |
- return false, errno |
|
| 442 |
-} |
| 443 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,152 +0,0 @@ |
| 1 |
-// +build linux |
|
| 2 |
- |
|
| 3 |
-package quota // import "github.com/docker/docker/daemon/graphdriver/quota" |
|
| 4 |
- |
|
| 5 |
-import ( |
|
| 6 |
- "io" |
|
| 7 |
- "io/ioutil" |
|
| 8 |
- "os" |
|
| 9 |
- "os/exec" |
|
| 10 |
- "path/filepath" |
|
| 11 |
- "testing" |
|
| 12 |
- |
|
| 13 |
- "golang.org/x/sys/unix" |
|
| 14 |
- "gotest.tools/v3/assert" |
|
| 15 |
- is "gotest.tools/v3/assert/cmp" |
|
| 16 |
- "gotest.tools/v3/fs" |
|
| 17 |
-) |
|
| 18 |
- |
|
| 19 |
-// 10MB |
|
| 20 |
-const testQuotaSize = 10 * 1024 * 1024 |
|
| 21 |
-const imageSize = 64 * 1024 * 1024 |
|
| 22 |
- |
|
| 23 |
-func TestBlockDev(t *testing.T) {
|
|
| 24 |
- mkfs, err := exec.LookPath("mkfs.xfs")
|
|
| 25 |
- if err != nil {
|
|
| 26 |
- t.Skip("mkfs.xfs not found in PATH")
|
|
| 27 |
- } |
|
| 28 |
- |
|
| 29 |
- // create a sparse image |
|
| 30 |
- imageFile, err := ioutil.TempFile("", "xfs-image")
|
|
| 31 |
- if err != nil {
|
|
| 32 |
- t.Fatal(err) |
|
| 33 |
- } |
|
| 34 |
- imageFileName := imageFile.Name() |
|
| 35 |
- defer os.Remove(imageFileName) |
|
| 36 |
- if _, err = imageFile.Seek(imageSize-1, 0); err != nil {
|
|
| 37 |
- t.Fatal(err) |
|
| 38 |
- } |
|
| 39 |
- if _, err = imageFile.Write([]byte{0}); err != nil {
|
|
| 40 |
- t.Fatal(err) |
|
| 41 |
- } |
|
| 42 |
- if err = imageFile.Close(); err != nil {
|
|
| 43 |
- t.Fatal(err) |
|
| 44 |
- } |
|
| 45 |
- |
|
| 46 |
- // The reason for disabling these options is sometimes people run with a newer userspace |
|
| 47 |
- // than kernelspace |
|
| 48 |
- out, err := exec.Command(mkfs, "-m", "crc=0,finobt=0", imageFileName).CombinedOutput() |
|
| 49 |
- if len(out) > 0 {
|
|
| 50 |
- t.Log(string(out)) |
|
| 51 |
- } |
|
| 52 |
- if err != nil {
|
|
| 53 |
- t.Fatal(err) |
|
| 54 |
- } |
|
| 55 |
- |
|
| 56 |
- t.Run("testBlockDevQuotaDisabled", wrapMountTest(imageFileName, false, testBlockDevQuotaDisabled))
|
|
| 57 |
- t.Run("testBlockDevQuotaEnabled", wrapMountTest(imageFileName, true, testBlockDevQuotaEnabled))
|
|
| 58 |
- t.Run("testSmallerThanQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testSmallerThanQuota)))
|
|
| 59 |
- t.Run("testBiggerThanQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testBiggerThanQuota)))
|
|
| 60 |
- t.Run("testRetrieveQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testRetrieveQuota)))
|
|
| 61 |
-} |
|
| 62 |
- |
|
| 63 |
-func wrapMountTest(imageFileName string, enableQuota bool, testFunc func(t *testing.T, mountPoint, backingFsDev string)) func(*testing.T) {
|
|
| 64 |
- return func(t *testing.T) {
|
|
| 65 |
- mountOptions := "loop" |
|
| 66 |
- |
|
| 67 |
- if enableQuota {
|
|
| 68 |
- mountOptions = mountOptions + ",prjquota" |
|
| 69 |
- } |
|
| 70 |
- |
|
| 71 |
- mountPointDir := fs.NewDir(t, "xfs-mountPoint") |
|
| 72 |
- defer mountPointDir.Remove() |
|
| 73 |
- mountPoint := mountPointDir.Path() |
|
| 74 |
- |
|
| 75 |
- out, err := exec.Command("mount", "-o", mountOptions, imageFileName, mountPoint).CombinedOutput()
|
|
| 76 |
- if err != nil {
|
|
| 77 |
- _, err := os.Stat("/proc/fs/xfs")
|
|
| 78 |
- if os.IsNotExist(err) {
|
|
| 79 |
- t.Skip("no /proc/fs/xfs")
|
|
| 80 |
- } |
|
| 81 |
- } |
|
| 82 |
- |
|
| 83 |
- assert.NilError(t, err, "mount failed: %s", out) |
|
| 84 |
- |
|
| 85 |
- defer func() {
|
|
| 86 |
- assert.NilError(t, unix.Unmount(mountPoint, 0)) |
|
| 87 |
- }() |
|
| 88 |
- |
|
| 89 |
- backingFsDev, err := makeBackingFsDev(mountPoint) |
|
| 90 |
- assert.NilError(t, err) |
|
| 91 |
- |
|
| 92 |
- testFunc(t, mountPoint, backingFsDev) |
|
| 93 |
- } |
|
| 94 |
-} |
|
| 95 |
- |
|
| 96 |
-func testBlockDevQuotaDisabled(t *testing.T, mountPoint, backingFsDev string) {
|
|
| 97 |
- hasSupport, err := hasQuotaSupport(backingFsDev) |
|
| 98 |
- assert.NilError(t, err) |
|
| 99 |
- assert.Check(t, !hasSupport) |
|
| 100 |
-} |
|
| 101 |
- |
|
| 102 |
-func testBlockDevQuotaEnabled(t *testing.T, mountPoint, backingFsDev string) {
|
|
| 103 |
- hasSupport, err := hasQuotaSupport(backingFsDev) |
|
| 104 |
- assert.NilError(t, err) |
|
| 105 |
- assert.Check(t, hasSupport) |
|
| 106 |
-} |
|
| 107 |
- |
|
| 108 |
-func wrapQuotaTest(testFunc func(t *testing.T, ctrl *Control, mountPoint, testDir, testSubDir string)) func(t *testing.T, mountPoint, backingFsDev string) {
|
|
| 109 |
- return func(t *testing.T, mountPoint, backingFsDev string) {
|
|
| 110 |
- testDir, err := ioutil.TempDir(mountPoint, "per-test") |
|
| 111 |
- assert.NilError(t, err) |
|
| 112 |
- defer os.RemoveAll(testDir) |
|
| 113 |
- |
|
| 114 |
- ctrl, err := NewControl(testDir) |
|
| 115 |
- assert.NilError(t, err) |
|
| 116 |
- |
|
| 117 |
- testSubDir, err := ioutil.TempDir(testDir, "quota-test") |
|
| 118 |
- assert.NilError(t, err) |
|
| 119 |
- testFunc(t, ctrl, mountPoint, testDir, testSubDir) |
|
| 120 |
- } |
|
| 121 |
- |
|
| 122 |
-} |
|
| 123 |
- |
|
| 124 |
-func testSmallerThanQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
|
|
| 125 |
- assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
|
|
| 126 |
- smallerThanQuotaFile := filepath.Join(testSubDir, "smaller-than-quota") |
|
| 127 |
- assert.NilError(t, ioutil.WriteFile(smallerThanQuotaFile, make([]byte, testQuotaSize/2), 0644)) |
|
| 128 |
- assert.NilError(t, os.Remove(smallerThanQuotaFile)) |
|
| 129 |
-} |
|
| 130 |
- |
|
| 131 |
-func testBiggerThanQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
|
|
| 132 |
- // Make sure the quota is being enforced |
|
| 133 |
- // TODO: When we implement this under EXT4, we need to shed CAP_SYS_RESOURCE, otherwise |
|
| 134 |
- // we're able to violate quota without issue |
|
| 135 |
- assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
|
|
| 136 |
- |
|
| 137 |
- biggerThanQuotaFile := filepath.Join(testSubDir, "bigger-than-quota") |
|
| 138 |
- err := ioutil.WriteFile(biggerThanQuotaFile, make([]byte, testQuotaSize+1), 0644) |
|
| 139 |
- assert.Assert(t, is.ErrorContains(err, "")) |
|
| 140 |
- if err == io.ErrShortWrite {
|
|
| 141 |
- assert.NilError(t, os.Remove(biggerThanQuotaFile)) |
|
| 142 |
- } |
|
| 143 |
-} |
|
| 144 |
- |
|
| 145 |
-func testRetrieveQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
|
|
| 146 |
- // Validate that we can retrieve quota |
|
| 147 |
- assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
|
|
| 148 |
- |
|
| 149 |
- var q Quota |
|
| 150 |
- assert.NilError(t, ctrl.GetQuota(testSubDir, &q)) |
|
| 151 |
- assert.Check(t, is.Equal(uint64(testQuotaSize), q.Size)) |
|
| 152 |
-} |
| 153 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,18 +0,0 @@ |
| 1 |
-// +build linux,exclude_disk_quota linux,!cgo !linux |
|
| 2 |
- |
|
| 3 |
-package quota // import "github.com/docker/docker/daemon/graphdriver/quota" |
|
| 4 |
- |
|
| 5 |
-func NewControl(basePath string) (*Control, error) {
|
|
| 6 |
- return nil, ErrQuotaNotSupported |
|
| 7 |
-} |
|
| 8 |
- |
|
| 9 |
-// SetQuota - assign a unique project id to directory and set the quota limits |
|
| 10 |
-// for that project id |
|
| 11 |
-func (q *Control) SetQuota(targetPath string, quota Quota) error {
|
|
| 12 |
- return ErrQuotaNotSupported |
|
| 13 |
-} |
|
| 14 |
- |
|
| 15 |
-// GetQuota - get the quota limits of a directory that was configured with SetQuota |
|
| 16 |
-func (q *Control) GetQuota(targetPath string, quota *Quota) error {
|
|
| 17 |
- return ErrQuotaNotSupported |
|
| 18 |
-} |
| 19 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,16 +0,0 @@ |
| 1 |
-package quota // import "github.com/docker/docker/daemon/graphdriver/quota" |
|
| 2 |
- |
|
| 3 |
-import "sync" |
|
| 4 |
- |
|
| 5 |
-// Quota limit params - currently we only control blocks hard limit |
|
| 6 |
-type Quota struct {
|
|
| 7 |
- Size uint64 |
|
| 8 |
-} |
|
| 9 |
- |
|
| 10 |
-// Control - Context to be used by storage driver (e.g. overlay) |
|
| 11 |
-// who wants to apply project quotas to container dirs |
|
| 12 |
-type Control struct {
|
|
| 13 |
- backingFsBlockDev string |
|
| 14 |
- sync.RWMutex // protect nextProjectID and quotas map |
|
| 15 |
- quotas map[string]uint32 |
|
| 16 |
-} |
| ... | ... |
@@ -6,12 +6,12 @@ import ( |
| 6 | 6 |
"path/filepath" |
| 7 | 7 |
|
| 8 | 8 |
"github.com/docker/docker/daemon/graphdriver" |
| 9 |
- "github.com/docker/docker/daemon/graphdriver/quota" |
|
| 10 | 9 |
"github.com/docker/docker/errdefs" |
| 11 | 10 |
"github.com/docker/docker/pkg/containerfs" |
| 12 | 11 |
"github.com/docker/docker/pkg/idtools" |
| 13 | 12 |
"github.com/docker/docker/pkg/parsers" |
| 14 | 13 |
"github.com/docker/docker/pkg/system" |
| 14 |
+ "github.com/docker/docker/quota" |
|
| 15 | 15 |
units "github.com/docker/go-units" |
| 16 | 16 |
"github.com/opencontainers/selinux/go-selinux/label" |
| 17 | 17 |
"github.com/pkg/errors" |
| 9 | 9 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,19 @@ |
| 0 |
+package quota // import "github.com/docker/docker/quota" |
|
| 1 |
+ |
|
| 2 |
+import "github.com/docker/docker/errdefs" |
|
| 3 |
+ |
|
| 4 |
+var ( |
|
| 5 |
+ _ errdefs.ErrNotImplemented = (*errQuotaNotSupported)(nil) |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// ErrQuotaNotSupported indicates if were found the FS didn't have projects quotas available |
|
| 9 |
+var ErrQuotaNotSupported = errQuotaNotSupported{}
|
|
| 10 |
+ |
|
| 11 |
+type errQuotaNotSupported struct {
|
|
| 12 |
+} |
|
| 13 |
+ |
|
| 14 |
+func (e errQuotaNotSupported) NotImplemented() {}
|
|
| 15 |
+ |
|
| 16 |
+func (e errQuotaNotSupported) Error() string {
|
|
| 17 |
+ return "Filesystem does not support, or has not enabled quotas" |
|
| 18 |
+} |
| 0 | 19 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,442 @@ |
| 0 |
+// +build linux,!exclude_disk_quota,cgo |
|
| 1 |
+ |
|
| 2 |
+// |
|
| 3 |
+// projectquota.go - implements XFS project quota controls |
|
| 4 |
+// for setting quota limits on a newly created directory. |
|
| 5 |
+// It currently supports the legacy XFS specific ioctls. |
|
| 6 |
+// |
|
| 7 |
+// TODO: use generic quota control ioctl FS_IOC_FS{GET,SET}XATTR
|
|
| 8 |
+// for both xfs/ext4 for kernel version >= v4.5 |
|
| 9 |
+// |
|
| 10 |
+ |
|
| 11 |
+package quota // import "github.com/docker/docker/quota" |
|
| 12 |
+ |
|
| 13 |
+/* |
|
| 14 |
+#include <stdlib.h> |
|
| 15 |
+#include <dirent.h> |
|
| 16 |
+#include <linux/fs.h> |
|
| 17 |
+#include <linux/quota.h> |
|
| 18 |
+#include <linux/dqblk_xfs.h> |
|
| 19 |
+ |
|
| 20 |
+#ifndef FS_XFLAG_PROJINHERIT |
|
| 21 |
+struct fsxattr {
|
|
| 22 |
+ __u32 fsx_xflags; |
|
| 23 |
+ __u32 fsx_extsize; |
|
| 24 |
+ __u32 fsx_nextents; |
|
| 25 |
+ __u32 fsx_projid; |
|
| 26 |
+ unsigned char fsx_pad[12]; |
|
| 27 |
+}; |
|
| 28 |
+#define FS_XFLAG_PROJINHERIT 0x00000200 |
|
| 29 |
+#endif |
|
| 30 |
+#ifndef FS_IOC_FSGETXATTR |
|
| 31 |
+#define FS_IOC_FSGETXATTR _IOR ('X', 31, struct fsxattr)
|
|
| 32 |
+#endif |
|
| 33 |
+#ifndef FS_IOC_FSSETXATTR |
|
| 34 |
+#define FS_IOC_FSSETXATTR _IOW ('X', 32, struct fsxattr)
|
|
| 35 |
+#endif |
|
| 36 |
+ |
|
| 37 |
+#ifndef PRJQUOTA |
|
| 38 |
+#define PRJQUOTA 2 |
|
| 39 |
+#endif |
|
| 40 |
+#ifndef XFS_PROJ_QUOTA |
|
| 41 |
+#define XFS_PROJ_QUOTA 2 |
|
| 42 |
+#endif |
|
| 43 |
+#ifndef Q_XSETPQLIM |
|
| 44 |
+#define Q_XSETPQLIM QCMD(Q_XSETQLIM, PRJQUOTA) |
|
| 45 |
+#endif |
|
| 46 |
+#ifndef Q_XGETPQUOTA |
|
| 47 |
+#define Q_XGETPQUOTA QCMD(Q_XGETQUOTA, PRJQUOTA) |
|
| 48 |
+#endif |
|
| 49 |
+ |
|
| 50 |
+const int Q_XGETQSTAT_PRJQUOTA = QCMD(Q_XGETQSTAT, PRJQUOTA); |
|
| 51 |
+*/ |
|
| 52 |
+import "C" |
|
| 53 |
+import ( |
|
| 54 |
+ "io/ioutil" |
|
| 55 |
+ "path" |
|
| 56 |
+ "path/filepath" |
|
| 57 |
+ "sync" |
|
| 58 |
+ "unsafe" |
|
| 59 |
+ |
|
| 60 |
+ "github.com/containerd/containerd/sys" |
|
| 61 |
+ "github.com/pkg/errors" |
|
| 62 |
+ "github.com/sirupsen/logrus" |
|
| 63 |
+ "golang.org/x/sys/unix" |
|
| 64 |
+) |
|
| 65 |
+ |
|
| 66 |
+type pquotaState struct {
|
|
| 67 |
+ sync.Mutex |
|
| 68 |
+ nextProjectID uint32 |
|
| 69 |
+} |
|
| 70 |
+ |
|
| 71 |
+var pquotaStateInst *pquotaState |
|
| 72 |
+var pquotaStateOnce sync.Once |
|
| 73 |
+ |
|
| 74 |
+// getPquotaState - get global pquota state tracker instance |
|
| 75 |
+func getPquotaState() *pquotaState {
|
|
| 76 |
+ pquotaStateOnce.Do(func() {
|
|
| 77 |
+ pquotaStateInst = &pquotaState{
|
|
| 78 |
+ nextProjectID: 1, |
|
| 79 |
+ } |
|
| 80 |
+ }) |
|
| 81 |
+ return pquotaStateInst |
|
| 82 |
+} |
|
| 83 |
+ |
|
| 84 |
+// registerBasePath - register a new base path and update nextProjectID |
|
| 85 |
+func (state *pquotaState) updateMinProjID(minProjectID uint32) {
|
|
| 86 |
+ state.Lock() |
|
| 87 |
+ defer state.Unlock() |
|
| 88 |
+ if state.nextProjectID <= minProjectID {
|
|
| 89 |
+ state.nextProjectID = minProjectID + 1 |
|
| 90 |
+ } |
|
| 91 |
+} |
|
| 92 |
+ |
|
| 93 |
+// NewControl - initialize project quota support. |
|
| 94 |
+// Test to make sure that quota can be set on a test dir and find |
|
| 95 |
+// the first project id to be used for the next container create. |
|
| 96 |
+// |
|
| 97 |
+// Returns nil (and error) if project quota is not supported. |
|
| 98 |
+// |
|
| 99 |
+// First get the project id of the home directory. |
|
| 100 |
+// This test will fail if the backing fs is not xfs. |
|
| 101 |
+// |
|
| 102 |
+// xfs_quota tool can be used to assign a project id to the driver home directory, e.g.: |
|
| 103 |
+// echo 999:/var/lib/docker/overlay2 >> /etc/projects |
|
| 104 |
+// echo docker:999 >> /etc/projid |
|
| 105 |
+// xfs_quota -x -c 'project -s docker' /<xfs mount point> |
|
| 106 |
+// |
|
| 107 |
+// In that case, the home directory project id will be used as a "start offset" |
|
| 108 |
+// and all containers will be assigned larger project ids (e.g. >= 1000). |
|
| 109 |
+// This is a way to prevent xfs_quota management from conflicting with docker. |
|
| 110 |
+// |
|
| 111 |
+// Then try to create a test directory with the next project id and set a quota |
|
| 112 |
+// on it. If that works, continue to scan existing containers to map allocated |
|
| 113 |
+// project ids. |
|
| 114 |
+// |
|
| 115 |
+func NewControl(basePath string) (*Control, error) {
|
|
| 116 |
+ // |
|
| 117 |
+ // If we are running in a user namespace quota won't be supported for |
|
| 118 |
+ // now since makeBackingFsDev() will try to mknod(). |
|
| 119 |
+ // |
|
| 120 |
+ if sys.RunningInUserNS() {
|
|
| 121 |
+ return nil, ErrQuotaNotSupported |
|
| 122 |
+ } |
|
| 123 |
+ |
|
| 124 |
+ // |
|
| 125 |
+ // create backing filesystem device node |
|
| 126 |
+ // |
|
| 127 |
+ backingFsBlockDev, err := makeBackingFsDev(basePath) |
|
| 128 |
+ if err != nil {
|
|
| 129 |
+ return nil, err |
|
| 130 |
+ } |
|
| 131 |
+ |
|
| 132 |
+ // check if we can call quotactl with project quotas |
|
| 133 |
+ // as a mechanism to determine (early) if we have support |
|
| 134 |
+ hasQuotaSupport, err := hasQuotaSupport(backingFsBlockDev) |
|
| 135 |
+ if err != nil {
|
|
| 136 |
+ return nil, err |
|
| 137 |
+ } |
|
| 138 |
+ if !hasQuotaSupport {
|
|
| 139 |
+ return nil, ErrQuotaNotSupported |
|
| 140 |
+ } |
|
| 141 |
+ |
|
| 142 |
+ // |
|
| 143 |
+ // Get project id of parent dir as minimal id to be used by driver |
|
| 144 |
+ // |
|
| 145 |
+ baseProjectID, err := getProjectID(basePath) |
|
| 146 |
+ if err != nil {
|
|
| 147 |
+ return nil, err |
|
| 148 |
+ } |
|
| 149 |
+ minProjectID := baseProjectID + 1 |
|
| 150 |
+ |
|
| 151 |
+ // |
|
| 152 |
+ // Test if filesystem supports project quotas by trying to set |
|
| 153 |
+ // a quota on the first available project id |
|
| 154 |
+ // |
|
| 155 |
+ quota := Quota{
|
|
| 156 |
+ Size: 0, |
|
| 157 |
+ } |
|
| 158 |
+ if err := setProjectQuota(backingFsBlockDev, minProjectID, quota); err != nil {
|
|
| 159 |
+ return nil, err |
|
| 160 |
+ } |
|
| 161 |
+ |
|
| 162 |
+ q := Control{
|
|
| 163 |
+ backingFsBlockDev: backingFsBlockDev, |
|
| 164 |
+ quotas: make(map[string]uint32), |
|
| 165 |
+ } |
|
| 166 |
+ |
|
| 167 |
+ // |
|
| 168 |
+ // update minimum project ID |
|
| 169 |
+ // |
|
| 170 |
+ state := getPquotaState() |
|
| 171 |
+ state.updateMinProjID(minProjectID) |
|
| 172 |
+ |
|
| 173 |
+ // |
|
| 174 |
+ // get first project id to be used for next container |
|
| 175 |
+ // |
|
| 176 |
+ err = q.findNextProjectID(basePath, baseProjectID) |
|
| 177 |
+ if err != nil {
|
|
| 178 |
+ return nil, err |
|
| 179 |
+ } |
|
| 180 |
+ |
|
| 181 |
+ logrus.Debugf("NewControl(%s): nextProjectID = %d", basePath, state.nextProjectID)
|
|
| 182 |
+ return &q, nil |
|
| 183 |
+} |
|
| 184 |
+ |
|
| 185 |
+// SetQuota - assign a unique project id to directory and set the quota limits |
|
| 186 |
+// for that project id |
|
| 187 |
+func (q *Control) SetQuota(targetPath string, quota Quota) error {
|
|
| 188 |
+ q.RLock() |
|
| 189 |
+ projectID, ok := q.quotas[targetPath] |
|
| 190 |
+ q.RUnlock() |
|
| 191 |
+ if !ok {
|
|
| 192 |
+ state := getPquotaState() |
|
| 193 |
+ state.Lock() |
|
| 194 |
+ projectID = state.nextProjectID |
|
| 195 |
+ |
|
| 196 |
+ // |
|
| 197 |
+ // assign project id to new container directory |
|
| 198 |
+ // |
|
| 199 |
+ err := setProjectID(targetPath, projectID) |
|
| 200 |
+ if err != nil {
|
|
| 201 |
+ state.Unlock() |
|
| 202 |
+ return err |
|
| 203 |
+ } |
|
| 204 |
+ |
|
| 205 |
+ state.nextProjectID++ |
|
| 206 |
+ state.Unlock() |
|
| 207 |
+ |
|
| 208 |
+ q.Lock() |
|
| 209 |
+ q.quotas[targetPath] = projectID |
|
| 210 |
+ q.Unlock() |
|
| 211 |
+ } |
|
| 212 |
+ |
|
| 213 |
+ // |
|
| 214 |
+ // set the quota limit for the container's project id |
|
| 215 |
+ // |
|
| 216 |
+ logrus.Debugf("SetQuota(%s, %d): projectID=%d", targetPath, quota.Size, projectID)
|
|
| 217 |
+ return setProjectQuota(q.backingFsBlockDev, projectID, quota) |
|
| 218 |
+} |
|
| 219 |
+ |
|
| 220 |
+// setProjectQuota - set the quota for project id on xfs block device |
|
| 221 |
+func setProjectQuota(backingFsBlockDev string, projectID uint32, quota Quota) error {
|
|
| 222 |
+ var d C.fs_disk_quota_t |
|
| 223 |
+ d.d_version = C.FS_DQUOT_VERSION |
|
| 224 |
+ d.d_id = C.__u32(projectID) |
|
| 225 |
+ d.d_flags = C.XFS_PROJ_QUOTA |
|
| 226 |
+ |
|
| 227 |
+ d.d_fieldmask = C.FS_DQ_BHARD | C.FS_DQ_BSOFT |
|
| 228 |
+ d.d_blk_hardlimit = C.__u64(quota.Size / 512) |
|
| 229 |
+ d.d_blk_softlimit = d.d_blk_hardlimit |
|
| 230 |
+ |
|
| 231 |
+ var cs = C.CString(backingFsBlockDev) |
|
| 232 |
+ defer C.free(unsafe.Pointer(cs)) |
|
| 233 |
+ |
|
| 234 |
+ _, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XSETPQLIM, |
|
| 235 |
+ uintptr(unsafe.Pointer(cs)), uintptr(d.d_id), |
|
| 236 |
+ uintptr(unsafe.Pointer(&d)), 0, 0) |
|
| 237 |
+ if errno != 0 {
|
|
| 238 |
+ return errors.Wrapf(errno, "failed to set quota limit for projid %d on %s", |
|
| 239 |
+ projectID, backingFsBlockDev) |
|
| 240 |
+ } |
|
| 241 |
+ |
|
| 242 |
+ return nil |
|
| 243 |
+} |
|
| 244 |
+ |
|
| 245 |
+// GetQuota - get the quota limits of a directory that was configured with SetQuota |
|
| 246 |
+func (q *Control) GetQuota(targetPath string, quota *Quota) error {
|
|
| 247 |
+ q.RLock() |
|
| 248 |
+ projectID, ok := q.quotas[targetPath] |
|
| 249 |
+ q.RUnlock() |
|
| 250 |
+ if !ok {
|
|
| 251 |
+ return errors.Errorf("quota not found for path: %s", targetPath)
|
|
| 252 |
+ } |
|
| 253 |
+ |
|
| 254 |
+ // |
|
| 255 |
+ // get the quota limit for the container's project id |
|
| 256 |
+ // |
|
| 257 |
+ var d C.fs_disk_quota_t |
|
| 258 |
+ |
|
| 259 |
+ var cs = C.CString(q.backingFsBlockDev) |
|
| 260 |
+ defer C.free(unsafe.Pointer(cs)) |
|
| 261 |
+ |
|
| 262 |
+ _, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XGETPQUOTA, |
|
| 263 |
+ uintptr(unsafe.Pointer(cs)), uintptr(C.__u32(projectID)), |
|
| 264 |
+ uintptr(unsafe.Pointer(&d)), 0, 0) |
|
| 265 |
+ if errno != 0 {
|
|
| 266 |
+ return errors.Wrapf(errno, "Failed to get quota limit for projid %d on %s", |
|
| 267 |
+ projectID, q.backingFsBlockDev) |
|
| 268 |
+ } |
|
| 269 |
+ quota.Size = uint64(d.d_blk_hardlimit) * 512 |
|
| 270 |
+ |
|
| 271 |
+ return nil |
|
| 272 |
+} |
|
| 273 |
+ |
|
| 274 |
+// getProjectID - get the project id of path on xfs |
|
| 275 |
+func getProjectID(targetPath string) (uint32, error) {
|
|
| 276 |
+ dir, err := openDir(targetPath) |
|
| 277 |
+ if err != nil {
|
|
| 278 |
+ return 0, err |
|
| 279 |
+ } |
|
| 280 |
+ defer closeDir(dir) |
|
| 281 |
+ |
|
| 282 |
+ var fsx C.struct_fsxattr |
|
| 283 |
+ _, _, errno := unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR, |
|
| 284 |
+ uintptr(unsafe.Pointer(&fsx))) |
|
| 285 |
+ if errno != 0 {
|
|
| 286 |
+ return 0, errors.Wrapf(errno, "failed to get projid for %s", targetPath) |
|
| 287 |
+ } |
|
| 288 |
+ |
|
| 289 |
+ return uint32(fsx.fsx_projid), nil |
|
| 290 |
+} |
|
| 291 |
+ |
|
| 292 |
+// setProjectID - set the project id of path on xfs |
|
| 293 |
+func setProjectID(targetPath string, projectID uint32) error {
|
|
| 294 |
+ dir, err := openDir(targetPath) |
|
| 295 |
+ if err != nil {
|
|
| 296 |
+ return err |
|
| 297 |
+ } |
|
| 298 |
+ defer closeDir(dir) |
|
| 299 |
+ |
|
| 300 |
+ var fsx C.struct_fsxattr |
|
| 301 |
+ _, _, errno := unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR, |
|
| 302 |
+ uintptr(unsafe.Pointer(&fsx))) |
|
| 303 |
+ if errno != 0 {
|
|
| 304 |
+ return errors.Wrapf(errno, "failed to get projid for %s", targetPath) |
|
| 305 |
+ } |
|
| 306 |
+ fsx.fsx_projid = C.__u32(projectID) |
|
| 307 |
+ fsx.fsx_xflags |= C.FS_XFLAG_PROJINHERIT |
|
| 308 |
+ _, _, errno = unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSSETXATTR, |
|
| 309 |
+ uintptr(unsafe.Pointer(&fsx))) |
|
| 310 |
+ if errno != 0 {
|
|
| 311 |
+ return errors.Wrapf(errno, "failed to set projid for %s", targetPath) |
|
| 312 |
+ } |
|
| 313 |
+ |
|
| 314 |
+ return nil |
|
| 315 |
+} |
|
| 316 |
+ |
|
| 317 |
+// findNextProjectID - find the next project id to be used for containers |
|
| 318 |
+// by scanning driver home directory to find used project ids |
|
| 319 |
+func (q *Control) findNextProjectID(home string, baseID uint32) error {
|
|
| 320 |
+ state := getPquotaState() |
|
| 321 |
+ state.Lock() |
|
| 322 |
+ defer state.Unlock() |
|
| 323 |
+ |
|
| 324 |
+ checkProjID := func(path string) (uint32, error) {
|
|
| 325 |
+ projid, err := getProjectID(path) |
|
| 326 |
+ if err != nil {
|
|
| 327 |
+ return projid, err |
|
| 328 |
+ } |
|
| 329 |
+ if projid > 0 {
|
|
| 330 |
+ q.quotas[path] = projid |
|
| 331 |
+ } |
|
| 332 |
+ if state.nextProjectID <= projid {
|
|
| 333 |
+ state.nextProjectID = projid + 1 |
|
| 334 |
+ } |
|
| 335 |
+ return projid, nil |
|
| 336 |
+ } |
|
| 337 |
+ |
|
| 338 |
+ files, err := ioutil.ReadDir(home) |
|
| 339 |
+ if err != nil {
|
|
| 340 |
+ return errors.Errorf("read directory failed: %s", home)
|
|
| 341 |
+ } |
|
| 342 |
+ for _, file := range files {
|
|
| 343 |
+ if !file.IsDir() {
|
|
| 344 |
+ continue |
|
| 345 |
+ } |
|
| 346 |
+ path := filepath.Join(home, file.Name()) |
|
| 347 |
+ projid, err := checkProjID(path) |
|
| 348 |
+ if err != nil {
|
|
| 349 |
+ return err |
|
| 350 |
+ } |
|
| 351 |
+ if projid > 0 && projid != baseID {
|
|
| 352 |
+ continue |
|
| 353 |
+ } |
|
| 354 |
+ subfiles, err := ioutil.ReadDir(path) |
|
| 355 |
+ if err != nil {
|
|
| 356 |
+ return errors.Errorf("read directory failed: %s", path)
|
|
| 357 |
+ } |
|
| 358 |
+ for _, subfile := range subfiles {
|
|
| 359 |
+ if !subfile.IsDir() {
|
|
| 360 |
+ continue |
|
| 361 |
+ } |
|
| 362 |
+ subpath := filepath.Join(path, subfile.Name()) |
|
| 363 |
+ _, err := checkProjID(subpath) |
|
| 364 |
+ if err != nil {
|
|
| 365 |
+ return err |
|
| 366 |
+ } |
|
| 367 |
+ } |
|
| 368 |
+ } |
|
| 369 |
+ |
|
| 370 |
+ return nil |
|
| 371 |
+} |
|
| 372 |
+ |
|
| 373 |
+func free(p *C.char) {
|
|
| 374 |
+ C.free(unsafe.Pointer(p)) |
|
| 375 |
+} |
|
| 376 |
+ |
|
| 377 |
+func openDir(path string) (*C.DIR, error) {
|
|
| 378 |
+ Cpath := C.CString(path) |
|
| 379 |
+ defer free(Cpath) |
|
| 380 |
+ |
|
| 381 |
+ dir := C.opendir(Cpath) |
|
| 382 |
+ if dir == nil {
|
|
| 383 |
+ return nil, errors.Errorf("failed to open dir: %s", path)
|
|
| 384 |
+ } |
|
| 385 |
+ return dir, nil |
|
| 386 |
+} |
|
| 387 |
+ |
|
| 388 |
+func closeDir(dir *C.DIR) {
|
|
| 389 |
+ if dir != nil {
|
|
| 390 |
+ C.closedir(dir) |
|
| 391 |
+ } |
|
| 392 |
+} |
|
| 393 |
+ |
|
| 394 |
+func getDirFd(dir *C.DIR) uintptr {
|
|
| 395 |
+ return uintptr(C.dirfd(dir)) |
|
| 396 |
+} |
|
| 397 |
+ |
|
| 398 |
+// Get the backing block device of the driver home directory |
|
| 399 |
+// and create a block device node under the home directory |
|
| 400 |
+// to be used by quotactl commands |
|
| 401 |
+func makeBackingFsDev(home string) (string, error) {
|
|
| 402 |
+ var stat unix.Stat_t |
|
| 403 |
+ if err := unix.Stat(home, &stat); err != nil {
|
|
| 404 |
+ return "", err |
|
| 405 |
+ } |
|
| 406 |
+ |
|
| 407 |
+ backingFsBlockDev := path.Join(home, "backingFsBlockDev") |
|
| 408 |
+ // Re-create just in case someone copied the home directory over to a new device |
|
| 409 |
+ unix.Unlink(backingFsBlockDev) |
|
| 410 |
+ err := unix.Mknod(backingFsBlockDev, unix.S_IFBLK|0600, int(stat.Dev)) |
|
| 411 |
+ switch err {
|
|
| 412 |
+ case nil: |
|
| 413 |
+ return backingFsBlockDev, nil |
|
| 414 |
+ |
|
| 415 |
+ case unix.ENOSYS, unix.EPERM: |
|
| 416 |
+ return "", ErrQuotaNotSupported |
|
| 417 |
+ |
|
| 418 |
+ default: |
|
| 419 |
+ return "", errors.Wrapf(err, "failed to mknod %s", backingFsBlockDev) |
|
| 420 |
+ } |
|
| 421 |
+} |
|
| 422 |
+ |
|
| 423 |
+func hasQuotaSupport(backingFsBlockDev string) (bool, error) {
|
|
| 424 |
+ var cs = C.CString(backingFsBlockDev) |
|
| 425 |
+ defer free(cs) |
|
| 426 |
+ var qstat C.fs_quota_stat_t |
|
| 427 |
+ |
|
| 428 |
+ _, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, uintptr(C.Q_XGETQSTAT_PRJQUOTA), uintptr(unsafe.Pointer(cs)), 0, uintptr(unsafe.Pointer(&qstat)), 0, 0) |
|
| 429 |
+ if errno == 0 && qstat.qs_flags&C.FS_QUOTA_PDQ_ENFD > 0 && qstat.qs_flags&C.FS_QUOTA_PDQ_ACCT > 0 {
|
|
| 430 |
+ return true, nil |
|
| 431 |
+ } |
|
| 432 |
+ |
|
| 433 |
+ switch errno {
|
|
| 434 |
+ // These are the known fatal errors, consider all other errors (ENOTTY, etc.. not supporting quota) |
|
| 435 |
+ case unix.EFAULT, unix.ENOENT, unix.ENOTBLK, unix.EPERM: |
|
| 436 |
+ default: |
|
| 437 |
+ return false, nil |
|
| 438 |
+ } |
|
| 439 |
+ |
|
| 440 |
+ return false, errno |
|
| 441 |
+} |
| 0 | 442 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,152 @@ |
| 0 |
+// +build linux |
|
| 1 |
+ |
|
| 2 |
+package quota // import "github.com/docker/docker/quota" |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "io" |
|
| 6 |
+ "io/ioutil" |
|
| 7 |
+ "os" |
|
| 8 |
+ "os/exec" |
|
| 9 |
+ "path/filepath" |
|
| 10 |
+ "testing" |
|
| 11 |
+ |
|
| 12 |
+ "golang.org/x/sys/unix" |
|
| 13 |
+ "gotest.tools/v3/assert" |
|
| 14 |
+ is "gotest.tools/v3/assert/cmp" |
|
| 15 |
+ "gotest.tools/v3/fs" |
|
| 16 |
+) |
|
| 17 |
+ |
|
| 18 |
+// 10MB |
|
| 19 |
+const testQuotaSize = 10 * 1024 * 1024 |
|
| 20 |
+const imageSize = 64 * 1024 * 1024 |
|
| 21 |
+ |
|
| 22 |
+func TestBlockDev(t *testing.T) {
|
|
| 23 |
+ mkfs, err := exec.LookPath("mkfs.xfs")
|
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ t.Skip("mkfs.xfs not found in PATH")
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ // create a sparse image |
|
| 29 |
+ imageFile, err := ioutil.TempFile("", "xfs-image")
|
|
| 30 |
+ if err != nil {
|
|
| 31 |
+ t.Fatal(err) |
|
| 32 |
+ } |
|
| 33 |
+ imageFileName := imageFile.Name() |
|
| 34 |
+ defer os.Remove(imageFileName) |
|
| 35 |
+ if _, err = imageFile.Seek(imageSize-1, 0); err != nil {
|
|
| 36 |
+ t.Fatal(err) |
|
| 37 |
+ } |
|
| 38 |
+ if _, err = imageFile.Write([]byte{0}); err != nil {
|
|
| 39 |
+ t.Fatal(err) |
|
| 40 |
+ } |
|
| 41 |
+ if err = imageFile.Close(); err != nil {
|
|
| 42 |
+ t.Fatal(err) |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ // The reason for disabling these options is sometimes people run with a newer userspace |
|
| 46 |
+ // than kernelspace |
|
| 47 |
+ out, err := exec.Command(mkfs, "-m", "crc=0,finobt=0", imageFileName).CombinedOutput() |
|
| 48 |
+ if len(out) > 0 {
|
|
| 49 |
+ t.Log(string(out)) |
|
| 50 |
+ } |
|
| 51 |
+ if err != nil {
|
|
| 52 |
+ t.Fatal(err) |
|
| 53 |
+ } |
|
| 54 |
+ |
|
| 55 |
+ t.Run("testBlockDevQuotaDisabled", wrapMountTest(imageFileName, false, testBlockDevQuotaDisabled))
|
|
| 56 |
+ t.Run("testBlockDevQuotaEnabled", wrapMountTest(imageFileName, true, testBlockDevQuotaEnabled))
|
|
| 57 |
+ t.Run("testSmallerThanQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testSmallerThanQuota)))
|
|
| 58 |
+ t.Run("testBiggerThanQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testBiggerThanQuota)))
|
|
| 59 |
+ t.Run("testRetrieveQuota", wrapMountTest(imageFileName, true, wrapQuotaTest(testRetrieveQuota)))
|
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+func wrapMountTest(imageFileName string, enableQuota bool, testFunc func(t *testing.T, mountPoint, backingFsDev string)) func(*testing.T) {
|
|
| 63 |
+ return func(t *testing.T) {
|
|
| 64 |
+ mountOptions := "loop" |
|
| 65 |
+ |
|
| 66 |
+ if enableQuota {
|
|
| 67 |
+ mountOptions = mountOptions + ",prjquota" |
|
| 68 |
+ } |
|
| 69 |
+ |
|
| 70 |
+ mountPointDir := fs.NewDir(t, "xfs-mountPoint") |
|
| 71 |
+ defer mountPointDir.Remove() |
|
| 72 |
+ mountPoint := mountPointDir.Path() |
|
| 73 |
+ |
|
| 74 |
+ out, err := exec.Command("mount", "-o", mountOptions, imageFileName, mountPoint).CombinedOutput()
|
|
| 75 |
+ if err != nil {
|
|
| 76 |
+ _, err := os.Stat("/proc/fs/xfs")
|
|
| 77 |
+ if os.IsNotExist(err) {
|
|
| 78 |
+ t.Skip("no /proc/fs/xfs")
|
|
| 79 |
+ } |
|
| 80 |
+ } |
|
| 81 |
+ |
|
| 82 |
+ assert.NilError(t, err, "mount failed: %s", out) |
|
| 83 |
+ |
|
| 84 |
+ defer func() {
|
|
| 85 |
+ assert.NilError(t, unix.Unmount(mountPoint, 0)) |
|
| 86 |
+ }() |
|
| 87 |
+ |
|
| 88 |
+ backingFsDev, err := makeBackingFsDev(mountPoint) |
|
| 89 |
+ assert.NilError(t, err) |
|
| 90 |
+ |
|
| 91 |
+ testFunc(t, mountPoint, backingFsDev) |
|
| 92 |
+ } |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+func testBlockDevQuotaDisabled(t *testing.T, mountPoint, backingFsDev string) {
|
|
| 96 |
+ hasSupport, err := hasQuotaSupport(backingFsDev) |
|
| 97 |
+ assert.NilError(t, err) |
|
| 98 |
+ assert.Check(t, !hasSupport) |
|
| 99 |
+} |
|
| 100 |
+ |
|
| 101 |
+func testBlockDevQuotaEnabled(t *testing.T, mountPoint, backingFsDev string) {
|
|
| 102 |
+ hasSupport, err := hasQuotaSupport(backingFsDev) |
|
| 103 |
+ assert.NilError(t, err) |
|
| 104 |
+ assert.Check(t, hasSupport) |
|
| 105 |
+} |
|
| 106 |
+ |
|
| 107 |
+func wrapQuotaTest(testFunc func(t *testing.T, ctrl *Control, mountPoint, testDir, testSubDir string)) func(t *testing.T, mountPoint, backingFsDev string) {
|
|
| 108 |
+ return func(t *testing.T, mountPoint, backingFsDev string) {
|
|
| 109 |
+ testDir, err := ioutil.TempDir(mountPoint, "per-test") |
|
| 110 |
+ assert.NilError(t, err) |
|
| 111 |
+ defer os.RemoveAll(testDir) |
|
| 112 |
+ |
|
| 113 |
+ ctrl, err := NewControl(testDir) |
|
| 114 |
+ assert.NilError(t, err) |
|
| 115 |
+ |
|
| 116 |
+ testSubDir, err := ioutil.TempDir(testDir, "quota-test") |
|
| 117 |
+ assert.NilError(t, err) |
|
| 118 |
+ testFunc(t, ctrl, mountPoint, testDir, testSubDir) |
|
| 119 |
+ } |
|
| 120 |
+ |
|
| 121 |
+} |
|
| 122 |
+ |
|
| 123 |
+func testSmallerThanQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
|
|
| 124 |
+ assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
|
|
| 125 |
+ smallerThanQuotaFile := filepath.Join(testSubDir, "smaller-than-quota") |
|
| 126 |
+ assert.NilError(t, ioutil.WriteFile(smallerThanQuotaFile, make([]byte, testQuotaSize/2), 0644)) |
|
| 127 |
+ assert.NilError(t, os.Remove(smallerThanQuotaFile)) |
|
| 128 |
+} |
|
| 129 |
+ |
|
| 130 |
+func testBiggerThanQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
|
|
| 131 |
+ // Make sure the quota is being enforced |
|
| 132 |
+ // TODO: When we implement this under EXT4, we need to shed CAP_SYS_RESOURCE, otherwise |
|
| 133 |
+ // we're able to violate quota without issue |
|
| 134 |
+ assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
|
|
| 135 |
+ |
|
| 136 |
+ biggerThanQuotaFile := filepath.Join(testSubDir, "bigger-than-quota") |
|
| 137 |
+ err := ioutil.WriteFile(biggerThanQuotaFile, make([]byte, testQuotaSize+1), 0644) |
|
| 138 |
+ assert.Assert(t, is.ErrorContains(err, "")) |
|
| 139 |
+ if err == io.ErrShortWrite {
|
|
| 140 |
+ assert.NilError(t, os.Remove(biggerThanQuotaFile)) |
|
| 141 |
+ } |
|
| 142 |
+} |
|
| 143 |
+ |
|
| 144 |
+func testRetrieveQuota(t *testing.T, ctrl *Control, homeDir, testDir, testSubDir string) {
|
|
| 145 |
+ // Validate that we can retrieve quota |
|
| 146 |
+ assert.NilError(t, ctrl.SetQuota(testSubDir, Quota{testQuotaSize}))
|
|
| 147 |
+ |
|
| 148 |
+ var q Quota |
|
| 149 |
+ assert.NilError(t, ctrl.GetQuota(testSubDir, &q)) |
|
| 150 |
+ assert.Check(t, is.Equal(uint64(testQuotaSize), q.Size)) |
|
| 151 |
+} |
| 0 | 152 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,18 @@ |
| 0 |
+// +build linux,exclude_disk_quota linux,!cgo !linux |
|
| 1 |
+ |
|
| 2 |
+package quota // import "github.com/docker/docker/quota" |
|
| 3 |
+ |
|
| 4 |
+func NewControl(basePath string) (*Control, error) {
|
|
| 5 |
+ return nil, ErrQuotaNotSupported |
|
| 6 |
+} |
|
| 7 |
+ |
|
| 8 |
+// SetQuota - assign a unique project id to directory and set the quota limits |
|
| 9 |
+// for that project id |
|
| 10 |
+func (q *Control) SetQuota(targetPath string, quota Quota) error {
|
|
| 11 |
+ return ErrQuotaNotSupported |
|
| 12 |
+} |
|
| 13 |
+ |
|
| 14 |
+// GetQuota - get the quota limits of a directory that was configured with SetQuota |
|
| 15 |
+func (q *Control) GetQuota(targetPath string, quota *Quota) error {
|
|
| 16 |
+ return ErrQuotaNotSupported |
|
| 17 |
+} |
| 0 | 18 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,16 @@ |
| 0 |
+package quota // import "github.com/docker/docker/quota" |
|
| 1 |
+ |
|
| 2 |
+import "sync" |
|
| 3 |
+ |
|
| 4 |
+// Quota limit params - currently we only control blocks hard limit |
|
| 5 |
+type Quota struct {
|
|
| 6 |
+ Size uint64 |
|
| 7 |
+} |
|
| 8 |
+ |
|
| 9 |
+// Control - Context to be used by storage driver (e.g. overlay) |
|
| 10 |
+// who wants to apply project quotas to container dirs |
|
| 11 |
+type Control struct {
|
|
| 12 |
+ backingFsBlockDev string |
|
| 13 |
+ sync.RWMutex // protect nextProjectID and quotas map |
|
| 14 |
+ quotas map[string]uint32 |
|
| 15 |
+} |