diff --git a/apis/swagger.yml b/apis/swagger.yml index 76802b3fb..aa5d0e8e5 100644 --- a/apis/swagger.yml +++ b/apis/swagger.yml @@ -1964,7 +1964,12 @@ definitions: description: "Initial script executed in container. The script will be executed before entrypoint or command" DiskQuota: type: "object" - description: "Set disk quota for container" + description: | + Set disk quota for container. + Key is the dir in container. + Value is disk quota size for the dir. + / means rootfs dir in container. + .* includes rootfs dir and all volume dir. x-nullable: true additionalProperties: type: "string" @@ -1975,7 +1980,10 @@ definitions: type: "string" QuotaID: type: "string" - description: "set disk quota by specified quota id, if id < 0, it means pouchd alloc a unique quota id" + description: | + Set disk quota by specified quota id. + If QuotaID <= 0, it means pouchd should allocate a unique quota id by sequence automatically. + By default, a quota ID is mapped to only one container. And one quota ID can include several mountpoint. NetPriority: description: "net priority." type: "integer" diff --git a/apis/types/container_config.go b/apis/types/container_config.go index 87c96a75c..88ca86a40 100644 --- a/apis/types/container_config.go +++ b/apis/types/container_config.go @@ -38,7 +38,12 @@ type ContainerConfig struct { // Whether to generate the network files(/etc/hostname, /etc/hosts and /etc/resolv.conf) for container. DisableNetworkFiles bool `json:"DisableNetworkFiles,omitempty"` - // Set disk quota for container + // Set disk quota for container. + // Key is the dir in container. + // Value is disk quota size for the dir. + // / means rootfs dir in container. + // .* includes rootfs dir and all volume dir. + // DiskQuota map[string]string `json:"DiskQuota,omitempty"` // The domain name to use for the container. @@ -85,7 +90,10 @@ type ContainerConfig struct { // Open `stdin` OpenStdin bool `json:"OpenStdin,omitempty"` - // set disk quota by specified quota id, if id < 0, it means pouchd alloc a unique quota id + // Set disk quota by specified quota id. + // If QuotaID <= 0, it means pouchd should allocate a unique quota id by sequence automatically. + // By default, a quota ID is mapped to only one container. And one quota ID can include several mountpoint. + // QuotaID string `json:"QuotaID,omitempty"` // Whether to start container in rich container mode. (default false) diff --git a/daemon/mgr/container.go b/daemon/mgr/container.go index d2004a141..d46762bf6 100644 --- a/daemon/mgr/container.go +++ b/daemon/mgr/container.go @@ -1095,7 +1095,7 @@ func (mgr *ContainerManager) updateContainerDiskQuota(ctx context.Context, c *Co qid = uint32(id) if id < 0 { // QuotaID is < 0, it means pouchd alloc a unique quota id. - qid, err = quota.GetNextQuatoID() + qid, err = quota.GetNextQuotaID() if err != nil { return errors.Wrap(err, "failed to get next quota id") } @@ -2250,7 +2250,7 @@ func (mgr *ContainerManager) setMountPointDiskQuota(ctx context.Context, c *Cont // if QuotaID is < 0, it means pouchd alloc a unique quota id. if id < 0 { - qid, err = quota.GetNextQuatoID() + qid, err = quota.GetNextQuotaID() if err != nil { return errors.Wrap(err, "failed to get next quota id") } diff --git a/daemon/mgr/spec_hook.go b/daemon/mgr/spec_hook.go index 46bb7844f..f8ae41edc 100644 --- a/daemon/mgr/spec_hook.go +++ b/daemon/mgr/spec_hook.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "github.com/alibaba/pouch/pkg/system" "github.com/alibaba/pouch/storage/quota" specs "github.com/opencontainers/runtime-spec/specs-go" @@ -98,7 +99,7 @@ func setMountTab(ctx context.Context, c *Container, spec *SpecWrapper) error { // set rootfs mount tab context := "/ / ext4 rw 0 0\n" - if rootID, e := quota.GetDevID(c.BaseFS); e == nil { + if rootID, e := system.GetDevID(c.BaseFS); e == nil { _, _, rootFsType := quota.CheckMountpoint(rootID) if len(rootFsType) > 0 { context = fmt.Sprintf("/ / %s rw 0 0\n", rootFsType) @@ -118,7 +119,7 @@ func setMountTab(ctx context.Context, c *Container, spec *SpecWrapper) error { } tempLine := fmt.Sprintf("/dev/v%02dd %s ext4 rw 0 0\n", i, m.Destination) - if tmpID, e := quota.GetDevID(m.Source); e == nil { + if tmpID, e := system.GetDevID(m.Source); e == nil { _, _, fsType := quota.CheckMountpoint(tmpID) if len(fsType) > 0 { tempLine = fmt.Sprintf("/dev/v%02dd %s %s rw 0 0\n", i, m.Destination, fsType) diff --git a/pkg/system/device.go b/pkg/system/device.go new file mode 100644 index 000000000..e3f2ecb5e --- /dev/null +++ b/pkg/system/device.go @@ -0,0 +1,19 @@ +// +build linux + +package system + +import ( + "syscall" + + "github.com/sirupsen/logrus" +) + +// GetDevID returns device id via syscall according to the input directory. +func GetDevID(dir string) (uint64, error) { + var st syscall.Stat_t + if err := syscall.Stat(dir, &st); err != nil { + logrus.Warnf("failed to get device id of dir %s: %v", dir, err) + return 0, err + } + return st.Dev, nil +} diff --git a/pkg/system/sysinfo.go b/pkg/system/sysinfo.go index cfc11e59a..77d88eb88 100644 --- a/pkg/system/sysinfo.go +++ b/pkg/system/sysinfo.go @@ -1,3 +1,5 @@ +// +build linux + package system import ( diff --git a/storage/quota/grpquota.go b/storage/quota/grpquota.go index 33a37983c..f387e3299 100644 --- a/storage/quota/grpquota.go +++ b/storage/quota/grpquota.go @@ -1,3 +1,5 @@ +// +build linux + package quota import ( @@ -10,29 +12,39 @@ import ( "github.com/alibaba/pouch/pkg/bytefmt" "github.com/alibaba/pouch/pkg/exec" + "github.com/alibaba/pouch/pkg/system" "github.com/sirupsen/logrus" ) -// GrpQuota represents group quota. -type GrpQuota struct { +var ( + grpQuotaType = "grpquota" +) + +// GrpQuotaDriver represents group quota driver. +type GrpQuotaDriver struct { lock sync.Mutex + // quotaIDs saves all of quota ids. - quotaIDs map[uint32]uint32 - // mountPoints saves all the mount point of volume. + // key: quota ID which means this ID is used in the global scope. + // value: stuct{} + quotaIDs map[uint32]struct{} + + // mountPoints saves all the mount point of volume which have already been enforced disk quota. + // key: device ID such as /dev/sda1 + // value: the mountpoint of the device in the filesystem mountPoints map[uint64]string - // quotaLastID is used to mark last used quota id. - quotaLastID uint32 + + // LastID is used to mark last used quota ID. + // quota ID is allocated increasingly by sequence one by one. + lastID uint32 } -// StartQuotaDriver is used to start quota driver. -func (quota *GrpQuota) StartQuotaDriver(dir string) (string, error) { +// EnforceQuota is used to enforce disk quota effect on specified directory. +func (quota *GrpQuotaDriver) EnforceQuota(dir string) (string, error) { logrus.Debugf("start group quota driver: %s", dir) - if !UseQuota { - return "", nil - } - devID, err := GetDevID(dir) + devID, err := system.GetDevID(dir) if err != nil { return "", err } @@ -41,21 +53,24 @@ func (quota *GrpQuota) StartQuotaDriver(dir string) (string, error) { defer quota.lock.Unlock() if mp, ok := quota.mountPoints[devID]; ok { + // if the device has already been enforced quota, just return. return mp, nil } mountPoint, hasQuota, _ := quota.CheckMountpoint(devID) if len(mountPoint) == 0 { - return mountPoint, fmt.Errorf("mountPoint not found: %s", dir) + return mountPoint, fmt.Errorf("mountPoint not found for the device on which dir %s lies", dir) } if !hasQuota { + // remount option grpquota for mountpoint + // FIXME: ignore or record err? Why inconsistent with prjquota (rudyfly) _, _, _, err := exec.Run(0, "mount", "-o", "remount,grpquota", mountPoint) if err != nil { return "", err } } - vfsVersion, quotaFilename, err := quota.getVFSVersionAndQuotaFile(devID) + vfsVersion, quotaFilename, err := getVFSVersionAndQuotaFile(devID) if err != nil { return "", err } @@ -79,7 +94,7 @@ func (quota *GrpQuota) StartQuotaDriver(dir string) (string, error) { os.Remove(filename) return mountPoint, err } - if err := quota.setUserQuota(0, 0, mountPoint); err != nil { + if err := quota.setQuota(0, 0, mountPoint); err != nil { os.Remove(filename) return mountPoint, err } @@ -105,20 +120,16 @@ func (quota *GrpQuota) StartQuotaDriver(dir string) (string, error) { // SetSubtree is used to set quota id for directory, // setfattr -n system.subtree -v $QUOTAID -func (quota *GrpQuota) SetSubtree(dir string, qid uint32) (uint32, error) { +func (quota *GrpQuotaDriver) SetSubtree(dir string, qid uint32) (uint32, error) { logrus.Debugf("set subtree, dir: %s, quotaID: %d", dir, qid) - if !UseQuota { - return 0, nil - } - id := qid var err error if id == 0 { - id = quota.GetFileAttr(dir) + id = quota.GetQuotaIDInFileAttr(dir) if id > 0 { return id, nil } - id, err = quota.GetNextQuatoID() + id, err = quota.GetNextQuotaID() } if err != nil { @@ -129,91 +140,91 @@ func (quota *GrpQuota) SetSubtree(dir string, qid uint32) (uint32, error) { return id, err } -// SetDiskQuota is used to set quota for directory. -func (quota *GrpQuota) SetDiskQuota(dir string, size string, quotaID uint32) error { - logrus.Debugf("set disk quota, dir: %s, size: %s, quotaID: %d", dir, size, quotaID) - if !UseQuota { - return nil - } - - mountPoint, err := quota.StartQuotaDriver(dir) - if err != nil { - return err - } - if len(mountPoint) == 0 { - return fmt.Errorf("mountpoint not found: %s", dir) - } - - id, err := quota.SetSubtree(dir, quotaID) - if err != nil || id == 0 { - return fmt.Errorf("subtree not found: %s %v", dir, err) - } - - limit, err := bytefmt.ToKilobytes(size) - if err != nil { - return err - } - - return quota.setUserQuota(id, limit, mountPoint) -} - // CheckMountpoint is used to check mount point. +// It returns mointpoint, enable quota and filesystem type of the device. +// // cat /proc/mounts as follows: // /dev/sda3 / ext4 rw,relatime,data=ordered 0 0 // /dev/sda2 /boot/grub2 ext4 rw,relatime,stripe=4,data=ordered 0 0 // /dev/sda5 /home ext4 rw,relatime,data=ordered 0 0 -// /dev/sdb1 /home/pouch ext4 rw,relatime,grpquota,data=ordered 0 0 +// /dev/sdb1 /home/pouch ext4 rw,relatime,prjquota,data=ordered 0 0 // tmpfs /run tmpfs rw,nosuid,nodev,mode=755 0 0 // tmpfs /sys/fs/cgroup tmpfs ro,nosuid,nodev,noexec,mode=755 0 0 // cgroup /sys/fs/cgroup/cpuset,cpu,cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpuacct,cpu,cpuset 0 0 // cgroup /sys/fs/cgroup/devices cgroup rw,nosuid,nodev,noexec,relatime,devices 0 0 // cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,memory 0 0 // cgroup /sys/fs/cgroup/blkio cgroup rw,nosuid,nodev,noexec,relatime,blkio 0 0 -func (quota *GrpQuota) CheckMountpoint(devID uint64) (string, bool, string) { +func (quota *GrpQuotaDriver) CheckMountpoint(devID uint64) (string, bool, string) { logrus.Debugf("check mountpoint, devID: %d", devID) - output, err := ioutil.ReadFile("/proc/mounts") + output, err := ioutil.ReadFile(procMountFile) if err != nil { - logrus.Warnf("ReadFile: %v", err) + logrus.Warnf("failed to ReadFile %s: %v", procMountFile, err) return "", false, "" } - var mountPoint, fsType string - hasQuota := false - // /dev/sdb1 /home/pouch ext4 rw,relatime,grpquota,data=ordered 0 0 + // /dev/sdb1 /home/pouch ext4 rw,relatime,prjquota,data=ordered 0 0 for _, line := range strings.Split(string(output), "\n") { parts := strings.Split(line, " ") if len(parts) != 6 { continue } - devID2, _ := GetDevID(parts[1]) - if devID == devID2 { - mountPoint = parts[1] - fsType = parts[2] - for _, opt := range strings.Split(parts[3], ",") { - if opt == "grpquota" { - hasQuota = true - } + + mountPoint := parts[1] + fsType := parts[2] + + devID2, _ := system.GetDevID(mountPoint) + if devID != devID2 { + continue + } + + for _, value := range strings.Split(parts[3], ",") { + if value == "grpquota" { + return mountPoint, true, fsType } - break } } - return mountPoint, hasQuota, fsType + return "", false, "" } -func (quota *GrpQuota) setUserQuota(quotaID uint32, diskQuota uint64, mountPoint string) error { +// SetDiskQuota is used to set quota for directory. +func (quota *GrpQuotaDriver) SetDiskQuota(dir string, size string, quotaID uint32) error { + logrus.Debugf("set disk quota, dir: %s, size: %s, quotaID: %d", dir, size, quotaID) + + mountPoint, err := quota.EnforceQuota(dir) + if err != nil { + return err + } + if len(mountPoint) == 0 { + return fmt.Errorf("mountpoint not found: %s", dir) + } + + id, err := quota.SetSubtree(dir, quotaID) + if err != nil || id == 0 { + return fmt.Errorf("subtree not found: %s %v", dir, err) + } + + limit, err := bytefmt.ToKilobytes(size) + if err != nil { + return err + } + + return quota.setQuota(id, limit, mountPoint) +} + +func (quota *GrpQuotaDriver) setQuota(quotaID uint32, diskQuota uint64, mountPoint string) error { logrus.Debugf("set user quota, quotaID: %d, limit: %d, mountpoint: %s", quotaID, diskQuota, mountPoint) - uid := strconv.FormatUint(uint64(quotaID), 10) + quotaIDStr := strconv.FormatUint(uint64(quotaID), 10) limit := strconv.FormatUint(diskQuota, 10) - _, _, _, err := exec.Run(0, "setquota", "-g", uid, "0", limit, "0", "0", mountPoint) + _, _, _, err := exec.Run(0, "setquota", "-g", quotaIDStr, "0", limit, "0", "0", mountPoint) return err } -// GetFileAttr returns the directory attributes +// GetQuotaIDInFileAttr returns quota ID in the directory attributes. // getfattr -n system.subtree --only-values --absolute-names / -func (quota *GrpQuota) GetFileAttr(dir string) uint32 { +func (quota *GrpQuotaDriver) GetQuotaIDInFileAttr(dir string) uint32 { logrus.Debugf("get file attr, dir: %s", dir) v := 0 @@ -224,8 +235,8 @@ func (quota *GrpQuota) GetFileAttr(dir string) uint32 { return uint32(v) } -// SetFileAttr is used to set file attributes. -func (quota *GrpQuota) SetFileAttr(dir string, id uint32) error { +// SetQuotaIDInFileAttr is used to set quota ID in file attributes. +func (quota *GrpQuotaDriver) SetQuotaIDInFileAttr(dir string, id uint32) error { logrus.Debugf("set file attr, dir: %s, quotaID: %d", dir, id) strid := strconv.FormatUint(uint64(id), 10) @@ -233,65 +244,25 @@ func (quota *GrpQuota) SetFileAttr(dir string, id uint32) error { return err } -// SetFileAttrNoOutput is used to set file attributes without error. -func (quota *GrpQuota) SetFileAttrNoOutput(dir string, id uint32) { - strid := strconv.FormatUint(uint64(id), 10) +// SetQuotaIDInFileAttrNoOutput is used to set file attributes of quota ID without error. +func (quota *GrpQuotaDriver) SetQuotaIDInFileAttrNoOutput(dir string, quotaID uint32) { + strid := strconv.FormatUint(uint64(quotaID), 10) exec.Run(0, "setfattr", "-n", "system.subtree", "-v", strid, dir) } -// load -// repquota -gan -// Group used soft hard grace used soft hard grace -// ---------------------------------------------------------------------- -// #0 -- 494472 0 0 938 0 0 -// #54 -- 8 0 0 2 0 0 -// #4 -- 16 0 0 4 0 0 -// #22 -- 28 0 0 4 0 0 -// #16777220 +- 2048576 0 2048575 9 0 0 -// #500 -- 47504 0 0 101 0 0 -// #16777221 -- 3048576 0 3048576 8 0 0 -func (quota *GrpQuota) loadQuotaIDs() (uint32, error) { - minID := QuotaMinID - _, stdout, _, err := exec.Run(0, "repquota", "-gan") - if err != nil { - return minID, err - } - - lines := strings.Split(string(stdout), "\n") - for _, line := range lines { - if len(line) == 0 || line[0] != '#' { - continue - } - parts := strings.Split(line, " ") - if len(parts) < 2 { - continue - } - id, err := strconv.Atoi(parts[0][1:]) - uid := uint32(id) - if err == nil && uid > QuotaMinID { - quota.quotaIDs[uid] = 1 - if uid > minID { - minID = uid - } - } - } - logrus.Infof("Load repquota ids: %d, list: %v", len(quota.quotaIDs), quota.quotaIDs) - return minID, nil -} - -// GetNextQuatoID returns the next available quota id. -func (quota *GrpQuota) GetNextQuatoID() (uint32, error) { +// GetNextQuotaID returns the next available quota id. +func (quota *GrpQuotaDriver) GetNextQuotaID() (uint32, error) { quota.lock.Lock() defer quota.lock.Unlock() - if quota.quotaLastID == 0 { + if quota.lastID == 0 { var err error - quota.quotaLastID, err = quota.loadQuotaIDs() + quota.quotaIDs, quota.lastID, err = loadQuotaIDs("-gan") if err != nil { return 0, err } } - id := quota.quotaLastID + id := quota.lastID for { if id < QuotaMinID { id = QuotaMinID @@ -301,44 +272,48 @@ func (quota *GrpQuota) GetNextQuatoID() (uint32, error) { break } } - quota.quotaIDs[id] = 1 - quota.quotaLastID = id + quota.quotaIDs[id] = struct{}{} + quota.lastID = id logrus.Debugf("get next project quota id: %d", id) return id, nil } -func (quota *GrpQuota) getVFSVersionAndQuotaFile(devID uint64) (string, string, error) { - output, err := ioutil.ReadFile("/proc/mounts") +func getVFSVersionAndQuotaFile(devID uint64) (string, string, error) { + output, err := ioutil.ReadFile(procMountFile) if err != nil { - logrus.Warnf("ReadFile: %v", err) + logrus.Warnf("failed to read file %s: %v", procMountFile, err) return "", "", err } vfsVersion := "vfsv0" quotaFilename := "aquota.group" for _, line := range strings.Split(string(output), "\n") { + // TODO: add an example here to make following code readable. + // /dev/sdb1 /home/pouch ext4 rw,relatime,prjquota,data=ordered 0 0 ? parts := strings.Split(line, " ") if len(parts) != 6 { continue } - devID2, _ := GetDevID(parts[1]) - if devID == devID2 { - for _, opt := range strings.Split(parts[3], ",") { - items := strings.SplitN(opt, "=", 2) - if len(items) != 2 { - continue - } - switch items[0] { - case "jqfmt": - vfsVersion = items[1] - case "grpjquota": - quotaFilename = items[1] - } + devID2, _ := system.GetDevID(parts[1]) + if devID != devID2 { + continue + } + + for _, opt := range strings.Split(parts[3], ",") { + items := strings.SplitN(opt, "=", 2) + if len(items) != 2 { + continue + } + switch items[0] { + case "jqfmt": + vfsVersion = items[1] + case "grpjquota": + quotaFilename = items[1] } - break } + return vfsVersion, quotaFilename, nil } return vfsVersion, quotaFilename, nil diff --git a/storage/quota/prjquota.go b/storage/quota/prjquota.go index bede74411..5308ce2d6 100644 --- a/storage/quota/prjquota.go +++ b/storage/quota/prjquota.go @@ -1,3 +1,5 @@ +// +build linux + package quota import ( @@ -11,35 +13,49 @@ import ( "github.com/alibaba/pouch/pkg/bytefmt" "github.com/alibaba/pouch/pkg/exec" + "github.com/alibaba/pouch/pkg/system" "github.com/sirupsen/logrus" ) -// PrjQuota represents project quota. -type PrjQuota struct { +var ( + prjQuotaType = "prjquota" +) + +// PrjQuotaDriver represents project quota driver. +type PrjQuotaDriver struct { lock sync.Mutex + // quotaIDs saves all of quota ids. - quotaIDs map[uint32]uint32 - // mountPoints saves all the mount point of volume. + // key: quota ID which means this ID is used in the global scope. + // value: stuct{} + quotaIDs map[uint32]struct{} + + // mountPoints saves all the mount point of volume which have already been enforced disk quota. + // key: device ID such as /dev/sda1 + // value: the mountpoint of the device in the filesystem mountPoints map[uint64]string + // devLimits saves all the limit of device. + // key: device ID + // value: the storage upper limit size of the device(unit:B) devLimits map[uint64]uint64 - // quotaLastID is used to mark last used quota id. - quotaLastID uint32 + + // lastID is used to mark last used quota ID. + // quota ID is allocated increasingly by sequence one by one. + lastID uint32 } -// StartQuotaDriver is used to start quota driver. -func (quota *PrjQuota) StartQuotaDriver(dir string) (string, error) { +// EnforceQuota is used to enforce disk quota effect on specified directory. +func (quota *PrjQuotaDriver) EnforceQuota(dir string) (string, error) { logrus.Debugf("start project quota driver: %s", dir) - if !UseQuota { - return "", nil - } - devID, err := GetDevID(dir) + devID, err := system.GetDevID(dir) if err != nil { return "", err } + // set limit of dir's device in driver if _, err = quota.setDevLimit(dir, devID); err != nil { return "", err } @@ -48,65 +64,67 @@ func (quota *PrjQuota) StartQuotaDriver(dir string) (string, error) { defer quota.lock.Unlock() if mp, ok := quota.mountPoints[devID]; ok { + // if the device has already been enforced quota, just return. return mp, nil } mountPoint, hasQuota, _ := quota.CheckMountpoint(devID) if len(mountPoint) == 0 { - return mountPoint, fmt.Errorf("mountPoint not found: %s", dir) + return mountPoint, fmt.Errorf("mountPoint not found for the device on which dir %s lies", dir) } if !hasQuota { + // remount option prjquota for mountpoint + // FIXME: ignore or record err? (rudyfly) exec.Run(0, "mount", "-o", "remount,prjquota", mountPoint) } - // on + // use tool quotaon to set disk quota for mountpoint _, _, stderr, err := exec.Run(0, "quotaon", "-P", mountPoint) if err != nil { if strings.Contains(stderr, " File exists") { err = nil } else { + // FIXME: this else is quite strange mountPoint = "" } } + // record device which has quota settings quota.mountPoints[devID] = mountPoint + return mountPoint, err } -// SetSubtree is used to set quota id for directory, -//chattr -p qid +P $QUOTAID -func (quota *PrjQuota) SetSubtree(dir string, qid uint32) (uint32, error) { +// SetSubtree is used to set quota id for substree dir which is container's root dir. +// For container, it has its own root dir. +// And this dir is a subtree of the host dir which is mapped to a device. +// chattr -p qid +P $QUOTAID +func (quota *PrjQuotaDriver) SetSubtree(dir string, qid uint32) (uint32, error) { logrus.Debugf("set subtree, dir: %s, quotaID: %d", dir, qid) - if !UseQuota { - return 0, nil - } - id := qid var err error + if id == 0 { - id = quota.GetFileAttr(dir) + id = quota.GetQuotaIDInFileAttr(dir) if id > 0 { return id, nil } - id, err = quota.GetNextQuatoID() + if id, err = quota.GetNextQuotaID(); err != nil { + return 0, err + } } - if err != nil { - return 0, err - } strid := strconv.FormatUint(uint64(id), 10) _, _, _, err = exec.Run(0, "chattr", "-p", strid, "+P", dir) return id, err } -// SetDiskQuota is used to set quota for directory. -func (quota *PrjQuota) SetDiskQuota(dir string, size string, quotaID uint32) error { +// SetDiskQuota uses the following two parameters to set disk quota for a directory. +// * quota size: a byte size of requested quota. +// * quota ID: an ID represent quota attr which is used in the global scope. +func (quota *PrjQuotaDriver) SetDiskQuota(dir string, size string, quotaID uint32) error { logrus.Debugf("set disk quota, dir: %s, size: %s, quotaID: %d", dir, size, quotaID) - if !UseQuota { - return nil - } - - mountPoint, err := quota.StartQuotaDriver(dir) + mountPoint, err := quota.EnforceQuota(dir) if err != nil { return err } @@ -129,10 +147,12 @@ func (quota *PrjQuota) SetDiskQuota(dir string, size string, quotaID uint32) err return err } - return quota.setUserQuota(id, limit, mountPoint) + return quota.setQuota(id, limit, mountPoint) } // CheckMountpoint is used to check mount point. +// It returns mointpoint, enable quota and filesystem type of the device. +// // cat /proc/mounts as follows: // /dev/sda3 / ext4 rw,relatime,data=ordered 0 0 // /dev/sda2 /boot/grub2 ext4 rw,relatime,stripe=4,data=ordered 0 0 @@ -144,137 +164,113 @@ func (quota *PrjQuota) SetDiskQuota(dir string, size string, quotaID uint32) err // cgroup /sys/fs/cgroup/devices cgroup rw,nosuid,nodev,noexec,relatime,devices 0 0 // cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,memory 0 0 // cgroup /sys/fs/cgroup/blkio cgroup rw,nosuid,nodev,noexec,relatime,blkio 0 0 -func (quota *PrjQuota) CheckMountpoint(devID uint64) (string, bool, string) { +func (quota *PrjQuotaDriver) CheckMountpoint(devID uint64) (string, bool, string) { logrus.Debugf("check mountpoint, devID: %d", devID) - output, err := ioutil.ReadFile("/proc/mounts") + output, err := ioutil.ReadFile(procMountFile) if err != nil { - logrus.Warnf("ReadFile: %v", err) + logrus.Warnf("failed to ReadFile %s: %v", procMountFile, err) return "", false, "" } - var mountPoint, fsType string - hasQuota := false // /dev/sdb1 /home/pouch ext4 rw,relatime,prjquota,data=ordered 0 0 for _, line := range strings.Split(string(output), "\n") { parts := strings.Split(line, " ") if len(parts) != 6 { continue } - devID2, _ := GetDevID(parts[1]) - - if devID == devID2 { - mountPoint = parts[1] - fsType = parts[2] - for _, opt := range strings.Split(parts[3], ",") { - if opt == "prjquota" { - hasQuota = true - } + + mountPoint := parts[1] + fsType := parts[2] + + devID2, _ := system.GetDevID(mountPoint) + if devID != devID2 { + continue + } + + for _, value := range strings.Split(parts[3], ",") { + if value == "prjquota" { + return mountPoint, true, fsType } - break } } - return mountPoint, hasQuota, fsType + return "", false, "" } -func (quota *PrjQuota) setUserQuota(quotaID uint32, diskQuota uint64, mountPoint string) error { - logrus.Debugf("set user quota, quotaID: %d, limit: %d, mountpoint: %s", quotaID, diskQuota, mountPoint) - - uid := strconv.FormatUint(uint64(quotaID), 10) - limit := strconv.FormatUint(diskQuota, 10) - _, _, _, err := exec.Run(0, "setquota", "-P", uid, "0", limit, "0", "0", mountPoint) +// setQuota uses system tool "setquota" to set project quota for binding of limit and mountpoint and quotaID. +// * quotaID: quota ID which means this ID is used in the global scope. +// * blockLimit: block limit number for mountpoint. +// * mountPoint: the mountpoint of the device in the filesystem +func (quota *PrjQuotaDriver) setQuota(quotaID uint32, blockLimit uint64, mountPoint string) error { + logrus.Debugf("set project quota, quotaID: %d, limit: %d, mountpoint: %s", quotaID, blockLimit, mountPoint) + + quotaIDStr := strconv.FormatUint(uint64(quotaID), 10) + blockLimitStr := strconv.FormatUint(blockLimit, 10) + // set project quota + _, _, _, err := exec.Run(0, "setquota", "-P", quotaIDStr, "0", blockLimitStr, "0", "0", mountPoint) return err } -// GetFileAttr returns the directory attributes -// lsattr -p $dir -func (quota *PrjQuota) GetFileAttr(dir string) uint32 { +// GetQuotaIDInFileAttr gets attributes of the file which is in the inode. +// The returned result is quota ID. +// return 0 if failure happens, since quota ID must be positive. +// execution command: `lsattr -p $dir` +func (quota *PrjQuotaDriver) GetQuotaIDInFileAttr(dir string) uint32 { parent := path.Dir(dir) qid := 0 _, out, _, err := exec.Run(0, "lsattr", "-p", parent) if err != nil { + // failure, then return invalid value 0 for quota ID. return 0 } + // example output: + // 16777256 --------------e---P ./exampleDir lines := strings.Split(out, "\n") for _, line := range lines { parts := strings.Split(line, " ") if len(parts) > 2 && parts[2] == dir { + // find the corresponding quota ID, return directly. qid, _ = strconv.Atoi(parts[0]) - break + logrus.Debugf("get file attr: [%s], quota id: [%d]", dir, qid) + return uint32(qid) } } - logrus.Debugf("get file attr: [%s], quota id: [%d]", dir, qid) - - return uint32(qid) + logrus.Errorf("failed to get file attr of quota ID for dir %s", dir) + return 0 } -// SetFileAttr is used to set file attributes. -func (quota *PrjQuota) SetFileAttr(dir string, id uint32) error { - logrus.Debugf("set file attr, dir: %s, quotaID: %d", dir, id) +// SetQuotaIDInFileAttr sets file attributes of quota ID for the input directory. +// The input attributes is quota ID. +func (quota *PrjQuotaDriver) SetQuotaIDInFileAttr(dir string, quotaID uint32) error { + logrus.Debugf("set file attr, dir: %s, quotaID: %d", dir, quotaID) - strid := strconv.FormatUint(uint64(id), 10) + strid := strconv.FormatUint(uint64(quotaID), 10) _, _, _, err := exec.Run(0, "chattr", "-p", strid, "+P", dir) return err } -// SetFileAttrNoOutput is used to set file attributes without error. -func (quota *PrjQuota) SetFileAttrNoOutput(dir string, id uint32) { - strid := strconv.FormatUint(uint64(id), 10) +// SetQuotaIDInFileAttrNoOutput is used to set file attributes without error. +func (quota *PrjQuotaDriver) SetQuotaIDInFileAttrNoOutput(dir string, quotaID uint32) { + strid := strconv.FormatUint(uint64(quotaID), 10) exec.Run(0, "chattr", "-p", strid, "+P", dir) } -// load -// repquota -Pan -// Project used soft hard grace used soft hard grace -// ---------------------------------------------------------------------- -// #0 -- 220 0 0 25 0 0 -// #123 -- 4 0 88589934592 1 0 0 -// #8888 -- 8 0 0 2 0 0 -func (quota *PrjQuota) loadQuotaIds() (uint32, error) { - minID := QuotaMinID - _, output, _, err := exec.Run(0, "repquota", "-Pan") - if err != nil { - return minID, err - } - - lines := strings.Split(string(output), "\n") - for _, line := range lines { - if len(line) == 0 || line[0] != '#' { - continue - } - parts := strings.Split(line, " ") - if len(parts) == 0 { - continue - } - id, err := strconv.Atoi(parts[0][1:]) - uid := uint32(id) - if err == nil && uid > QuotaMinID { - quota.quotaIDs[uid] = 1 - if uid > minID { - minID = uid - } - } - } - logrus.Infof("Load repquota ids: %d, list: %v", len(quota.quotaIDs), quota.quotaIDs) - return minID, nil -} - -// GetNextQuatoID returns the next available quota id. -func (quota *PrjQuota) GetNextQuatoID() (uint32, error) { +// GetNextQuotaID returns the next available quota id. +func (quota *PrjQuotaDriver) GetNextQuotaID() (uint32, error) { quota.lock.Lock() defer quota.lock.Unlock() - if quota.quotaLastID == 0 { + if quota.lastID == 0 { var err error - quota.quotaLastID, err = quota.loadQuotaIds() + quota.quotaIDs, quota.lastID, err = loadQuotaIDs("-Pan") if err != nil { return 0, err } } - id := quota.quotaLastID + id := quota.lastID for { if id < QuotaMinID { id = QuotaMinID @@ -284,24 +280,25 @@ func (quota *PrjQuota) GetNextQuatoID() (uint32, error) { break } } - quota.quotaIDs[id] = 1 - quota.quotaLastID = id + quota.quotaIDs[id] = struct{}{} + quota.lastID = id logrus.Debugf("get next project quota id: %d", id) return id, nil } -func (quota *PrjQuota) setDevLimit(dir string, devID uint64) (uint64, error) { +// setDevLimit sets device storage upper limit in quota driver according to inpur dir. +func (quota *PrjQuotaDriver) setDevLimit(dir string, devID uint64) (uint64, error) { if limit, exist := quota.devLimits[devID]; exist { return limit, nil } + // get storage upper limit of the device which the dir is on. var stfs syscall.Statfs_t if err := syscall.Statfs(dir, &stfs); err != nil { logrus.Errorf("fail to get path %s limit: %v", dir, err) return 0, err } - limit := stfs.Blocks * uint64(stfs.Bsize) quota.lock.Lock() @@ -312,24 +309,26 @@ func (quota *PrjQuota) setDevLimit(dir string, devID uint64) (uint64, error) { return limit, nil } -func (quota *PrjQuota) checkDevLimit(dir string, size uint64) error { - devID, err := GetDevID(dir) +// checkDevLimit checks if the device on which the input dir lies has already been recorded in driver. +func (quota *PrjQuotaDriver) checkDevLimit(dir string, size uint64) error { + devID, err := system.GetDevID(dir) if err != nil { return err } limit, exist := quota.devLimits[devID] if !exist { + // if has not recorded, just add (dir, device, limit) to driver. if limit, err = quota.setDevLimit(dir, devID); err != nil { return err } } if limit < size { - return fmt.Errorf("dir %s quota limit should < %v, use exceed limit %v", dir, limit, size) + return fmt.Errorf("dir %s quota limit %v must be less than %v", dir, size, limit) } - logrus.Debugf("checkDevLimit dir %s quota limit %v B, size %v B", dir, limit, size) + logrus.Debugf("succeeded in checkDevLimit (dir %s quota limit %v B) with size %v B", dir, limit, size) return nil } diff --git a/storage/quota/quota.go b/storage/quota/quota.go index e238a66a2..8ca9f26ff 100644 --- a/storage/quota/quota.go +++ b/storage/quota/quota.go @@ -1,69 +1,97 @@ +// +build linux + package quota import ( - "bufio" "fmt" - "io" + "io/ioutil" "os" "path/filepath" + "strconv" "strings" - "syscall" + "github.com/alibaba/pouch/pkg/exec" "github.com/alibaba/pouch/pkg/kernel" - "github.com/sirupsen/logrus" ) const ( // QuotaMinID represents the minimize quota id. + // The value is unit32(2^24). + // 这个需要问苦志? QuotaMinID = uint32(16777216) + + // procMountFile represent the mounts file in proc virtual file system. + procMountFile = "/proc/mounts" ) +var hasQuota bool + var ( - // UseQuota represents use quota or not. - UseQuota = true - // Gquota represents global quota. - Gquota = NewQuota("") + // GQuotaDriver represents global quota driver. + GQuotaDriver = NewQuotaDriver("") ) // BaseQuota defines the quota operation interface. +// It abstracts the common operation ways a quota driver should implement. type BaseQuota interface { - StartQuotaDriver(dir string) (string, error) + // EnforceQuota is used to enforce disk quota effect on specified directory. + EnforceQuota(dir string) (string, error) + + // SetSubtree sets quota for container root dir which is a subtree of host's dir mapped on a device. SetSubtree(dir string, qid uint32) (uint32, error) + + // SetDiskQuota uses the following two parameters to set disk quota for a directory. + // * quota size: a byte size of requested quota. + // * quota ID: an ID represent quota attr which is used in the global scope. SetDiskQuota(dir string, size string, quotaID uint32) error + + // CheckMountpoint is used to check mount point. + // It returns mointpoint, enable quota and filesystem type of the device. CheckMountpoint(devID uint64) (string, bool, string) - GetFileAttr(dir string) uint32 - SetFileAttr(dir string, id uint32) error - SetFileAttrNoOutput(dir string, id uint32) - GetNextQuatoID() (uint32, error) + + // GetQuotaIDInFileAttr gets attributes of the file which is in the inode. + // The returned result is quota ID. + GetQuotaIDInFileAttr(dir string) uint32 + + // SetQuotaIDInFileAttr sets file attributes of quota ID for the input directory. + // The input attributes is quota ID. + SetQuotaIDInFileAttr(dir string, quotaID uint32) error + + // SetQuotaIDInFileAttrNoOutput sets file attributes of quota ID for the input directory without returning error if exists. + // The input attributes is quota ID. + SetQuotaIDInFileAttrNoOutput(dir string, quotaID uint32) + + // GetNextQuotaID gets next quota ID in global scope of host. + GetNextQuotaID() (uint32, error) } -// NewQuota returns a quota instance. -func NewQuota(name string) BaseQuota { +// NewQuotaDriver returns a quota instance. +func NewQuotaDriver(name string) BaseQuota { var quota BaseQuota switch name { case "grpquota": - quota = &GrpQuota{ - quotaIDs: make(map[uint32]uint32), + quota = &GrpQuotaDriver{ + quotaIDs: make(map[uint32]struct{}), mountPoints: make(map[uint64]string), } case "prjquota": - quota = &PrjQuota{ - quotaIDs: make(map[uint32]uint32), + quota = &PrjQuotaDriver{ + quotaIDs: make(map[uint32]struct{}), mountPoints: make(map[uint64]string), devLimits: make(map[uint64]uint64), } default: kernelVersion, err := kernel.GetKernelVersion() if err == nil && kernelVersion.Kernel >= 4 { - quota = &PrjQuota{ - quotaIDs: make(map[uint32]uint32), + quota = &PrjQuotaDriver{ + quotaIDs: make(map[uint32]struct{}), mountPoints: make(map[uint64]string), devLimits: make(map[uint64]uint64), } } else { - quota = &GrpQuota{ - quotaIDs: make(map[uint32]uint32), + quota = &GrpQuotaDriver{ + quotaIDs: make(map[uint32]struct{}), mountPoints: make(map[uint64]string), } } @@ -74,57 +102,47 @@ func NewQuota(name string) BaseQuota { // SetQuotaDriver is used to set global quota driver. func SetQuotaDriver(name string) { - Gquota = NewQuota(name) -} - -// GetDevID returns device id. -func GetDevID(dir string) (uint64, error) { - var st syscall.Stat_t - if err := syscall.Stat(dir, &st); err != nil { - logrus.Warnf("getDirDev: %s, %v", dir, err) - return 0, err - } - return st.Dev, nil + GQuotaDriver = NewQuotaDriver(name) } // StartQuotaDriver is used to start quota driver. func StartQuotaDriver(dir string) (string, error) { - return Gquota.StartQuotaDriver(dir) + return GQuotaDriver.EnforceQuota(dir) } // SetSubtree is used to set quota id for directory. func SetSubtree(dir string, qid uint32) (uint32, error) { - return Gquota.SetSubtree(dir, qid) + return GQuotaDriver.SetSubtree(dir, qid) } // SetDiskQuota is used to set quota for directory. func SetDiskQuota(dir string, size string, quotaID uint32) error { - return Gquota.SetDiskQuota(dir, size, quotaID) + return GQuotaDriver.SetDiskQuota(dir, size, quotaID) } // CheckMountpoint is used to check mount point. func CheckMountpoint(devID uint64) (string, bool, string) { - return Gquota.CheckMountpoint(devID) + return GQuotaDriver.CheckMountpoint(devID) } -// GetFileAttr returns the directory attributes. -func GetFileAttr(dir string) uint32 { - return Gquota.GetFileAttr(dir) +// GetQuotaIDInFileAttr returns the directory attributes of quota ID. +func GetQuotaIDInFileAttr(dir string) uint32 { + return GQuotaDriver.GetQuotaIDInFileAttr(dir) } -// SetFileAttr is used to set file attributes. -func SetFileAttr(dir string, id uint32) error { - return Gquota.SetFileAttr(dir, id) +// SetQuotaIDInFileAttr is used to set file attributes of quota ID. +func SetQuotaIDInFileAttr(dir string, id uint32) error { + return GQuotaDriver.SetQuotaIDInFileAttr(dir, id) } -// SetFileAttrNoOutput is used to set file attributes without error. -func SetFileAttrNoOutput(dir string, id uint32) { - Gquota.SetFileAttrNoOutput(dir, id) +// SetQuotaIDInFileAttrNoOutput is used to set file attribute of quota ID without error. +func SetQuotaIDInFileAttrNoOutput(dir string, quotaID uint32) { + GQuotaDriver.SetQuotaIDInFileAttrNoOutput(dir, quotaID) } -//GetNextQuatoID returns the next available quota id. -func GetNextQuatoID() (uint32, error) { - return Gquota.GetNextQuatoID() +//GetNextQuotaID returns the next available quota id. +func GetNextQuotaID() (uint32, error) { + return GQuotaDriver.GetNextQuotaID() } //GetDefaultQuota returns the default quota size. @@ -133,11 +151,13 @@ func GetDefaultQuota(quotas map[string]string) string { return "" } + // "/" means the disk quota only takes effect on rootfs + 0 * volume quota, ok := quotas["/"] if ok && quota != "" { return quota } + // ".*" means the disk quota only takes effect on rootfs + n * volume quota, ok = quotas[".*"] if ok && quota != "" { return quota @@ -148,16 +168,12 @@ func GetDefaultQuota(quotas map[string]string) string { // SetRootfsDiskQuota is to set container rootfs dir disk quota. func SetRootfsDiskQuota(basefs, size string, quotaID uint32) error { - overlayfs, err := getOverlay(basefs) + overlayMountInfo, err := getOverlayMountInfo(basefs) if err != nil { - return fmt.Errorf("failed to get lowerdir: %v", err) - } - if overlayfs == nil || (*overlayfs) == (OverlayMount{}) || - overlayfs.Upper == "" || overlayfs.Work == "" { - return fmt.Errorf("failed to get overlay mount for %s: %+v", basefs, overlayfs) + return fmt.Errorf("failed to get overlay mount info: %v", err) } - for _, dir := range []string{overlayfs.Upper, overlayfs.Work} { + for _, dir := range []string{overlayMountInfo.Upper, overlayMountInfo.Work} { _, err = StartQuotaDriver(dir) if err != nil { return fmt.Errorf("failed to start quota driver: %v", err) @@ -180,35 +196,31 @@ func SetRootfsDiskQuota(basefs, size string, quotaID uint32) error { return nil } -func setQuotaForDir(src string, qid uint32) error { +// setQuotaForDir sets file attribute +func setQuotaForDir(src string, quotaID uint32) error { filepath.Walk(src, func(path string, fd os.FileInfo, err error) error { if err != nil { return fmt.Errorf("setQuota walk dir %s get error %v", path, err) } - SetFileAttrNoOutput(path, qid) + SetQuotaIDInFileAttrNoOutput(path, quotaID) return nil }) return nil } -func getOverlay(basefs string) (*OverlayMount, error) { - overlayfs := &OverlayMount{} - - fd, err := os.Open("/proc/mounts") +// getOverlayMountInfo gets overlayFS informantion from /proc/mounts. +// upperdir, mergeddir and workdir would be dealt. +func getOverlayMountInfo(basefs string) (*OverlayMount, error) { + output, err := ioutil.ReadFile(procMountFile) if err != nil { + logrus.Warnf("failed to ReadFile %s: %v", procMountFile, err) return nil, err } - defer fd.Close() - - br := bufio.NewReader(fd) - for { - line, _, c := br.ReadLine() - if c == io.EOF { - break - } + var lowerDir, upperDir, workDir string + for _, line := range strings.Split(string(output), "\n") { parts := strings.Split(string(line), " ") if len(parts) != 6 { continue @@ -216,28 +228,94 @@ func getOverlay(basefs string) (*OverlayMount, error) { if parts[1] != basefs || parts[2] != "overlay" { continue } - + // the expected format is like following: + // overlay /var/lib/pouch/containerd/state/io.containerd.runtime.v1.linux/default/8d849ee68c8698531a2575f890be027dbd4dcb64f39cce37d7d22a703cbb362b/rootfs overlay rw,relatime,lowerdir=/var/lib/pouch/containerd/root/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs,upperdir=/var/lib/pouch/containerd/root/io.containerd.snapshotter.v1.overlayfs/snapshots/274/fs,workdir=/var/lib/pouch/containerd/root/io.containerd.snapshotter.v1.overlayfs/snapshots/274/work 0 0 + // In part[3], it stored lowerdir, upperdir and workdir. mountParams := strings.Split(parts[3], ",") for _, p := range mountParams { switch { case strings.Contains(p, "lowerdir"): if s := strings.Split(p, "="); len(s) == 2 { - overlayfs.Lower = s[1] + lowerDir = s[1] } - case strings.Contains(p, "upperdir"): if s := strings.Split(p, "="); len(s) == 2 { - overlayfs.Upper = s[1] + upperDir = s[1] } - case strings.Contains(p, "workdir"): if s := strings.Split(p, "="); len(s) == 2 { - overlayfs.Work = s[1] + workDir = s[1] break } } } } - return overlayfs, nil + if lowerDir == "" || upperDir == "" || workDir == "" { + return nil, fmt.Errorf("failed to get OverlayFs Mount Info: lowerdir, upperdir, workdir must be non-empty") + } + + return &OverlayMount{ + Lower: lowerDir, + Upper: upperDir, + Work: workDir, + }, nil +} + +// loadQuotaIDs loads quota IDs for quota driver from reqquota execution result. +// This function utils `repquota` which summarizes quotas for a filesystem. +// see http://man7.org/linux/man-pages/man8/repquota.8.html +// +// $ repquota -Pan +// Project used soft hard grace used soft hard grace +// ---------------------------------------------------------------------- +// #0 -- 220 0 0 25 0 0 +// #123 -- 4 0 88589934592 1 0 0 +// #8888 -- 8 0 0 2 0 0 +// +// Or +// +// $ repquota -gan +// Group used soft hard grace used soft hard grace +// ---------------------------------------------------------------------- +// #0 -- 494472 0 0 938 0 0 +// #54 -- 8 0 0 2 0 0 +// #4 -- 16 0 0 4 0 0 +// #22 -- 28 0 0 4 0 0 +// #16777220 +- 2048576 0 2048575 9 0 0 +// #500 -- 47504 0 0 101 0 0 +// #16777221 -- 3048576 0 3048576 8 0 0 +func loadQuotaIDs(repquotaOpt string) (map[uint32]struct{}, uint32, error) { + quotaIDs := make(map[uint32]struct{}, 0) + + minID := QuotaMinID + _, output, _, err := exec.Run(0, "repquota", repquotaOpt) + if err != nil { + return nil, minID, err + } + + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if len(line) == 0 || line[0] != '#' { + continue + } + // find all lines with prefix '#' + parts := strings.Split(line, " ") + // part[0] is "#123456" + if len(parts[0]) <= 1 { + continue + } + + id, err := strconv.Atoi(parts[0][1:]) + quotaID := uint32(id) + // 这里需要问苦志, minID is max ID in quotaIDs? + if err == nil && quotaID > QuotaMinID { + quotaIDs[quotaID] = struct{}{} + if quotaID > minID { + minID = quotaID + } + } + } + logrus.Infof("Load repquota ids: %d, list: %v", len(quotaIDs), quotaIDs) + return quotaIDs, minID, nil } diff --git a/storage/quota/quota_test.go b/storage/quota/quota_test.go new file mode 100644 index 000000000..4e40d90db --- /dev/null +++ b/storage/quota/quota_test.go @@ -0,0 +1,61 @@ +// +build linux + +package quota + +import "testing" + +func TestGetDefaultQuota(t *testing.T) { + type args struct { + quotas map[string]string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "normal case with supposed data root /", + args: args{ + quotas: map[string]string{ + "/": "1000kb", + }, + }, + want: "1000kb", + }, + { + name: "normal case with supposed data .*", + args: args{ + quotas: map[string]string{ + ".*": "2000kb", + }, + }, + want: "2000kb", + }, + { + name: "normal case with supposed data .* and /", + args: args{ + quotas: map[string]string{ + ".*": "2000kb", + "/": "1000kb", + }, + }, + want: "1000kb", + }, + { + name: "normal case with no supposed data", + args: args{ + quotas: map[string]string{ + "asdfghj": "2000kb", + }, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetDefaultQuota(tt.args.quotas); got != tt.want { + t.Errorf("GetDefaultQuota() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/storage/quota/set_diskquota.go b/storage/quota/set_diskquota.go index 0d5ac2d0b..dc839ba34 100644 --- a/storage/quota/set_diskquota.go +++ b/storage/quota/set_diskquota.go @@ -1,3 +1,5 @@ +// +build linux + package quota import (