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

Adds quota interface to allow multiple quota-enabled filesystems #22

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test:

fmt:
go fmt ./...
find ./xfs -name "*.c" -o -name "*.h" | \
find ./quota -name "*.c" -o -name "*.h" | \
xargs clang-format -style=file -i


Expand Down
31 changes: 23 additions & 8 deletions manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import (
"path/filepath"
"regexp"

"github.com/cirocosta/xfsvol/xfs"
"github.com/cirocosta/xfsvol/quota"
"github.com/cirocosta/xfsvol/quota/xfs"
"github.com/pkg/errors"
)

Expand All @@ -17,25 +18,34 @@ var (
NameRegex = regexp.MustCompile(`^[a-zA-Z0-9][\w\-]{1,250}$`)

ErrInvalidName = errors.Errorf("Invalid name")
ErrEmptyQuota = errors.Errorf("Invalid quota - Can't be 0")
ErrEmptyINode = errors.Errorf("Invalid inode - Can't be 0")
ErrEmptyQuota = errors.Errorf("Invalid quota")
ErrEmptyINode = errors.Errorf("Invalid inode")
ErrNotFound = errors.Errorf("Volume not found")
)

// Manager is the entity responsible for managing
// volumes under a given base path. It takes the
// responsability of 'CRUD'ing these volumes.
type Manager struct {
quotaCtl *xfs.Control
quotaCtl quota.Control
root string
}

type FilesystemType uint8

const (
UnknownFilesystemType FilesystemType = iota
Ext4FilesystemType
XfsFilesystemType
)

// Config represents the configuration to
// create a manager. It takes a root directory
// that is used to create a block device that
// controls project quotas bellow it.
type Config struct {
Root string
Type FilesystemType
}

// Volume represents a volume under a given
Expand Down Expand Up @@ -64,12 +74,17 @@ func New(cfg Config) (manager Manager, err error) {
return
}

if cfg.Type == Ext4FilesystemType {
err = errors.Errorf("fs type ext4 is not implemented yet")
return
}

