treewide: introduce osbase package and move things around
All except localregistry moved from metropolis/pkg to osbase,
localregistry moved to metropolis/test as its only used there anyway.
Change-Id: If1a4bf377364bef0ac23169e1b90379c71b06d72
Reviewed-on: https://review.monogon.dev/c/monogon/+/3079
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/osbase/devicemapper/BUILD.bazel b/osbase/devicemapper/BUILD.bazel
new file mode 100644
index 0000000..13957b9
--- /dev/null
+++ b/osbase/devicemapper/BUILD.bazel
@@ -0,0 +1,16 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "devicemapper",
+ srcs = [
+ "ctype.go",
+ "devicemapper.go",
+ ],
+ importpath = "source.monogon.dev/osbase/devicemapper",
+ visibility = ["//visibility:public"],
+ deps = [
+ "@com_github_pkg_errors//:errors",
+ "@com_github_yalue_native_endian//:native_endian",
+ "@org_golang_x_sys//unix",
+ ],
+)
diff --git a/osbase/devicemapper/ctype.go b/osbase/devicemapper/ctype.go
new file mode 100644
index 0000000..05e6934
--- /dev/null
+++ b/osbase/devicemapper/ctype.go
@@ -0,0 +1,40 @@
+package devicemapper
+
+// Linux kernel ctype data from @linux//include/linux:ctype.h
+
+const (
+ _U = 0x01 /* upper */
+ _L = 0x02 /* lower */
+ _D = 0x04 /* digit */
+ _C = 0x08 /* cntrl */
+ _P = 0x10 /* punct */
+ _S = 0x20 /* white space (space/lf/tab) */
+ _X = 0x40 /* hex digit */
+ _SP = 0x80 /* hard space (0x20) */
+)
+
+var ctypeLookup = [256]byte{
+ _C, _C, _C, _C, _C, _C, _C, _C, /* 0-7 */
+ _C, _C | _S, _C | _S, _C | _S, _C | _S, _C | _S, _C, _C, /* 8-15 */
+ _C, _C, _C, _C, _C, _C, _C, _C, /* 16-23 */
+ _C, _C, _C, _C, _C, _C, _C, _C, /* 24-31 */
+ _S | _SP, _P, _P, _P, _P, _P, _P, _P, /* 32-39 */
+ _P, _P, _P, _P, _P, _P, _P, _P, /* 40-47 */
+ _D, _D, _D, _D, _D, _D, _D, _D, /* 48-55 */
+ _D, _D, _P, _P, _P, _P, _P, _P, /* 56-63 */
+ _P, _U | _X, _U | _X, _U | _X, _U | _X, _U | _X, _U | _X, _U, /* 64-71 */
+ _U, _U, _U, _U, _U, _U, _U, _U, /* 72-79 */
+ _U, _U, _U, _U, _U, _U, _U, _U, /* 80-87 */
+ _U, _U, _U, _P, _P, _P, _P, _P, /* 88-95 */
+ _P, _L | _X, _L | _X, _L | _X, _L | _X, _L | _X, _L | _X, _L, /* 96-103 */
+ _L, _L, _L, _L, _L, _L, _L, _L, /* 104-111 */
+ _L, _L, _L, _L, _L, _L, _L, _L, /* 112-119 */
+ _L, _L, _L, _P, _P, _P, _P, _C, /* 120-127 */
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 128-143 */
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 144-159 */
+ _S | _SP, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, /* 160-175 */
+ _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, /* 176-191 */
+ _U, _U, _U, _U, _U, _U, _U, _U, _U, _U, _U, _U, _U, _U, _U, _U, /* 192-207 */
+ _U, _U, _U, _U, _U, _U, _U, _P, _U, _U, _U, _U, _U, _U, _U, _L, /* 208-223 */
+ _L, _L, _L, _L, _L, _L, _L, _L, _L, _L, _L, _L, _L, _L, _L, _L, /* 224-239 */
+ _L, _L, _L, _L, _L, _L, _L, _P, _L, _L, _L, _L, _L, _L, _L, _L} /* 240-255 */
diff --git a/osbase/devicemapper/devicemapper.go b/osbase/devicemapper/devicemapper.go
new file mode 100644
index 0000000..1999a00
--- /dev/null
+++ b/osbase/devicemapper/devicemapper.go
@@ -0,0 +1,345 @@
+// 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 devicemapper is a thin wrapper for the devicemapper ioctl API.
+// See: https://github.com/torvalds/linux/blob/master/include/uapi/linux/dm-ioctl.h
+package devicemapper
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "os"
+ "runtime"
+ "strings"
+ "sync"
+ "unsafe"
+
+ "github.com/pkg/errors"
+ "github.com/yalue/native_endian"
+ "golang.org/x/sys/unix"
+)
+
+type DMIoctl struct {
+ Version Version
+ DataSize uint32
+ DataStart uint32
+ TargetCount uint32
+ OpenCount int32
+ Flags uint32
+ EventNumber uint32
+ _padding1 uint32
+ Dev uint64
+ Name [128]byte
+ UUID [129]byte
+ _padding2 [7]byte
+ Data [16384]byte
+}
+
+type DMTargetSpec struct {
+ SectorStart uint64
+ Length uint64
+ Status int32
+ Next uint32
+ TargetType [16]byte
+}
+
+type DMTargetDeps struct {
+ Count uint32
+ Padding uint32
+ Dev []uint64
+}
+
+type DMNameList struct {
+ Dev uint64
+ Next uint32
+ Name []byte
+}
+
+type DMTargetVersions struct {
+ Next uint32
+ Version [3]uint32
+}
+
+type DMTargetMessage struct {
+ Sector uint64
+ Message []byte
+}
+
+type Version [3]uint32
+
+const (
+ /* Top level cmds */
+ DM_VERSION_CMD uintptr = (0xc138fd << 8) + iota
+ DM_REMOVE_ALL_CMD
+ DM_LIST_DEVICES_CMD
+
+ /* device level cmds */
+ DM_DEV_CREATE_CMD
+ DM_DEV_REMOVE_CMD
+ DM_DEV_RENAME_CMD
+ DM_DEV_SUSPEND_CMD
+ DM_DEV_STATUS_CMD
+ DM_DEV_WAIT_CMD
+
+ /* Table level cmds */
+ DM_TABLE_LOAD_CMD
+ DM_TABLE_CLEAR_CMD
+ DM_TABLE_DEPS_CMD
+ DM_TABLE_STATUS_CMD
+
+ /* Added later */
+ DM_LIST_VERSIONS_CMD
+ DM_TARGET_MSG_CMD
+ DM_DEV_SET_GEOMETRY_CMD
+ DM_DEV_ARM_POLL_CMD
+)
+
+const (
+ DM_READONLY_FLAG = 1 << 0 /* In/Out */
+ DM_SUSPEND_FLAG = 1 << 1 /* In/Out */
+ DM_PERSISTENT_DEV_FLAG = 1 << 3 /* In */
+)
+
+const baseDataSize = uint32(unsafe.Sizeof(DMIoctl{})) - 16384
+
+func newReq() DMIoctl {
+ return DMIoctl{
+ Version: Version{4, 0, 0},
+ DataSize: baseDataSize,
+ DataStart: baseDataSize,
+ }
+}
+
+// stringToDelimitedBuf copies src to dst and returns an error if len(src) >
+// len(dst), or when the string contains a null byte.
+func stringToDelimitedBuf(dst []byte, src string) error {
+ if len(src) > len(dst)-1 {
+ return fmt.Errorf("string longer than target buffer (%v > %v)", len(src), len(dst)-1)
+ }
+ for i := 0; i < len(src); i++ {
+ if src[i] == 0x00 {
+ return errors.New("string contains null byte, this is unsupported by DM")
+ }
+ dst[i] = src[i]
+ }
+ return nil
+}
+
+// marshalParams marshals a list of strings into a single string according to
+// the rules in the kernel-side decoder. Strings with null bytes or only
+// whitespace characters cannot be encoded and will return an errors.
+func marshalParams(params []string) (string, error) {
+ var strb strings.Builder
+ for _, param := range params {
+ var hasNonWhitespace bool
+ for i := 0; i < len(param); i++ {
+ b := param[i]
+ if b == 0x00 {
+ return "", errors.New("parameter with null bytes cannot be encoded")
+ }
+ isWhitespace := ctypeLookup[b]&_S != 0
+ if !isWhitespace {
+ hasNonWhitespace = true
+ }
+ if isWhitespace || b == '\\' {
+ strb.WriteByte('\\')
+ }
+ strb.WriteByte(b)
+ }
+ if !hasNonWhitespace {
+ return "", errors.New("parameter with only whitespace cannot be encoded")
+ }
+ strb.WriteByte(' ')
+ }
+ return strb.String(), nil
+}
+
+var ctrlFile *os.File
+var ctrlFileError error
+var ctrlFileOnce sync.Once
+
+func initCtrlFile() {
+ ctrlFile, ctrlFileError = os.Open("/dev/mapper/control")
+ if os.IsNotExist(ctrlFileError) {
+ _ = os.MkdirAll("/dev/mapper", 0755)
+ ctrlFileError = unix.Mknod("/dev/mapper/control", unix.S_IFCHR|0600, int(unix.Mkdev(10, 236)))
+ if ctrlFileError != nil {
+ ctrlFileError = fmt.Errorf("devicemapper control device doesn't exist and can't be mknod()ed: %w", ctrlFileError)
+ return
+ }
+ ctrlFile, ctrlFileError = os.Open("/dev/mapper/control")
+ }
+ if ctrlFileError != nil {
+ ctrlFileError = fmt.Errorf("failed to open devicemapper control device: %w", ctrlFileError)
+ }
+}
+
+func GetVersion() (Version, error) {
+ req := newReq()
+ ctrlFileOnce.Do(initCtrlFile)
+ if ctrlFileError != nil {
+ return Version{}, ctrlFileError
+ }
+ if _, _, err := unix.Syscall(unix.SYS_IOCTL, ctrlFile.Fd(), DM_VERSION_CMD, uintptr(unsafe.Pointer(&req))); err != 0 {
+ return Version{}, err
+ }
+ return req.Version, nil
+}
+
+func CreateDevice(name string) (uint64, error) {
+ req := newReq()
+ if err := stringToDelimitedBuf(req.Name[:], name); err != nil {
+ return 0, err
+ }
+ ctrlFileOnce.Do(initCtrlFile)
+ if ctrlFileError != nil {
+ return 0, ctrlFileError
+ }
+ if _, _, err := unix.Syscall(unix.SYS_IOCTL, ctrlFile.Fd(), DM_DEV_CREATE_CMD, uintptr(unsafe.Pointer(&req))); err != 0 {
+ return 0, err
+ }
+ return req.Dev, nil
+}
+
+func RemoveDevice(name string) error {
+ req := newReq()
+ if err := stringToDelimitedBuf(req.Name[:], name); err != nil {
+ return err
+ }
+ ctrlFileOnce.Do(initCtrlFile)
+ if ctrlFileError != nil {
+ return ctrlFileError
+ }
+ if _, _, err := unix.Syscall(unix.SYS_IOCTL, ctrlFile.Fd(), DM_DEV_REMOVE_CMD, uintptr(unsafe.Pointer(&req))); err != 0 {
+ return err
+ }
+ runtime.KeepAlive(req)
+ return nil
+}
+
+// Target represents a byte region inside a devicemapper table for a given
+// device provided by a given target implementation.
+type Target struct {
+ // StartSector is the first sector (defined as being 512 bytes long) this
+ // target covers.
+ StartSector uint64
+ // Length is the number of sectors (defined as being 512 bytes long) this
+ // target covers, starting from StartSector.
+ Length uint64
+ // Type is the type of target handling this byte region.
+ // Types implemented by the Linux kernel can be found at
+ // @linux//drivers/md/... by looking for dm_register_target() calls.
+ Type string
+ // Parameters are additional parameters specific to the target type.
+ // Note that null bytes and parameters consisting only of whitespace
+ // characters cannot be encoded and will return an error.
+ Parameters []string
+}
+
+func LoadTable(name string, readOnly bool, targets []Target) error {
+ req := newReq()
+ if err := stringToDelimitedBuf(req.Name[:], name); err != nil {
+ return err
+ }
+ var data bytes.Buffer
+ for _, target := range targets {
+ encodedParams, err := marshalParams(target.Parameters)
+ if err != nil {
+ return fmt.Errorf("cannot encode parameters: %w", err)
+ }
+ // Gives the size of the spec and the null-terminated params aligned to 8 bytes
+ padding := len(encodedParams) % 8
+ targetSize := uint32(int(unsafe.Sizeof(DMTargetSpec{})) + (len(encodedParams) + 1) + padding)
+
+ targetSpec := DMTargetSpec{
+ SectorStart: target.StartSector,
+ Length: target.Length,
+ Next: targetSize,
+ }
+ if err := stringToDelimitedBuf(targetSpec.TargetType[:], target.Type); err != nil {
+ return err
+ }
+ if err := binary.Write(&data, native_endian.NativeEndian(), &targetSpec); err != nil {
+ panic(err)
+ }
+ data.WriteString(encodedParams)
+ data.WriteByte(0x00)
+ for i := 0; i < padding; i++ {
+ data.WriteByte(0x00)
+ }
+ }
+ req.TargetCount = uint32(len(targets))
+ if data.Len() >= 16384 {
+ return errors.New("table too large for allocated memory")
+ }
+ req.DataSize = baseDataSize + uint32(data.Len())
+ copy(req.Data[:], data.Bytes())
+ if readOnly {
+ req.Flags = DM_READONLY_FLAG
+ }
+ ctrlFileOnce.Do(initCtrlFile)
+ if ctrlFileError != nil {
+ return ctrlFileError
+ }
+ if _, _, err := unix.Syscall(unix.SYS_IOCTL, ctrlFile.Fd(), DM_TABLE_LOAD_CMD, uintptr(unsafe.Pointer(&req))); err != 0 {
+ return err
+ }
+ runtime.KeepAlive(req)
+ return nil
+}
+
+func suspendResume(name string, suspend bool) error {
+ req := newReq()
+ if err := stringToDelimitedBuf(req.Name[:], name); err != nil {
+ return err
+ }
+ if suspend {
+ req.Flags = DM_SUSPEND_FLAG
+ }
+ ctrlFileOnce.Do(initCtrlFile)
+ if ctrlFileError != nil {
+ return ctrlFileError
+ }
+ if _, _, err := unix.Syscall(unix.SYS_IOCTL, ctrlFile.Fd(), DM_DEV_SUSPEND_CMD, uintptr(unsafe.Pointer(&req))); err != 0 {
+ return err
+ }
+ runtime.KeepAlive(req)
+ return nil
+}
+
+func Suspend(name string) error {
+ return suspendResume(name, true)
+}
+func Resume(name string) error {
+ return suspendResume(name, false)
+}
+
+func CreateActiveDevice(name string, readOnly bool, targets []Target) (uint64, error) {
+ dev, err := CreateDevice(name)
+ if err != nil {
+ return 0, fmt.Errorf("DM_DEV_CREATE failed: %w", err)
+ }
+ if err := LoadTable(name, readOnly, targets); err != nil {
+ _ = RemoveDevice(name)
+ return 0, fmt.Errorf("DM_TABLE_LOAD failed: %w", err)
+ }
+ if err := Resume(name); err != nil {
+ _ = RemoveDevice(name)
+ return 0, fmt.Errorf("DM_DEV_SUSPEND failed: %w", err)
+ }
+ return dev, nil
+}