Introduce fsquota package
This introduces a new fsquota package and
a few low-level support packages to simplify the
management of filesystem quotas.
To expose an API that's nice to use while staying
performant and safe the new fsinfo syscall is being
used. Since that syscall is not yet in mainline it has
been backported to our 5.6 kernel.
Test Plan:
Manually validated on our kernel, automated
tests are pending some Bazel work to be able to run them
inside our own kernel.
X-Origin-Diff: phab/D462
GitOrigin-RevId: bb463056589d2b13b7cf32d48ab0b884e70b1bad
diff --git a/core/pkg/fsquota/quotactl/BUILD.bazel b/core/pkg/fsquota/quotactl/BUILD.bazel
new file mode 100644
index 0000000..de5d085
--- /dev/null
+++ b/core/pkg/fsquota/quotactl/BUILD.bazel
@@ -0,0 +1,9 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["quotactl.go"],
+ importpath = "git.monogon.dev/source/nexantic.git/core/pkg/fsquota/quotactl",
+ visibility = ["//visibility:public"],
+ deps = ["@org_golang_x_sys//unix:go_default_library"],
+)
diff --git a/core/pkg/fsquota/quotactl/quotactl.go b/core/pkg/fsquota/quotactl/quotactl.go
new file mode 100644
index 0000000..5ed77d7
--- /dev/null
+++ b/core/pkg/fsquota/quotactl/quotactl.go
@@ -0,0 +1,233 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package quotactl implements a low-level wrapper around the modern portion of Linux's
+// quotactl() syscall. See the fsquota package for a nicer interface to the most common part
+// of this API.
+package quotactl
+
+import (
+ "fmt"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+)
+
+type QuotaType uint
+
+const (
+ QuotaTypeUser QuotaType = iota
+ QuotaTypeGroup
+ QuotaTypeProject
+)
+
+const (
+ Q_SYNC uint = ((0x800001 + iota) << 8)
+ Q_QUOTAON
+ Q_QUOTAOFF
+ Q_GETFMT
+ Q_GETINFO
+ Q_SETINFO
+ Q_GETQUOTA
+ Q_SETQUOTA
+ Q_GETNEXTQUOTA
+)
+
+const (
+ FlagBLimitsValid = 1 << iota
+ FlagSpaceValid
+ FlagILimitsValid
+ FlagInodesValid
+ FlagBTimeValid
+ FlagITimeValid
+)
+
+type DQInfo struct {
+ Bgrace uint64
+ Igrace uint64
+ Flags uint32
+ Valid uint32
+}
+
+type Quota struct {
+ BHardLimit uint64 // Both Byte limits are prescaled by 1024 (so are in KiB), but CurSpace is in B
+ BSoftLimit uint64
+ CurSpace uint64
+ IHardLimit uint64
+ ISoftLimit uint64
+ CurInodes uint64
+ BTime uint64
+ ITime uint64
+ Valid uint32
+}
+
+type NextDQBlk struct {
+ HardLimitBytes uint64
+ SoftLimitBytes uint64
+ CurrentBytes uint64
+ HardLimitInodes uint64
+ SoftLimitInodes uint64
+ CurrentInodes uint64
+ BTime uint64
+ ITime uint64
+ Valid uint32
+ ID uint32
+}
+
+type QuotaFormat uint32
+
+// Collected from quota_format_type structs
+const (
+ // QuotaFormatNone is a special case where all quota information is
+ // stored inside filesystem metadata and thus requires no quotaFilePath.
+ QuotaFormatNone QuotaFormat = 0
+ QuotaFormatVFSOld QuotaFormat = 1
+ QuotaFormatVFSV0 QuotaFormat = 2
+ QuotaFormatOCFS2 QuotaFormat = 3
+ QuotaFormatVFSV1 QuotaFormat = 4
+)
+
+// QuotaOn turns quota accounting and enforcement on
+func QuotaOn(device string, qtype QuotaType, quotaFormat QuotaFormat, quotaFilePath string) error {
+ devArg, err := unix.BytePtrFromString(device)
+ if err != nil {
+ return err
+ }
+ pathArg, err := unix.BytePtrFromString(quotaFilePath)
+ if err != nil {
+ return err
+ }
+ _, _, err = unix.Syscall6(unix.SYS_QUOTACTL, uintptr(Q_QUOTAON|uint(qtype)), uintptr(unsafe.Pointer(devArg)), uintptr(quotaFormat), uintptr(unsafe.Pointer(pathArg)), 0, 0)
+ if err != unix.Errno(0) {
+ return err
+ }
+ return nil
+}
+
+// QuotaOff turns quotas off
+func QuotaOff(device string, qtype QuotaType) error {
+ devArg, err := unix.BytePtrFromString(device)
+ if err != nil {
+ return err
+ }
+ _, _, err = unix.Syscall6(unix.SYS_QUOTACTL, uintptr(Q_QUOTAOFF|uint(qtype)), uintptr(unsafe.Pointer(devArg)), 0, 0, 0, 0)
+ if err != unix.Errno(0) {
+ return err
+ }
+ return nil
+}
+
+// GetFmt gets the quota format used on given filesystem
+func GetFmt(device string, qtype QuotaType) (uint32, error) {
+ var fmt uint32
+ devArg, err := unix.BytePtrFromString(device)
+ if err != nil {
+ return 0, err
+ }
+ _, _, err = unix.Syscall6(unix.SYS_QUOTACTL, uintptr(Q_GETFMT|uint(qtype)), uintptr(unsafe.Pointer(devArg)), 0, uintptr(unsafe.Pointer(&fmt)), 0, 0)
+ if err != unix.Errno(0) {
+ return 0, err
+ }
+ return fmt, nil
+}
+
+// GetInfo gets information about quota files
+func GetInfo(device string, qtype QuotaType) (*DQInfo, error) {
+ var info DQInfo
+ devArg, err := unix.BytePtrFromString(device)
+ if err != nil {
+ return nil, err
+ }
+ _, _, err = unix.Syscall6(unix.SYS_QUOTACTL, uintptr(Q_GETINFO|uint(qtype)), uintptr(unsafe.Pointer(devArg)), 0, uintptr(unsafe.Pointer(&info)), 0, 0)
+ if err != unix.Errno(0) {
+ return nil, err
+ }
+ return &info, nil
+}
+
+// SetInfo sets information about quota files
+func SetInfo(device string, qtype QuotaType, info *DQInfo) error {
+ devArg, err := unix.BytePtrFromString(device)
+ if err != nil {
+ return err
+ }
+ _, _, err = unix.Syscall6(unix.SYS_QUOTACTL, uintptr(Q_SETINFO|uint(qtype)), uintptr(unsafe.Pointer(devArg)), 0, uintptr(unsafe.Pointer(info)), 0, 0)
+ if err != unix.Errno(0) {
+ return err
+ }
+ return nil
+}
+
+// GetQuota gets user quota structure
+func GetQuota(device string, qtype QuotaType, id uint32) (*Quota, error) {
+ var info Quota
+ devArg, err := unix.BytePtrFromString(device)
+ if err != nil {
+ return nil, err
+ }
+ _, _, err = unix.Syscall6(unix.SYS_QUOTACTL, uintptr(Q_GETQUOTA|uint(qtype)), uintptr(unsafe.Pointer(devArg)), uintptr(id), uintptr(unsafe.Pointer(&info)), 0, 0)
+ if err != unix.Errno(0) {
+ return nil, err
+ }
+ return &info, nil
+}
+
+// GetNextQuota gets disk limits and usage > ID
+func GetNextQuota(device string, qtype QuotaType, id uint32) (*NextDQBlk, error) {
+ var info NextDQBlk
+ devArg, err := unix.BytePtrFromString(device)
+ if err != nil {
+ return nil, err
+ }
+ _, _, err = unix.Syscall6(unix.SYS_QUOTACTL, uintptr(Q_GETNEXTQUOTA|uint(qtype)), uintptr(unsafe.Pointer(devArg)), uintptr(id), uintptr(unsafe.Pointer(&info)), 0, 0)
+ if err != unix.Errno(0) {
+ return nil, err
+ }
+ return &info, nil
+}
+
+// SetQuota sets the given quota
+func SetQuota(device string, qtype QuotaType, id uint32, quota *Quota) error {
+ devArg, err := unix.BytePtrFromString(device)
+ if err != nil {
+ return err
+ }
+ _, _, err = unix.Syscall6(unix.SYS_QUOTACTL, uintptr(Q_SETQUOTA|uint(qtype)), uintptr(unsafe.Pointer(devArg)), uintptr(id), uintptr(unsafe.Pointer(quota)), 0, 0)
+ if err != unix.Errno(0) {
+ return fmt.Errorf("failed to set quota: %w", err)
+ }
+ return nil
+}
+
+// Sync syncs disk copy of filesystems quotas. If device is empty it syncs all filesystems.
+func Sync(device string) error {
+ if device != "" {
+ devArg, err := unix.BytePtrFromString(device)
+ if err != nil {
+ return err
+ }
+ _, _, err = unix.Syscall6(unix.SYS_QUOTACTL, uintptr(Q_SYNC), uintptr(unsafe.Pointer(devArg)), 0, 0, 0, 0)
+ if err != unix.Errno(0) {
+ return err
+ }
+ } else {
+ _, _, err := unix.Syscall6(unix.SYS_QUOTACTL, uintptr(Q_SYNC), 0, 0, 0, 0, 0)
+ if err != unix.Errno(0) {
+ return err
+ }
+ }
+ return nil
+}