metropolis: unify utility packages
One last sweeping rename / reshuffle.
We get rid of //metropolis/node/common and //golibs, unifying them into
a single //metropolis/pkg meta-package.
This is to be documented somwhere properly, but here's the new logic
behind selecting where to place a new library package:
- if it's specific to k8s-on-metropolis, put it in
//metropolis/node/kubernetes/*. This is a self-contained tree that
other paths cannot import from.
- if it's a big new subsystem of the metropolis core, put it in
//metropolis/node/core. This can be imported by anything in
//m/n (eg the Kubernetes code at //m/n/kubernetes
- otherwise, treat it as generic library that's part of the metropolis
project, and put it in //metropolis/pkg. This can be imported by
anything within //metropolis.
This will be followed up by a diff that updates visibility rules.
Test Plan: Pure refactor, CI only.
X-Origin-Diff: phab/D683
GitOrigin-RevId: 883e7f09a7d22d64e966d07bbe839454ed081c79
diff --git a/metropolis/pkg/fsquota/BUILD.bazel b/metropolis/pkg/fsquota/BUILD.bazel
new file mode 100644
index 0000000..5f875a9
--- /dev/null
+++ b/metropolis/pkg/fsquota/BUILD.bazel
@@ -0,0 +1,39 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+load("//metropolis/test/ktest:ktest.bzl", "ktest")
+
+go_library(
+ name = "go_default_library",
+ srcs = [
+ "fsinfo.go",
+ "fsquota.go",
+ ],
+ importpath = "git.monogon.dev/source/nexantic.git/metropolis/pkg/fsquota",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//metropolis/pkg/fsquota/fsxattrs:go_default_library",
+ "//metropolis/pkg/fsquota/quotactl:go_default_library",
+ "@org_golang_x_sys//unix:go_default_library",
+ ],
+)
+
+go_test(
+ name = "go_default_test",
+ srcs = ["fsquota_test.go"],
+ embed = [":go_default_library"],
+ pure = "on",
+ deps = [
+ "@com_github_stretchr_testify//require:go_default_library",
+ "@org_golang_x_sys//unix:go_default_library",
+ ],
+)
+
+ktest(
+ tester = ":go_default_test",
+ deps = [
+ "//third_party/xfsprogs:mkfs.xfs",
+ ],
+ initramfs_extra = """
+file /mkfs.xfs $(location //third_party/xfsprogs:mkfs.xfs) 0755 0 0
+ """,
+ cmdline = "ramdisk_size=51200",
+)
diff --git a/metropolis/pkg/fsquota/fsinfo.go b/metropolis/pkg/fsquota/fsinfo.go
new file mode 100644
index 0000000..e40a533
--- /dev/null
+++ b/metropolis/pkg/fsquota/fsinfo.go
@@ -0,0 +1,59 @@
+// 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 fsquota
+
+import (
+ "fmt"
+ "os"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+)
+
+// This requires fsinfo() support, which is not yet in any stable kernel.
+// Our kernel has that syscall backported. This would otherwise be an extremely expensive
+// operation and also involve lots of logic from our side.
+
+// From syscall_64.tbl
+const sys_fsinfo = 441
+
+// From uapi/linux/fsinfo.h
+const fsinfo_attr_source = 0x09
+const fsinfo_flags_query_path = 0x0000
+const fsinfo_flags_query_fd = 0x0001
+
+type fsinfoParams struct {
+ resolveFlags uint64
+ atFlags uint32
+ flags uint32
+ request uint32
+ nth uint32
+ mth uint32
+}
+
+func fsinfoGetSource(dir *os.File) (string, error) {
+ buf := make([]byte, 256)
+ params := fsinfoParams{
+ flags: fsinfo_flags_query_fd,
+ request: fsinfo_attr_source,
+ }
+ n, _, err := unix.Syscall6(sys_fsinfo, dir.Fd(), 0, uintptr(unsafe.Pointer(¶ms)), unsafe.Sizeof(params), uintptr(unsafe.Pointer(&buf[0])), 128)
+ if err != unix.Errno(0) {
+ return "", fmt.Errorf("failed to call fsinfo: %w", err)
+ }
+ return string(buf[:n]), nil
+}
diff --git a/metropolis/pkg/fsquota/fsquota.go b/metropolis/pkg/fsquota/fsquota.go
new file mode 100644
index 0000000..b1305f8
--- /dev/null
+++ b/metropolis/pkg/fsquota/fsquota.go
@@ -0,0 +1,146 @@
+// 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 fsquota provides a simplified interface to interact with Linux's filesystem qouta API.
+// It only supports setting quotas on directories, not groups or users.
+// Quotas need to be already enabled on the filesystem to be able to use them using this package.
+// See the quotactl package if you intend to use this on a filesystem where quotas need to be
+// enabled manually.
+package fsquota
+
+import (
+ "fmt"
+ "math"
+ "os"
+
+ "golang.org/x/sys/unix"
+
+ "git.monogon.dev/source/nexantic.git/metropolis/pkg/fsquota/fsxattrs"
+ "git.monogon.dev/source/nexantic.git/metropolis/pkg/fsquota/quotactl"
+)
+
+// SetQuota sets the quota of bytes and/or inodes in a given path. To not set a limit, set the
+// corresponding argument to zero. Setting both arguments to zero removes the quota entirely.
+// This function can only be called on an empty directory. It can't be used to create a quota
+// below a directory which already has a quota since Linux doesn't offer hierarchical quotas.
+func SetQuota(path string, maxBytes uint64, maxInodes uint64) error {
+ dir, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer dir.Close()
+ source, err := fsinfoGetSource(dir)
+ if err != nil {
+ return err
+ }
+ var valid uint32
+ if maxBytes > 0 {
+ valid |= quotactl.FlagBLimitsValid
+ }
+ if maxInodes > 0 {
+ valid |= quotactl.FlagILimitsValid
+ }
+
+ attrs, err := fsxattrs.Get(dir)
+ if err != nil {
+ return err
+ }
+
+ var lastID uint32 = attrs.ProjectID
+ if lastID == 0 {
+ // No project/quota exists for this directory, assign a new project quota
+ // TODO(lorenz): This is racy, but the kernel does not support atomically assigning
+ // quotas. So this needs to be added to the kernels setquota interface. Due to the short
+ // time window and infrequent calls this should not be an immediate issue.
+ for {
+ quota, err := quotactl.GetNextQuota(source, quotactl.QuotaTypeProject, lastID)
+ if err == unix.ENOENT || err == unix.ESRCH {
+ // We have enumerated all quotas, nothing exists here
+ break
+ } else if err != nil {
+ return fmt.Errorf("failed to call GetNextQuota: %w", err)
+ }
+ if quota.ID > lastID+1 {
+ // Take the first ID in the quota ID gap
+ lastID++
+ break
+ }
+ lastID++
+ }
+ }
+
+ // If both limits are zero, this is a delete operation, process it as such
+ if maxBytes == 0 && maxInodes == 0 {
+ valid = quotactl.FlagBLimitsValid | quotactl.FlagILimitsValid
+ attrs.ProjectID = 0
+ attrs.Flags &= ^fsxattrs.FlagProjectInherit
+ } else {
+ attrs.ProjectID = lastID
+ attrs.Flags |= fsxattrs.FlagProjectInherit
+ }
+
+ if err := fsxattrs.Set(dir, attrs); err != nil {
+ return err
+ }
+
+ // Always round up to the nearest block size
+ bytesLimitBlocks := uint64(math.Ceil(float64(maxBytes) / float64(1024)))
+
+ return quotactl.SetQuota(source, quotactl.QuotaTypeProject, lastID, "actl.Quota{
+ BHardLimit: bytesLimitBlocks,
+ BSoftLimit: bytesLimitBlocks,
+ IHardLimit: maxInodes,
+ ISoftLimit: maxInodes,
+ Valid: valid,
+ })
+}
+
+type Quota struct {
+ Bytes uint64
+ BytesUsed uint64
+ Inodes uint64
+ InodesUsed uint64
+}
+
+// GetQuota returns the current active quota and its utilization at the given path
+func GetQuota(path string) (*Quota, error) {
+ dir, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer dir.Close()
+ source, err := fsinfoGetSource(dir)
+ if err != nil {
+ return nil, err
+ }
+ attrs, err := fsxattrs.Get(dir)
+ if err != nil {
+ return nil, err
+ }
+ if attrs.ProjectID == 0 {
+ return nil, os.ErrNotExist
+ }
+ quota, err := quotactl.GetQuota(source, quotactl.QuotaTypeProject, attrs.ProjectID)
+ if err != nil {
+ return nil, err
+ }
+ return &Quota{
+ Bytes: quota.BHardLimit * 1024,
+ BytesUsed: quota.CurSpace,
+ Inodes: quota.IHardLimit,
+ InodesUsed: quota.CurInodes,
+ }, nil
+}
diff --git a/metropolis/pkg/fsquota/fsquota_test.go b/metropolis/pkg/fsquota/fsquota_test.go
new file mode 100644
index 0000000..4729dac
--- /dev/null
+++ b/metropolis/pkg/fsquota/fsquota_test.go
@@ -0,0 +1,152 @@
+// 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 fsquota
+
+import (
+ "fmt"
+ "io/ioutil"
+ "math"
+ "os"
+ "os/exec"
+ "syscall"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "golang.org/x/sys/unix"
+)
+
+// withinTolerance is a helper for asserting that a value is within a certain percentage of the
+// expected value. The tolerance is specified as a float between 0 (exact match)
+// and 1 (between 0 and twice the expected value).
+func withinTolerance(t *testing.T, expected uint64, actual uint64, tolerance float64, name string) {
+ t.Helper()
+ delta := uint64(math.Round(float64(expected) * tolerance))
+ lowerBound := expected - delta
+ upperBound := expected + delta
+ if actual < lowerBound {
+ t.Errorf("Value %v (%v) is too low, expected between %v and %v", name, actual, lowerBound, upperBound)
+ }
+ if actual > upperBound {
+ t.Errorf("Value %v (%v) is too high, expected between %v and %v", name, actual, lowerBound, upperBound)
+ }
+}
+
+func TestBasic(t *testing.T) {
+ if os.Getenv("IN_KTEST") != "true" {
+ t.Skip("Not in ktest")
+ }
+ mkfsCmd := exec.Command("/mkfs.xfs", "-qf", "/dev/ram0")
+ if _, err := mkfsCmd.Output(); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir("/test", 0755); err != nil {
+ t.Error(err)
+ }
+
+ if err := unix.Mount("/dev/ram0", "/test", "xfs", unix.MS_NOEXEC|unix.MS_NODEV, "prjquota"); err != nil {
+ t.Fatal(err)
+ }
+ defer unix.Unmount("/test", 0)
+ defer os.RemoveAll("/test")
+ t.Run("SetQuota", func(t *testing.T) {
+ defer func() {
+ os.RemoveAll("/test/set")
+ }()
+ if err := os.Mkdir("/test/set", 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := SetQuota("/test/set", 1024*1024, 100); err != nil {
+ t.Fatal(err)
+ }
+ })
+ t.Run("SetQuotaAndExhaust", func(t *testing.T) {
+ defer func() {
+ os.RemoveAll("/test/sizequota")
+ }()
+ if err := os.Mkdir("/test/sizequota", 0755); err != nil {
+ t.Fatal(err)
+ }
+ const bytesQuota = 1024 * 1024 // 1MiB
+ if err := SetQuota("/test/sizequota", bytesQuota, 0); err != nil {
+ t.Fatal(err)
+ }
+ testfile, err := os.Create("/test/sizequota/testfile")
+ if err != nil {
+ t.Fatal(err)
+ }
+ testdata := make([]byte, 1024)
+ var bytesWritten int
+ for {
+ n, err := testfile.Write([]byte(testdata))
+ if err != nil {
+ if pathErr, ok := err.(*os.PathError); ok {
+ if pathErr.Err == syscall.ENOSPC {
+ // Running out of space is the only acceptable error to continue execution
+ break
+ }
+ }
+ t.Fatal(err)
+ }
+ bytesWritten += n
+ }
+ if bytesWritten > bytesQuota {
+ t.Errorf("Wrote %v bytes, quota is only %v bytes", bytesWritten, bytesQuota)
+ }
+ })
+ t.Run("GetQuotaReadbackAndUtilization", func(t *testing.T) {
+ defer func() {
+ os.RemoveAll("/test/readback")
+ }()
+ if err := os.Mkdir("/test/readback", 0755); err != nil {
+ t.Fatal(err)
+ }
+ const bytesQuota = 1024 * 1024 // 1MiB
+ const inodesQuota = 100
+ if err := SetQuota("/test/readback", bytesQuota, inodesQuota); err != nil {
+ t.Fatal(err)
+ }
+ sizeFileData := make([]byte, 512*1024)
+ if err := ioutil.WriteFile("/test/readback/512kfile", sizeFileData, 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ quotaUtil, err := GetQuota("/test/readback")
+ if err != nil {
+ t.Fatal(err)
+ }
+ require.Equal(t, uint64(bytesQuota), quotaUtil.Bytes, "bytes quota readback incorrect")
+ require.Equal(t, uint64(inodesQuota), quotaUtil.Inodes, "inodes quota readback incorrect")
+
+ // Give 10% tolerance for quota used values to account for metadata overhead and internal
+ // structures that are also in there. If it's out by more than that it's an issue anyways.
+ withinTolerance(t, uint64(len(sizeFileData)), quotaUtil.BytesUsed, 0.1, "BytesUsed")
+
+ // Write 50 inodes for a total of 51 (with the 512K file)
+ for i := 0; i < 50; i++ {
+ if err := ioutil.WriteFile(fmt.Sprintf("/test/readback/ifile%v", i), []byte("test"), 0644); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ quotaUtil, err = GetQuota("/test/readback")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ withinTolerance(t, 51, quotaUtil.InodesUsed, 0.1, "InodesUsed")
+ })
+}
diff --git a/metropolis/pkg/fsquota/fsxattrs/BUILD.bazel b/metropolis/pkg/fsquota/fsxattrs/BUILD.bazel
new file mode 100644
index 0000000..87f2617
--- /dev/null
+++ b/metropolis/pkg/fsquota/fsxattrs/BUILD.bazel
@@ -0,0 +1,9 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["fsxattrs.go"],
+ importpath = "git.monogon.dev/source/nexantic.git/metropolis/pkg/fsquota/fsxattrs",
+ visibility = ["//visibility:public"],
+ deps = ["@org_golang_x_sys//unix:go_default_library"],
+)
diff --git a/metropolis/pkg/fsquota/fsxattrs/fsxattrs.go b/metropolis/pkg/fsquota/fsxattrs/fsxattrs.go
new file mode 100644
index 0000000..1d455eb
--- /dev/null
+++ b/metropolis/pkg/fsquota/fsxattrs/fsxattrs.go
@@ -0,0 +1,77 @@
+// 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 fsxattrs
+
+import (
+ "fmt"
+ "os"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+)
+
+type FSXAttrFlag uint32
+
+// Defined in uapi/linux/fs.h
+const (
+ FlagRealtime FSXAttrFlag = 0x00000001
+ FlagPreallocated FSXAttrFlag = 0x00000002
+ FlagImmutable FSXAttrFlag = 0x00000008
+ FlagAppend FSXAttrFlag = 0x00000010
+ FlagSync FSXAttrFlag = 0x00000020
+ FlagNoATime FSXAttrFlag = 0x00000040
+ FlagNoDump FSXAttrFlag = 0x00000080
+ FlagRealtimeInherit FSXAttrFlag = 0x00000100
+ FlagProjectInherit FSXAttrFlag = 0x00000200
+ FlagNoSymlinks FSXAttrFlag = 0x00000400
+ FlagExtentSize FSXAttrFlag = 0x00000800
+ FlagNoDefragment FSXAttrFlag = 0x00002000
+ FlagFilestream FSXAttrFlag = 0x00004000
+ FlagDAX FSXAttrFlag = 0x00008000
+ FlagCOWExtentSize FSXAttrFlag = 0x00010000
+ FlagHasAttribute FSXAttrFlag = 0x80000000
+)
+
+// FS_IOC_FSGETXATTR/FS_IOC_FSSETXATTR are defined in uapi/linux/fs.h
+const FS_IOC_FSGETXATTR = 0x801c581f
+const FS_IOC_FSSETXATTR = 0x401c5820
+
+type FSXAttrs struct {
+ Flags FSXAttrFlag
+ ExtentSize uint32
+ ExtentCount uint32
+ ProjectID uint32
+ CoWExtentSize uint32
+ _pad [8]byte
+}
+
+func Get(file *os.File) (*FSXAttrs, error) {
+ var attrs FSXAttrs
+ _, _, errno := unix.Syscall(unix.SYS_IOCTL, file.Fd(), FS_IOC_FSGETXATTR, uintptr(unsafe.Pointer(&attrs)))
+ if errno != 0 {
+ return nil, fmt.Errorf("failed to execute getFSXAttrs: %v", errno)
+ }
+ return &attrs, nil
+}
+
+func Set(file *os.File, attrs *FSXAttrs) error {
+ _, _, errno := unix.Syscall(unix.SYS_IOCTL, file.Fd(), FS_IOC_FSSETXATTR, uintptr(unsafe.Pointer(attrs)))
+ if errno != 0 {
+ return fmt.Errorf("failed to execute setFSXAttrs: %v", errno)
+ }
+ return nil
+}
diff --git a/metropolis/pkg/fsquota/quotactl/BUILD.bazel b/metropolis/pkg/fsquota/quotactl/BUILD.bazel
new file mode 100644
index 0000000..406c784
--- /dev/null
+++ b/metropolis/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/metropolis/pkg/fsquota/quotactl",
+ visibility = ["//visibility:public"],
+ deps = ["@org_golang_x_sys//unix:go_default_library"],
+)
diff --git a/metropolis/pkg/fsquota/quotactl/quotactl.go b/metropolis/pkg/fsquota/quotactl/quotactl.go
new file mode 100644
index 0000000..5ed77d7
--- /dev/null
+++ b/metropolis/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
+}