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
+}