Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement filesystem quota for overlay2 over ext4 if it support #34364

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions daemon/daemon_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,25 @@ func verifyPlatformContainerSettings(daemon *Daemon, hostConfig *containertypes.
}
}

if hostConfig.StorageOpt != nil && daemon.GraphDriverName(runtime.GOOS) == "overlay2" {
_, exist := hostConfig.StorageOpt["size"]
if exist {
status := daemon.stores[runtime.GOOS].layerStore.DriverStatus()
if status[0][0] == "Backing Filesystem" && status[0][1] != "xfs" {
if hostConfig.Privileged {
warnings = append(warnings, fmt.Sprintf("filesystem quota for overlay2 over %s can't take affect with privileged container", status[0][1]))
} else {
for _, cap := range hostConfig.CapAdd {
if cap == "SYS_RESOURCE" {
warnings = append(warnings, fmt.Sprintf("filesystem quota for overlay2 over %s can't take affect with CAP_SYS_RESOURCE", status[0][1]))
break
}
}
}
}
}
}

return warnings, nil
}

Expand Down
15 changes: 5 additions & 10 deletions daemon/graphdriver/overlay2/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,16 +199,11 @@ func Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (grap

d.naiveDiff = graphdriver.NewNaiveDiffDriver(d, uidMaps, gidMaps)

if backingFs == "xfs" {
// Try to enable project quota support over xfs.
if d.quotaCtl, err = quota.NewControl(home); err == nil {
projectQuotaSupported = true
} else if opts.quota.Size > 0 {
return nil, fmt.Errorf("Storage option overlay2.size not supported. Filesystem does not support Project Quota: %v", err)
}
// Try to enable project quota.
if d.quotaCtl, err = quota.NewControl(home, backingFs); err == nil {
projectQuotaSupported = true
} else if opts.quota.Size > 0 {
// if xfs is not the backing fs then error out if the storage-opt overlay2.size is used.
return nil, fmt.Errorf("Storage Option overlay2.size only supported for backingFS XFS. Found %v", backingFs)
return nil, fmt.Errorf("Storage option overlay2.size not supported. Filesystem %s does not support Project Quota: %v", backingFs, err)
}

logrus.Debugf("backingFs=%s, projectQuotaSupported=%v", backingFs, projectQuotaSupported)
Expand Down Expand Up @@ -324,7 +319,7 @@ func (d *Driver) Cleanup() error {
// file system.
func (d *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts) error {
if opts != nil && len(opts.StorageOpt) != 0 && !projectQuotaSupported {
return fmt.Errorf("--storage-opt is supported only for overlay over xfs with 'pquota' mount option")
return fmt.Errorf("--storage-opt is not supported for overlay on your system, make sure your backingFs %s support quota or mounted with 'prjquota' option", backingFs)
}

if opts == nil {
Expand Down
177 changes: 152 additions & 25 deletions daemon/graphdriver/quota/projectquota.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
// +build linux

//
// projectquota.go - implements XFS project quota controls
// projectquota.go - implements project quota controls
// for setting quota limits on a newly created directory.
// It currently supports the legacy XFS specific ioctls.
//
// TODO: use generic quota control ioctl FS_IOC_FS{GET,SET}XATTR
// for both xfs/ext4 for kernel version >= v4.5
//

package quota
Expand Down Expand Up @@ -47,13 +43,38 @@ struct fsxattr {
#ifndef Q_XGETPQUOTA
#define Q_XGETPQUOTA QCMD(Q_XGETQUOTA, PRJQUOTA)
#endif

#ifndef Q_XGETPQSTAT
#define Q_XGETPQSTAT QCMD(Q_XGETQSTAT, PRJQUOTA)
#endif

#ifndef Q_SETPQUOTA
#define Q_SETPQUOTA (unsigned int)QCMD(Q_SETQUOTA, PRJQUOTA)
#endif

#ifndef Q_GETPQUOTA
#define Q_GETPQUOTA (unsigned int)QCMD(Q_GETQUOTA, PRJQUOTA)
#endif

#define PDQ_ACCT_BIT 4
#define PDQ_ENFD_BIT 5

#ifndef QUOTA_PDQ_ACCT
#define QUOTA_PDQ_ACCT (1<<PDQ_ACCT_BIT)
#endif
#ifndef QUOTA_PDQ_ENFD
#define QUOTA_PDQ_ENFD (1<<PDQ_ENFD_BIT)
#endif


*/
import "C"
import (
"fmt"
"io/ioutil"
"path"
"path/filepath"
"sync"
"unsafe"

"github.com/Sirupsen/logrus"
Expand All @@ -71,6 +92,17 @@ type Control struct {
backingFsBlockDev string
nextProjectID uint32
quotas map[string]uint32
quotaOps QuotafileOps
lock sync.Mutex
}

// QuotafileOps is a interface for quotafile operations
type QuotafileOps interface {
// SetProjectQuota sets the project quota for project id on block device
SetProjectQuota(dev string, projectID uint32, quota Quota) error

// GetProjectQuota gets the project quota for project id on block device
GetProjectQuota(dev string, projectID uint32, quota *Quota) error
}

// NewControl - initialize project quota support.
Expand All @@ -95,7 +127,7 @@ type Control struct {
// on it. If that works, continue to scan existing containers to map allocated
// project ids.
//
func NewControl(basePath string) (*Control, error) {
func NewControl(basePath string, fs string) (*Control, error) {
//
// Get project id of parent dir as minimal id to be used by driver
//
Expand All @@ -113,21 +145,41 @@ func NewControl(basePath string) (*Control, error) {
return nil, err
}

//
// Get the qutoa stat to check whether the system support project quota
//
stat, err := getQuotaStat(backingFsBlockDev)
if err != nil || stat != 2 {
if err != nil {
logrus.Debugf("Get qutoa stat failed with: %v", err)
}
return nil, fmt.Errorf("quota isn't support on your system")
}

var quotaOps QuotafileOps

if fs == "xfs" {
quotaOps = new(XfsQuota)
} else {
quotaOps = new(VfsQuota)
}

//
// Test if filesystem supports project quotas by trying to set
// a quota on the first available project id
//
quota := Quota{
Size: 0,
}
if err := setProjectQuota(backingFsBlockDev, minProjectID, quota); err != nil {
if err := quotaOps.SetProjectQuota(backingFsBlockDev, minProjectID, quota); err != nil {
return nil, err
}

q := Control{
backingFsBlockDev: backingFsBlockDev,
nextProjectID: minProjectID + 1,
quotas: make(map[string]uint32),
quotaOps: quotaOps,
}

//
Expand All @@ -145,7 +197,7 @@ func NewControl(basePath string) (*Control, error) {
// SetQuota - assign a unique project id to directory and set the quota limits
// for that project id
func (q *Control) SetQuota(targetPath string, quota Quota) error {

q.lock.Lock()
projectID, ok := q.quotas[targetPath]
if !ok {
projectID = q.nextProjectID
Expand All @@ -155,22 +207,27 @@ func (q *Control) SetQuota(targetPath string, quota Quota) error {
//
err := setProjectID(targetPath, projectID)
if err != nil {
q.lock.Unlock()
return err
}

q.quotas[targetPath] = projectID
q.nextProjectID++
}

q.lock.Unlock()
//
// set the quota limit for the container's project id
//
logrus.Debugf("SetQuota(%s, %d): projectID=%d", targetPath, quota.Size, projectID)
return setProjectQuota(q.backingFsBlockDev, projectID, quota)
return q.quotaOps.SetProjectQuota(q.backingFsBlockDev, projectID, quota)
}

// setProjectQuota - set the quota for project id on xfs block device
func setProjectQuota(backingFsBlockDev string, projectID uint32, quota Quota) error {
// XfsQuota is a struct implements quota operations
type XfsQuota struct {
}

// SetProjectQuota - set the quota for project id on xfs block device
func (q *XfsQuota) SetProjectQuota(backingFsBlockDev string, projectID uint32, quota Quota) error {
var d C.fs_disk_quota_t
d.d_version = C.FS_DQUOT_VERSION
d.d_id = C.__u32(projectID)
Expand All @@ -194,34 +251,104 @@ func setProjectQuota(backingFsBlockDev string, projectID uint32, quota Quota) er
return nil
}

// GetQuota - get the quota limits of a directory that was configured with SetQuota
func (q *Control) GetQuota(targetPath string, quota *Quota) error {

projectID, ok := q.quotas[targetPath]
if !ok {
return fmt.Errorf("quota not found for path : %s", targetPath)
}

//
// get the quota limit for the container's project id
//
// GetProjectQuota gets the project quota for projectID on xfs block device
func (q *XfsQuota) GetProjectQuota(backingFsBlockDev string, projectID uint32, quota *Quota) error {
var d C.fs_disk_quota_t

var cs = C.CString(q.backingFsBlockDev)
var cs = C.CString(backingFsBlockDev)
defer C.free(unsafe.Pointer(cs))

_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XGETPQUOTA,
uintptr(unsafe.Pointer(cs)), uintptr(C.__u32(projectID)),
uintptr(unsafe.Pointer(&d)), 0, 0)
if errno != 0 {
return fmt.Errorf("Failed to get quota limit for projid %d on %s: %v",
projectID, q.backingFsBlockDev, errno.Error())
projectID, backingFsBlockDev, errno.Error())
}
quota.Size = uint64(d.d_blk_hardlimit) * 512

return nil
}

// VfsQuota is a struct implements quota operations
type VfsQuota struct {
}

// SetProjectQuota - set the quota for project id on block device
func (q *VfsQuota) SetProjectQuota(backingFsBlockDev string, projectID uint32, quota Quota) error {
var d C.struct_if_dqblk
d.dqb_bhardlimit = C.__u64(quota.Size / 1024)
d.dqb_bsoftlimit = d.dqb_bhardlimit
d.dqb_valid = C.QIF_LIMITS

var cs = C.CString(backingFsBlockDev)
defer C.free(unsafe.Pointer(cs))

_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_SETPQUOTA,
uintptr(unsafe.Pointer(cs)), uintptr(C.__u32(projectID)),
uintptr(unsafe.Pointer(&d)), 0, 0)
if errno != 0 {
return fmt.Errorf("Failed to set quota limit for projid %d on %s: %v",
projectID, backingFsBlockDev, errno.Error())
}

return nil
}

// GetProjectQuota - get the quota for project id on block device
func (q *VfsQuota) GetProjectQuota(backingFsBlockDev string, projectID uint32, quota *Quota) error {
var d C.struct_if_dqblk
d.dqb_valid = C.QIF_USAGE

var cs = C.CString(backingFsBlockDev)
defer C.free(unsafe.Pointer(cs))

_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_GETPQUOTA,
uintptr(unsafe.Pointer(cs)), uintptr(C.__u32(projectID)),
uintptr(unsafe.Pointer(&d)), 0, 0)
if errno != 0 {
return fmt.Errorf("Failed to get quota limit for projid %d on %s: %v",
projectID, backingFsBlockDev, errno.Error())
}

quota.Size = uint64(d.dqb_bhardlimit) * 1024

return nil
}

// getQuotaStat - get the quota stat
// return 2 means quota is on
func getQuotaStat(backingFsBlockDev string) (int, error) {
var info C.fs_quota_stat_t

var cs = C.CString(backingFsBlockDev)
defer C.free(unsafe.Pointer(cs))
_, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XGETPQSTAT,
uintptr(unsafe.Pointer(cs)), 0,
uintptr(unsafe.Pointer(&info)), 0, 0)
if errno != 0 {
return -1, fmt.Errorf("Failed to get quota stat on %s: %v",
backingFsBlockDev, errno.Error())
}

return int((info.qs_flags&C.QUOTA_PDQ_ACCT)>>C.PDQ_ACCT_BIT + (info.qs_flags&C.QUOTA_PDQ_ENFD)>>C.PDQ_ENFD_BIT), nil
}

// GetQuota - get the quota limits of a directory that was configured with SetQuota
func (q *Control) GetQuota(targetPath string, quota *Quota) error {
q.lock.Lock()
projectID, ok := q.quotas[targetPath]
q.lock.Unlock()
if !ok {
return fmt.Errorf("quota not found for path : %s", targetPath)
}

//
// get the quota limit for the container's project id
//
return q.quotaOps.GetProjectQuota(q.backingFsBlockDev, projectID, quota)
}

// getProjectID - get the project id of path on xfs
func getProjectID(targetPath string) (uint32, error) {
dir, err := openDir(targetPath)
Expand Down