quotaCtl, err := xfs.NewControl(xfs.ControlConfig{
BasePath: cfg.Root,
})
if err != nil {
err = errors.Wrapf(err,
"Couldn't initialize XFS quota control on root path %s",
"Couldn't initialize quota control on root path %s",
cfg.Root)
return
}
Expand All @@ -92,7 +107,7 @@ func (m Manager) List() (vols []Volume, err error) {

for _, file := range files {
if file.IsDir() {
var quota *xfs.Quota
var quota *quota.Quota
absPath := filepath.Join(m.root, file.Name())

quota, err = m.quotaCtl.GetQuota(absPath)
Expand Down Expand Up @@ -139,7 +154,7 @@ func (m Manager) Get(name string) (vol Volume, found bool, err error) {
for _, file := range files {
if file.IsDir() && file.Name() == name {
found = true
var quota *xfs.Quota
var quota *quota.Quota
absPath = filepath.Join(m.root, name)

quota, err = m.quotaCtl.GetQuota(absPath)
Expand Down Expand Up @@ -182,7 +197,7 @@ func (m Manager) Create(vol Volume) (absPath string, err error) {
return
}

err = m.quotaCtl.SetQuota(absPath, xfs.Quota{
err = m.quotaCtl.SetQuota(absPath, quota.Quota{
Size: vol.Size,
INode: vol.INode,
})
Expand Down
6 changes: 6 additions & 0 deletions quota/control.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package quota

type Control interface {
GetQuota(targetPath string) (q *Quota, err error)
SetQuota(targetPath string, quota Quota) (err error)
}
186 changes: 186 additions & 0 deletions quota/control_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package quota_test

import (
"github.com/pkg/errors"
"io/ioutil"
"os"
"path"
"testing"

"github.com/cirocosta/xfsvol/quota"
"github.com/cirocosta/xfsvol/quota/xfs"
"github.com/stretchr/testify/assert"

utils "github.com/cirocosta/xfsvol/test_utils"
)

var (
controlTypesMounts = map[string]string{
"xfs": "/mnt/xfs/tmp",
"ext4": "/mnt/ext4/tmp",
}
errNotImplemented = errors.Errorf("control type not implemented")
)

// createControl is a factory that takes a controlType (either
// `xfs` or `ext4`) and builds a Control implementor based on
// the type of configuration passed.
func createControl(controlType, root string, arg ...interface{}) (control Control, err error) {
switch controlType {
case "xfs":
control, err = xfs.NewControl(xfs.ControlConfig{
Dir: root,
StartingProjectId: arg[0].(*uint32),
})
return
case "ext4":
err = errNotImplemented
return
default:
err = errors.Errorf("unknown control type %s",
controlType)
return
}

return
}

func TestControlImplementations(t *testing.T) {
var (
dir string
err error
ctl quota.Control
)

for controlType, controlMount := range controlTypesMounts {
t.Run(controlType+" succeeds if properly mounted directory", func(t *testing.T) {
dir, err = ioutil.TempDir(controlMount, "")
assert.NoError(t, err)
defer os.RemoveAll(dir)

_, err = createControl(controlType, dir)
assert.NoError(t, err)
})

t.Run(controlType+" quota assignment fails if outside tree", func(t *testing.T) {
dirOutside, err := ioutil.TempDir("", "")
assert.NoError(t, err)
defer os.RemoveAll(dirOutside)

dir, err = ioutil.TempDir(controlMount, "")
assert.NoError(t, err)
defer os.RemoveAll(dir)

ctl, err = createControl(controlType, dir)
assert.NoError(t, err)

err = ctl.SetQuota(dirOutside, quota.Quota{
Size: 10 * (1 << 20),
})
assert.Error(t, err)
})

t.Run(controlType+" fails to assign to inexistent dir in tree", func(t *testing.T) {
dir, err = ioutil.TempDir(controlMount, "")
assert.NoError(t, err)
defer os.RemoveAll(dir)

dirInside := path.Join(dir, "abc")

ctl, err = createControl(controlType, dir)
assert.NoError(t, err)

err = ctl.SetQuota(dirInside, quota.Quota{
Size: 10 * (1 << 20),
})
assert.Error(t, err)
})

t.Run(controlType+" succeeds to assign to dir in tree", func(t *testing.T) {
dir, err = ioutil.TempDir(controlMount, "")
assert.NoError(t, err)
defer os.RemoveAll(dir)

dirInside := path.Join(dir, "abc")
err = os.MkdirAll(dirInside, 0755)
assert.NoError(t, err)

ctl, err = createControl(controlType, dir)
assert.NoError(t, err)

err = ctl.SetQuota(dirInside, quota.Quota{
Size: 10 * (1 << 20),
})
assert.NoError(t, err)

})

t.Run(controlType+" flatly enforces disk quota", func(t *testing.T) {
dir, err = ioutil.TempDir(controlMount, "")
assert.NoError(t, err)
defer os.RemoveAll(dir)

dir1M := path.Join(dir, "1M")
dir2M := path.Join(dir, "2M")

assert.NoError(t, os.MkdirAll(dir1M, 0755))
assert.NoError(t, os.MkdirAll(dir2M, 0755))

var startingProjectId uint32 = 100
ctl, err := quota.NewControl(quota.ControlConfig{
BasePath: dir,
StartingProjectId: &startingProjectId,
})
assert.NoError(t, err)

assert.NoError(t, ctl.SetQuota(dir1M, quota.Quota{
Size: 1 * (1 << 20),
}))

assert.NoError(t, ctl.SetQuota(dir2M, quota.Quota{
Size: 1 * (2 << 20),
}))

fileDir1M, err := os.Create(path.Join(dir1M, "file"))
assert.NoError(t, err)

fileDir2M, err := os.Create(path.Join(dir2M, "file"))
assert.NoError(t, err)

assert.Error(t, utils.WriteBytes(fileDir1M, 'c', 2*(1<<20)))
assert.NoError(t, utils.WriteBytes(fileDir2M, 'c', 1*(1<<20)))

})

t.Run(controlType+" flatly enforces inode quota", func(t *testing.T) {
dir, err := ioutil.TempDir(xfsMount, "aa")
assert.NoError(t, err)
defer os.RemoveAll(dir)

dirA := path.Join(dir, "A")
dirB := path.Join(dir, "B")

assert.NoError(t, os.MkdirAll(dirA, 0755))
assert.NoError(t, os.MkdirAll(dirB, 0755))

ctl, err := quota.NewControl(quota.ControlConfig{
BasePath: dir,
})
assert.NoError(t, err)

assert.NoError(t, ctl.SetQuota(dirA, quota.Quota{
Size: 2 * (1 << 20),
INode: 30,
}))

assert.NoError(t, ctl.SetQuota(dirB, quota.Quota{
Size: 2 * (1 << 20),
INode: 300,
}))

assert.Error(t, utils.CreateFiles(dirA, 100))
assert.NoError(t, utils.CreateFiles(dirB, 100))

})
}
}
3 changes: 3 additions & 0 deletions quota/ext4/ext4.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// +build linux

package ext4
21 changes: 21 additions & 0 deletions quota/quota.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package quota

// Quota defines the limit params to be applied or that
// are already set to a project.
//
// Fields named `Used` are meant to `Get` operations only,
// to display how much of the quotah as been used so far.
type Quota struct {
// Size represents total size that can be commited
// to a tree of directories under this quota.
Size uint64

// INode tells the maximum number of INodes that can be created;
INode uint64

// UsedSize is the disk size that has been used under quota;
UsedSize uint64

// UsedINode is the number of INodes that used so far.
UsedInode uint64
}
17 changes: 17 additions & 0 deletions quota/quota.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#ifndef __QUOTA_H
#define __QUOTA_H

#include <linux/types.h>

/**
* Provides the configuration to be used when
* invoking quota getter and setter commands.
*/
typedef struct quota {
__u64 size;
__u64 inodes;
__u64 used_size;
__u64 used_inodes;
} quota_t;

#endif
File renamed without changes.
Loading