// Copyright (c) Vitaliy Filippov, 2019+ // License: VNPL-1.1 or GNU GPL-2.0+ (see README.md for details) package vitastor import ( "bytes" "errors" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "k8s.io/klog" "k8s.io/utils/mount" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) type MountMethod int const ( MOUNT_NBD MountMethod = 0 MOUNT_VDUSE MountMethod = 1 MOUNT_UBLK MountMethod = 2 ) func Contains(list []string, s string) bool { for i := 0; i < len(list); i++ { if (list[i] == s) { return true } } return false } func selectMountMethod() MountMethod { // Check UBLK support (ublk_drv kernel module) if (checkModule("ublk_drv")) { klog.Infof("UBLK support enabled successfully") return MOUNT_UBLK } klog.Errorf( "Your host apparently has no UBLK support. UBLK support disabled."+ " For UBLK you need at least Linux 6.0 and the ublk_drv kernel module.", ) // Check VDUSE support (vdpa, vduse, virtio-vdpa kernel modules) vduse := true for _, mod := range []string{"vdpa", "vduse", "virtio-vdpa"} { if (!checkModule(mod)) { vduse = false break } } // Check that vdpa tool functions if (vduse) { c := exec.Command("/sbin/vdpa", "-j", "dev") c.Stderr = os.Stderr err := c.Run() if (err != nil) { klog.Errorf("/sbin/vdpa -j dev failed: %v", err) vduse = false } } if (vduse) { klog.Infof("VDUSE support enabled successfully") return MOUNT_VDUSE } klog.Errorf( "Your host apparently has no VDUSE support. VDUSE support disabled, NBD will be used to map devices."+ " For VDUSE you need at least Linux 5.15 and the following kernel modules: vdpa, virtio-vdpa, vduse.", ) return MOUNT_NBD } func checkModule(mod string) bool { _, err := os.Stat("/sys/module/"+mod) if (err != nil) { if (!errors.Is(err, os.ErrNotExist)) { klog.Errorf("failed to check /sys/module/%s: %v", mod, err) } c := exec.Command("/sbin/modprobe", mod) c.Stdout = os.Stderr c.Stderr = os.Stderr err := c.Run() if (err != nil) { klog.Errorf("/sbin/modprobe %s failed: %v", mod, err) return false } } return true } func mapNbd(volName string, ctxVars map[string]string, readonly bool) (string, error) { // Map NBD device // FIXME: Check if already mapped args := []string{ "map", "--image", volName, } if (ctxVars["configPath"] != "") { args = append(args, "--config_path", ctxVars["configPath"]) } if (readonly) { args = append(args, "--readonly", "1") } stdout, stderr, err := system("/usr/bin/vitastor-nbd", args...) dev := strings.TrimSpace(string(stdout)) if (dev == "") { return "", fmt.Errorf("vitastor-nbd did not return the name of NBD device. output: %s", stderr) } klog.Infof("Attached volume %s via NBD as %s", volName, dev) return dev, err } func unmapNbd(devicePath string) { // unmap NBD device unmapOut, unmapErr := exec.Command("/usr/bin/vitastor-nbd", "unmap", devicePath).CombinedOutput() if (unmapErr != nil) { klog.Errorf("failed to unmap NBD device %s: %s, error: %v", devicePath, unmapOut, unmapErr) } } func findByPidFile(pidFile string) (*os.Process, error) { pidBuf, err := os.ReadFile(pidFile) if (err != nil) { return nil, err } pid, err := strconv.ParseInt(strings.TrimSpace(string(pidBuf)), 0, 64) if (err != nil) { return nil, err } proc, err := os.FindProcess(int(pid)) if (err != nil) { return nil, err } return proc, nil } func killByPidFile(pidFile string) error { klog.Infof("killing process with PID from file %s", pidFile) proc, err := findByPidFile(pidFile) if (err != nil) { return err } return proc.Signal(syscall.SIGTERM) } func startStorageDaemon(vdpaId, volName, pidFile, configPath string, readonly bool) error { // Start qemu-storage-daemon blockSpec := map[string]interface{}{ "node-name": "disk1", "driver": "vitastor", "image": volName, "cache": map[string]bool{ "direct": true, "no-flush": false, }, "discard": "unmap", } if (configPath != "") { blockSpec["config-path"] = configPath } blockSpecJson, _ := json.Marshal(blockSpec) writable := "true" if (readonly) { writable = "false" } _, _, err := system( "/usr/bin/qemu-storage-daemon", "--daemonize", "--pidfile", pidFile, "--blockdev", string(blockSpecJson), "--export", "vduse-blk,id="+vdpaId+",node-name=disk1,name="+vdpaId+",num-queues=16,queue-size=128,writable="+writable, ) return err } func mapVduse(stateDir string, volName string, ctxVars map[string]string, readonly bool) (string, string, error) { // Generate state file stateFd, err := os.CreateTemp(stateDir, "vitastor-vduse-*.json") if (err != nil) { return "", "", err } stateFile := stateFd.Name() stateFd.Close() vdpaId := filepath.Base(stateFile) vdpaId = vdpaId[0:len(vdpaId)-5] // remove ".json" pidFile := stateDir + vdpaId + ".pid" // Map VDUSE device via qemu-storage-daemon err = startStorageDaemon(vdpaId, volName, pidFile, ctxVars["configPath"], readonly) if (err == nil) { // Add device to VDPA bus _, _, err = system("/sbin/vdpa", "-j", "dev", "add", "name", vdpaId, "mgmtdev", "vduse") if (err == nil) { // Find block device name var matches []string matches, err = filepath.Glob("/sys/bus/vdpa/devices/"+vdpaId+"/virtio*/block/*") if (err == nil && len(matches) == 0) { err = errors.New("/sys/bus/vdpa/devices/"+vdpaId+"/virtio*/block/* is not found") } if (err == nil) { blockdev := "/dev/"+filepath.Base(matches[0]) _, err = os.Stat(blockdev) if (err == nil) { // Generate state file stateJSON, _ := json.Marshal(&DeviceState{ ConfigPath: ctxVars["configPath"], VdpaId: vdpaId, Image: volName, Blockdev: blockdev, Readonly: readonly, PidFile: pidFile, }) err = os.WriteFile(stateFile, stateJSON, 0600) if (err == nil) { klog.Infof("Attached volume %s via VDUSE as %s (VDPA ID %s)", volName, blockdev, vdpaId) return blockdev, vdpaId, nil } } } } killErr := killByPidFile(pidFile) if (killErr != nil) { klog.Errorf("Failed to kill started qemu-storage-daemon: %v", killErr) } os.Remove(stateFile) os.Remove(pidFile) } return "", "", err } func unmapVduse(stateDir, devicePath string) { if (len(devicePath) < 6 || devicePath[0:6] != "/dev/v") { klog.Errorf("%s does not start with /dev/v", devicePath) return } vduseDev, err := os.Readlink("/sys/block/"+devicePath[5:]) if (err != nil) { klog.Errorf("%s is not a symbolic link to VDUSE device (../devices/virtual/vduse/xxx): %v", devicePath, err) return } vdpaId := "" p := strings.Index(vduseDev, "/vduse/") if (p >= 0) { vduseDev = vduseDev[p+7:] p = strings.Index(vduseDev, "/") if (p >= 0) { vdpaId = vduseDev[0:p] } } if (vdpaId == "") { klog.Errorf("%s is not a symbolic link to VDUSE device (../devices/virtual/vduse/xxx), but is %v", devicePath, vduseDev) return } unmapVduseById(stateDir, vdpaId) } func unmapVduseById(stateDir, vdpaId string) { _, err := os.Stat("/sys/bus/vdpa/devices/"+vdpaId) if (err != nil) { klog.Errorf("failed to stat /sys/bus/vdpa/devices/"+vdpaId+": %v", err) } else { _, _, _ = system("/sbin/vdpa", "-j", "dev", "del", vdpaId) } stateFile := stateDir + vdpaId + ".json" os.Remove(stateFile) pidFile := stateDir + vdpaId + ".pid" _, err = os.Stat(pidFile) if (os.IsNotExist(err)) { // ok, already killed } else if (err != nil) { klog.Errorf("Failed to stat %v: %v", pidFile, err) return } else { err = killByPidFile(pidFile) if (err != nil) { klog.Errorf("Failed to kill started qemu-storage-daemon: %v", err) } os.Remove(pidFile) } } func mapUblk(stateDir string, volName string, configPath string, readonly bool, recoverDev string) (string, error) { pidFile := "" if (recoverDev != "") { if (len(recoverDev) < 10 || recoverDev[0:10] != "/dev/ublkb") { return "", fmt.Errorf("recover: %s does not start with /dev/ublkb", recoverDev) } pidFile = stateDir + "vitastor-ublk-" + recoverDev[10:] + ".pid" } else { pidFd, err := os.CreateTemp(stateDir, "vitastor-tmp-*.pid") if (err != nil) { return "", err } pidFile = pidFd.Name() pidFd.Close() } // Map device via vitastor-ublk args := []string{ "map", "--image", volName, "--pidfile", pidFile, } if (configPath != "") { args = append(args, "--config_path", configPath) } if (readonly) { args = append(args, "--readonly") } if (recoverDev != "") { args = append(args, "--recover", recoverDev) } stdout, stderr, err := system("/usr/bin/vitastor-ublk", args...) if (err != nil) { return "", err } devicePath := strings.TrimSpace(string(stdout)) if (devicePath == "") { return "", fmt.Errorf("vitastor-ublk did not return the name of the device. output: %s", stderr) } if (len(devicePath) >= 10 && devicePath[0:10] == "/dev/ublkb") { // Generate state file devNum := devicePath[10:] pidNew := stateDir + "vitastor-ublk-" + devNum + ".pid" if (pidFile != pidNew) { err := os.Rename(pidFile, pidNew) if (err != nil) { klog.Errorf("Failed to rename PID file %s to %s: %v", pidFile, pidNew, err) } else { pidFile = pidNew } } stateFile := stateDir + "vitastor-ublk-" + devNum + ".json" stateJSON, _ := json.Marshal(&DeviceState{ ConfigPath: configPath, Image: volName, Readonly: readonly, PidFile: pidFile, }) err = os.WriteFile(stateFile, stateJSON, 0600) if (err == nil) { klog.Infof("Attached volume %s via UBLK as %s", volName, devicePath) return devicePath, nil } os.Remove(stateFile) } killErr := killByPidFile(pidFile) if (killErr != nil) { klog.Errorf("Failed to kill started vitastor-ublk: %v", killErr) } os.Remove(pidFile) return "", err } func unmapUblk(stateDir, devicePath string) { if (len(devicePath) < 10 || devicePath[0:10] != "/dev/ublkb") { klog.Errorf("%s does not start with /dev/ublkb", devicePath) return } unmapOut, unmapErr := exec.Command("/usr/bin/vitastor-ublk", "unmap", devicePath).CombinedOutput() if (unmapErr != nil) { klog.Errorf("failed to unmap UBLK device %s: %s, error: %v", devicePath, unmapOut, unmapErr) } for _, ext := range []string{"json", "pid"} { fn := stateDir + "vitastor-ublk-" + devicePath[10:] + "." + ext err := os.Remove(fn) if (err != nil) { klog.Errorf("failed to remove %s: %v", fn, err) } } } func system(program string, args ...string) ([]byte, []byte, error) { klog.Infof("Running "+program+" "+strings.Join(args, " ")) c := exec.Command(program, args...) var stdout, stderr bytes.Buffer c.Stdout, c.Stderr = &stdout, &stderr err := c.Run() if (err != nil) { stdoutStr, stderrStr := string(stdout.Bytes()), string(stderr.Bytes()) klog.Errorf(program+" "+strings.Join(args, " ")+" failed: %s\nOutput:\n%s", err, stdoutStr+stderrStr) return nil, nil, status.Error(codes.Internal, stdoutStr+stderrStr+" (status "+err.Error()+")") } return stdout.Bytes(), stderr.Bytes(), nil } func systemCombined(program string, args ...string) ([]byte, error) { klog.Infof("Running "+program+" "+strings.Join(args, " ")) c := exec.Command(program, args...) var out bytes.Buffer c.Stdout, c.Stderr = &out, &out err := c.Run() if (err != nil) { outStr := string(out.Bytes()) klog.Errorf(program+" "+strings.Join(args, " ")+" failed: %s, status %s\n", outStr, err) return nil, status.Error(codes.Internal, outStr+" (status "+err.Error()+")") } return out.Bytes(), nil } func GetDeviceNameFromMount(mountPath string) (string, error) { // Use /proc/self/mountinfo to correctly parse bind mounts for block device files mps, err := mount.ParseMountInfo("/proc/self/mountinfo") if (err != nil) { return "", err } slTarget, err := filepath.EvalSymlinks(mountPath) if (err != nil) { slTarget = mountPath } device := "" for _, mp := range mps { if (mp.MountPoint == slTarget) { device = mp.Source if (device[0] != '/' && mp.Root != "/") { // Handle {Source=udev Root=/vdb MountPoint=/var/lib/kubelet/tralaleylo/tralala} for _, other := range mps { if (other.Root == "/" && other.Source == mp.Source) { device = other.MountPoint + mp.Root break } } } break } } return device, nil }