// Copyright (c) Vitaliy Filippov, 2019+
// License: VNPL-1.1 or GNU GPL-2.0+ (see README.md for details)

package vitastor

import (
    "context"
    "crypto/sha1"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "regexp"
    "strconv"
    "strings"
    "sync"
    "syscall"
    "time"

    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "k8s.io/utils/mount"
    utilexec "k8s.io/utils/exec"

    "github.com/container-storage-interface/spec/lib/go/csi"
    "github.com/kubernetes-csi/csi-lib-utils/protosanitizer"
    "k8s.io/klog"
)

// NodeServer struct of Vitastor CSI driver with supported methods of CSI node server spec.
type NodeServer struct
{
    *Driver
    method          MountMethod
    stateDir        string
    nfsStageDir     string
    mounter         mount.Interface
    restartInterval time.Duration
    mu              sync.Mutex
    cond            *sync.Cond
    volumeLocks     map[string]bool
}

type DeviceState struct
{
    ConfigPath string `json:"configPath"`
    VdpaId     string `json:"vdpaId"`
    Image      string `json:"image"`
    Blockdev   string `json:"blockdev"`
    Readonly   bool   `json:"readonly"`
    PidFile    string `json:"pidFile"`
}

type NfsState struct
{
    ConfigPath string `json:"configPath"`
    FsName     string `json:"fsName"`
    Pool       string `json:"pool"`
    Path       string `json:"path"`
    Port       int    `json:"port"`
}

// NewNodeServer create new instance node
func NewNodeServer(driver *Driver) *NodeServer
{
    stateDir := os.Getenv("STATE_DIR")
    if (stateDir == "")
    {
        stateDir = "/run/vitastor-csi"
    }
    if (stateDir[len(stateDir)-1] != '/')
    {
        stateDir += "/"
    }
    nfsStageDir := os.Getenv("NFS_STAGE_DIR")
    if (nfsStageDir == "")
    {
        nfsStageDir = "/var/lib/kubelet/plugins/csi.vitastor.io/nfs"
    }
    ns := &NodeServer{
        Driver:      driver,
        method:      selectMountMethod(),
        stateDir:    stateDir,
        nfsStageDir: nfsStageDir,
        mounter:     mount.New(""),
        volumeLocks: make(map[string]bool),
    }
    ns.cond = sync.NewCond(&ns.mu)
    if (ns.method == MOUNT_VDUSE)
    {
        ns.restoreVduseDaemons()
    }
    else if (ns.method == MOUNT_UBLK)
    {
        ns.restoreUblkDaemons()
    }
    if (ns.method == MOUNT_VDUSE || ns.method == MOUNT_UBLK)
    {
        dur, err := time.ParseDuration(os.Getenv("RESTART_INTERVAL"))
        if (err != nil)
        {
            dur = 10 * time.Second
        }
        ns.restartInterval = dur
        if (ns.restartInterval != time.Duration(0))
        {
            go ns.restarter()
        }
    }
    return ns
}

func (ns *NodeServer) lockVolume(lockId string)
{
    ns.mu.Lock()
    defer ns.mu.Unlock()
    for (ns.volumeLocks[lockId])
    {
        ns.cond.Wait()
    }
    ns.volumeLocks[lockId] = true
    ns.cond.Broadcast()
}

func (ns *NodeServer) unlockVolume(lockId string)
{
    ns.mu.Lock()
    defer ns.mu.Unlock()
    delete(ns.volumeLocks, lockId)
    ns.cond.Broadcast()
}

func (ns *NodeServer) restarter()
{
    // Restart dead VDUSE daemons at regular intervals
    // Otherwise volume I/O may hang in case of a qemu-storage-daemon crash
    // Moreover, it may lead to a kernel panic of the kernel is configured to
    // panic on hung tasks
    ticker := time.NewTicker(ns.restartInterval)
    defer ticker.Stop()
    for
    {
        <-ticker.C
        if (ns.method == MOUNT_VDUSE)
        {
            ns.restoreVduseDaemons()
        }
        else if (ns.method == MOUNT_UBLK)
        {
            ns.restoreUblkDaemons()
        }
    }
}

func (ns *NodeServer) restoreVduseDaemons()
{
    pattern := ns.stateDir+"vitastor-vduse-*.json"
    stateFiles, err := filepath.Glob(pattern)
    if (err != nil)
    {
        klog.Errorf("failed to list %s: %v", pattern, err)
    }
    if (len(stateFiles) == 0)
    {
        return
    }
    devList := make(map[string]interface{})
    // example output: {"dev":{"test1":{"type":"block","mgmtdev":"vduse","vendor_id":0,"max_vqs":16,"max_vq_size":128}}}
    devListJSON, _, err := system("/sbin/vdpa", "-j", "dev", "list")
    if (err != nil)
    {
        return
    }
    err = json.Unmarshal(devListJSON, &devList)
    devs, ok := devList["dev"].(map[string]interface{})
    if (err != nil || !ok)
    {
        klog.Errorf("/sbin/vdpa -j dev list returned bad JSON (error %v): %v", err, string(devListJSON))
        return
    }
    for _, stateFile := range stateFiles
    {
        ns.checkVduseState(stateFile, devs)
    }
}

func (ns *NodeServer) checkVduseState(stateFile string, devs map[string]interface{})
{
    // Check if VDPA device is still added to the bus
    vdpaId := filepath.Base(stateFile)
    vdpaId = vdpaId[0:len(vdpaId)-5]
    if (devs[vdpaId] == nil)
    {
        // Unused, clean it up
        unmapVduseById(ns.stateDir, vdpaId)
        return
    }

    // Read state file
    stateJSON, err := os.ReadFile(stateFile)
    if (err != nil)
    {
        klog.Warningf("error reading state file %v: %v", stateFile, err)
        return
    }
    var state DeviceState
    err = json.Unmarshal(stateJSON, &state)
    if (err != nil)
    {
        klog.Warningf("state file %v contains invalid JSON (error %v): %v", stateFile, err, string(stateJSON))
        return
    }

    // Lock volume
    ns.lockVolume(state.ConfigPath+":block:"+state.Image)
    defer ns.unlockVolume(state.ConfigPath+":block:"+state.Image)

    // Recheck state file after locking
    _, err = os.ReadFile(stateFile)
    if (err != nil)
    {
        klog.Warningf("state file %v disappeared, skipping volume", stateFile)
        return
    }

    // Check if the storage daemon is still active
    pidFile := ns.stateDir + vdpaId + ".pid"
    exists := false
    proc, err := findByPidFile(pidFile)
    if (err == nil)
    {
        exists = proc.Signal(syscall.Signal(0)) == nil
    }
    if (!exists)
    {
        // Restart daemon
        klog.Warningf("restarting storage daemon for volume %v (VDPA ID %v)", state.Image, vdpaId)
        err = startStorageDaemon(vdpaId, state.Image, pidFile, state.ConfigPath, state.Readonly)
        if (err != nil)
        {
            klog.Warningf("failed to restart storage daemon for volume %v: %v", state.Image, err)
        }
    }
}

func (ns *NodeServer) restoreUblkDaemons()
{
    pattern := ns.stateDir+"vitastor-ublk-*.json"
    stateFiles, err := filepath.Glob(pattern)
    if (err != nil)
    {
        klog.Errorf("failed to list %s: %v", pattern, err)
    }
    if (len(stateFiles) == 0)
    {
        return
    }
    for _, stateFile := range stateFiles
    {
        deviceNum := stateFile[len(ns.stateDir) + len("vitastor-ublk-") :]
        deviceNum = deviceNum[0:len(deviceNum)-5]
        ns.checkUblkState(deviceNum)
    }
}

func (ns *NodeServer) checkUblkState(deviceNum string)
{
    // Check if the ublk daemon is still active

    // Read state file
    stateFile := ns.stateDir + "vitastor-ublk-" + deviceNum + ".json"
    stateJSON, err := os.ReadFile(stateFile)
    if (err != nil)
    {
        klog.Warningf("error reading state file %v: %v", stateFile, err)
        return
    }
    var state DeviceState
    err = json.Unmarshal(stateJSON, &state)
    if (err != nil)
    {
        klog.Warningf("state file %v contains invalid JSON (error %v): %v", stateFile, err, string(stateJSON))
        return
    }

    // Lock volume
    ns.lockVolume(state.ConfigPath+":block:"+state.Image)
    defer ns.unlockVolume(state.ConfigPath+":block:"+state.Image)

    // Recheck state file after locking
    _, err = os.ReadFile(stateFile)
    if (err != nil)
    {
        klog.Warningf("state file %v disappeared, skipping volume", stateFile)
        return
    }

    // Check if the vitastor-ublk process is still active
    pidFile := ns.stateDir + "vitastor-ublk-" + deviceNum + ".pid"
    exists := false
    proc, err := findByPidFile(pidFile)
    if (err == nil)
    {
        exists = proc.Signal(syscall.Signal(0)) == nil
    }
    if (!exists)
    {
        // Restart daemon
        klog.Warningf("recovering UBLK device /dev/ublkb%v for volume %v", deviceNum, state.Image)
        _, err = mapUblk(ns.stateDir, state.Image, state.ConfigPath, state.Readonly, "/dev/ublkb"+deviceNum)
        if (err != nil)
        {
            klog.Warningf("failed to recover ublk device for volume %v: %v", state.Image, err)
        }
    }
}

func (ns *NodeServer) restoreNfsDaemons()
{
    pattern := ns.stateDir+"vitastor-nfs-*.json"
    stateFiles, err := filepath.Glob(pattern)
    if (err != nil)
    {
        klog.Errorf("failed to list %s: %v", pattern, err)
    }
    if (len(stateFiles) == 0)
    {
        return
    }
    activeNFS, err := ns.listActiveNFS()
    if (err != nil)
    {
        return
    }
    // Check all state files and try to restore active mounts
    for _, stateFile := range stateFiles
    {
        ns.checkNfsState(stateFile, activeNFS)
    }
}

func (ns *NodeServer) readNfsState(stateFile string, allowNotExists bool) (*NfsState, error)
{
    stateJSON, err := os.ReadFile(stateFile)
    if (err != nil)
    {
        if (allowNotExists && os.IsNotExist(err))
        {
            return nil, nil
        }
        klog.Warningf("error reading state file %v: %v", stateFile, err)
        return nil, err
    }
    var state NfsState
    err = json.Unmarshal(stateJSON, &state)
    if (err != nil)
    {
        klog.Warningf("state file %v contains invalid JSON (error %v): %v", stateFile, err, string(stateJSON))
        return nil, err
    }
    return &state, nil
}

func (ns *NodeServer) checkNfsState(stateFile string, activeNfs map[int][]string)
{
    // Read state file
    state, err := ns.readNfsState(stateFile, false)
    if (err != nil)
    {
        return
    }
    // Lock FS
    ns.lockVolume(state.ConfigPath+":fs:"+state.FsName)
    defer ns.unlockVolume(state.ConfigPath+":fs:"+state.FsName)
    // Check if NFS at this port is still mounted
    pidFile := ns.stateDir + filepath.Base(stateFile)
    pidFile = pidFile[0:len(pidFile)-5] + ".pid"
    if (len(activeNfs[state.Port]) == 0)
    {
        // this is a stale state file, remove it
        klog.Warningf("state file %v contains stale mount at port %d, removing it", stateFile, state.Port)
        ns.stopNFS(stateFile, pidFile)
        return
    }
    // Check PID file
    exists := false
    proc, err := findByPidFile(pidFile)
    if (err == nil)
    {
        exists = proc.Signal(syscall.Signal(0)) == nil
    }
    if (!exists)
    {
        // Restart vitastor-nfs server
        klog.Warningf("restarting NFS server for FS %v at port %v", state.FsName, state.Port)
        _, _, err := system(
            "/usr/bin/vitastor-nfs", "start",
            "--pidfile", pidFile,
            "--bind", "127.0.0.1",
            "--port", fmt.Sprintf("%d", state.Port),
            "--fs", state.FsName,
            "--pool", state.Pool,
            "--portmap", "0",
        )
        if (err != nil)
        {
            klog.Warningf("failed to restart NFS server for FS %v: %v", state.FsName, err)
        }
    }
}

// NodeStageVolume mounts the volume to a staging path on the node.
func (ns *NodeServer) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error)
{
    klog.Infof("received node stage volume request %+v", protosanitizer.StripSecrets(req))

    ctxVars := make(map[string]string)
    err := json.Unmarshal([]byte(req.VolumeId), &ctxVars)
    if (err != nil)
    {
        return nil, status.Error(codes.Internal, "volume ID not in JSON format")
    }
    _, err = GetConnectionParams(ctxVars)
    if (err != nil)
    {
        return nil, err
    }
    volName := ctxVars["name"]

    if (ctxVars["vitastorfs"] != "")
    {
        return &csi.NodeStageVolumeResponse{}, nil
    }

    ns.lockVolume(ctxVars["configPath"]+":block:"+volName)
    defer ns.unlockVolume(ctxVars["configPath"]+":block:"+volName)

    targetPath := req.GetStagingTargetPath()
    isBlock := req.GetVolumeCapability().GetBlock() != nil

    // Check that it's not already mounted
    notmnt, err := mount.IsNotMountPoint(ns.mounter, targetPath)
    if (err == nil)
    {
        if (!notmnt)
        {
            klog.Errorf("target path %s is already mounted", targetPath)
            return nil, fmt.Errorf("target path %s is already mounted", targetPath)
        }
        var finfo os.FileInfo
        finfo, err = os.Stat(targetPath)
        if (err != nil)
        {
            klog.Errorf("failed to stat %s: %v", targetPath, err)
            return nil, err
        }
        if (finfo.IsDir() != (!isBlock))
        {
            err = os.Remove(targetPath)
            if (err != nil)
            {
                klog.Errorf("failed to remove %s (to recreate it with correct type): %v", targetPath, err)
                return nil, err
            }
            err = os.ErrNotExist
        }
    }
    if (err != nil)
    {
        if (os.IsNotExist(err))
        {
            if (isBlock)
            {
                pathFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, 0o600)
                if (err != nil)
                {
                    klog.Errorf("failed to create block device mount target %s with error: %v", targetPath, err)
                    return nil, err
                }
                err = pathFile.Close()
                if (err != nil)
                {
                    klog.Errorf("failed to close %s with error: %v", targetPath, err)
                    return nil, err
                }
            }
            else
            {
                err := os.MkdirAll(targetPath, 0777)
                if (err != nil)
                {
                    klog.Errorf("failed to create fs mount target %s with error: %v", targetPath, err)
                    return nil, err
                }
            }
        }
        else
        {
            return nil, err
        }
    }

    var devicePath, vdpaId string
    if (ns.method == MOUNT_UBLK)
    {
        devicePath, err = mapUblk(ns.stateDir, volName, ctxVars["configPath"], false, "")
    }
    else if (ns.method == MOUNT_VDUSE)
    {
        devicePath, vdpaId, err = mapVduse(ns.stateDir, volName, ctxVars, false)
    }
    else /* if (ns.method == MOUNT_NBD) */
    {
        devicePath, err = mapNbd(volName, ctxVars, false)
    }
    if (err != nil)
    {
        return nil, err
    }

    diskMounter := &mount.SafeFormatAndMount{Interface: ns.mounter, Exec: utilexec.New()}
    if (isBlock)
    {
        klog.Infof("bind-mounting %s to %s", devicePath, targetPath)
        err = diskMounter.Mount(devicePath, targetPath, "", []string{"bind"})
    }
    else
    {
        // Check existing format
        var existingFormat string
        existingFormat, err = diskMounter.GetDiskFormat(devicePath)
        if (err != nil)
        {
            klog.Errorf("failed to get disk format for path %s, error: %v", err)
            goto unmap
        }

        // Format the device (ext4 or xfs)
        fsType := req.GetVolumeCapability().GetMount().GetFsType()
        opt := req.GetVolumeCapability().GetMount().GetMountFlags()
        opt = append(opt, "_netdev")
        if ((req.VolumeCapability.AccessMode.Mode == csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY ||
            req.VolumeCapability.AccessMode.Mode == csi.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY) &&
            !Contains(opt, "ro"))
        {
            opt = append(opt, "ro")
        }
        if (fsType == "xfs")
        {
            opt = append(opt, "nouuid")
        }
        readOnly := Contains(opt, "ro")
        if (existingFormat == "" && !readOnly)
        {
            switch fsType
            {
                case "ext4":
                    args := []string{"-m0", "-Enodiscard,lazy_itable_init=1,lazy_journal_init=1", devicePath}
                    _, err = systemCombined("mkfs.ext4", args...)
                case "xfs":
                    _, err = systemCombined("mkfs.xfs", "-K", devicePath)
            }
            if (err != nil)
            {
                goto unmap
            }
        }

        klog.Infof("formatting and mounting %s to %s with FS %s, options: %v", devicePath, targetPath, fsType, opt)
        err = diskMounter.FormatAndMount(devicePath, targetPath, fsType, opt)
        if (err == nil)
        {
            klog.Infof("successfully mounted %s to %s", devicePath, targetPath)
        }

        // Try to run online resize on mount.
        // FIXME: Implement online resize. It requires online resize support in vitastor-nbd.
        if (err == nil && existingFormat != "" && !readOnly)
        {
            switch (fsType)
            {
                case "ext4":
                    _, err = systemCombined("resize2fs", devicePath)
                case "xfs":
                    _, err = systemCombined("xfs_growfs", devicePath)
            }
        }
    }
    if (err != nil)
    {
        klog.Errorf(
            "failed to mount device path (%s) to path (%s) for volume (%s) error: %s",
            devicePath, targetPath, volName, err,
        )
        goto unmap
    }
    return &csi.NodeStageVolumeResponse{}, nil

unmap:
    if (ns.method == MOUNT_UBLK)
    {
        unmapUblk(ns.stateDir, devicePath)
    }
    else if (ns.method == MOUNT_VDUSE)
    {
        unmapVduseById(ns.stateDir, vdpaId)
    }
    else /* if (ns.method == MOUNT_NBD) */
    {
        unmapNbd(devicePath)
    }
    return nil, err
}

// NodeUnstageVolume unstages the volume from the staging path
func (ns *NodeServer) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error)
{
    klog.Infof("received node unstage volume request %+v", protosanitizer.StripSecrets(req))

    ctxVars := make(map[string]string)
    err := json.Unmarshal([]byte(req.VolumeId), &ctxVars)
    if (err != nil)
    {
        return nil, status.Error(codes.Internal, "volume ID not in JSON format")
    }
    volName := ctxVars["name"]

    if (ctxVars["vitastorfs"] != "")
    {
        return &csi.NodeUnstageVolumeResponse{}, nil
    }

    ns.lockVolume(ctxVars["configPath"]+":block:"+volName)
    defer ns.unlockVolume(ctxVars["configPath"]+":block:"+volName)

    targetPath := req.GetStagingTargetPath()
    devicePath, err := GetDeviceNameFromMount(targetPath)
    if (err != nil)
    {
        if (os.IsNotExist(err))
        {
            return nil, status.Error(codes.NotFound, "Target path not found")
        }
        return nil, err
    }
    if (devicePath == "")
    {
        // volume not mounted
        klog.Warningf("%s is not a mountpoint, deleting", targetPath)
        os.Remove(targetPath)
        return &csi.NodeUnstageVolumeResponse{}, nil
    }

    refList, err := ns.mounter.GetMountRefs(targetPath)
    if (err != nil)
    {
        return nil, err
    }
    if (len(refList) > 0)
    {
        klog.Warningf("%s is still referenced: %v", targetPath, refList)
    }

    // unmount
    err = mount.CleanupMountPoint(targetPath, ns.mounter, false)
    if (err != nil)
    {
        return nil, err
    }

    // unmap device
    if (len(refList) == 0)
    {
        if (ns.method == MOUNT_UBLK)
        {
            unmapUblk(ns.stateDir, devicePath)
        }
        else if (ns.method == MOUNT_VDUSE)
        {
            unmapVduse(ns.stateDir, devicePath)
        }
        else /* if (ns.method == MOUNT_NBD) */
        {
            unmapNbd(devicePath)
        }
    }

    return &csi.NodeUnstageVolumeResponse{}, nil
}

// Mount or check if NFS is already mounted
func (ns *NodeServer) mountNFS(ctxVars map[string]string) (string, error)
{
    sum := sha1.Sum([]byte(ctxVars["configPath"]+":fs:"+ctxVars["vitastorfs"]))
    nfsHash := hex.EncodeToString(sum[:])
    stateFile := ns.stateDir+"vitastor-nfs-"+nfsHash+".json"
    pidFile := ns.stateDir+"vitastor-nfs-"+nfsHash+".pid"
    mountPath := ns.nfsStageDir+"/"+nfsHash
    state, err := ns.readNfsState(stateFile, true)
    if (state != nil)
    {
        return state.Path, nil
    }
    if (err != nil)
    {
        return "", err
    }
    err = os.MkdirAll(mountPath, 0777)
    if (err != nil)
    {
        return "", err
    }
    // Create a new mount
    state = &NfsState{
        ConfigPath: ctxVars["configPath"],
        FsName:     ctxVars["vitastorfs"],
        Pool:       ctxVars["pool"],
        Path:       mountPath,
    }
    klog.Infof("starting new NFS server for FS %v", state.FsName)
    stdout, _, err := system(
        "/usr/bin/vitastor-nfs", "start",
        "--pidfile", pidFile,
        "--bind", "127.0.0.1",
        "--port", "auto",
        "--fs", state.FsName,
        "--pool", state.Pool,
        "--portmap", "0",
    )
    if (err != nil)
    {
        return "", err
    }
    match := regexp.MustCompile("Port: (\\d+)").FindStringSubmatch(string(stdout))
    if (match == nil)
    {
        klog.Errorf("failed to find port in vitastor-nfs output: %v", string(stdout))
        ns.stopNFS(stateFile, pidFile)
        return "", fmt.Errorf("failed to find port in vitastor-nfs output (bad vitastor-nfs version?)")
    }
    port, _ := strconv.ParseUint(match[1], 0, 16)
    state.Port = int(port)
    // Write state file
    stateJSON, _ := json.Marshal(state)
    err = os.WriteFile(stateFile, stateJSON, 0600)
    if (err != nil)
    {
        klog.Errorf("failed to write state file %v", stateFile)
        ns.stopNFS(stateFile, pidFile)
        return "", err
    }
    // Mount NFS
    _, _, err = system(
        "mount", "-t", "nfs", "127.0.0.1:/", state.Path,
        "-o", fmt.Sprintf("port=%d,mountport=%d,nfsvers=3,soft,nolock,tcp", port, port),
    )
    if (err != nil)
    {
        ns.stopNFS(stateFile, pidFile)
        return "", err
    }
    return state.Path, nil
}

// Mount or check if NFS is already mounted
func (ns *NodeServer) checkStopNFS(ctxVars map[string]string)
{
    sum := sha1.Sum([]byte(ctxVars["configPath"]+":fs:"+ctxVars["vitastorfs"]))
    nfsHash := hex.EncodeToString(sum[:])
    stateFile := ns.stateDir+"vitastor-nfs-"+nfsHash+".json"
    pidFile := ns.stateDir+"vitastor-nfs-"+nfsHash+".pid"
    mountPath := ns.nfsStageDir+"/"+nfsHash
    state, err := ns.readNfsState(stateFile, true)
    if (state == nil)
    {
        return
    }
    activeNFS, err := ns.listActiveNFS()
    if (err != nil)
    {
        return
    }
    if (len(activeNFS[state.Port]) > 0)
    {
        return
    }
    // All volume mounts are detached, unmount the root mount and kill the server
    err = mount.CleanupMountPoint(mountPath, ns.mounter, false)
    if (err != nil)
    {
        klog.Errorf("failed to unmount %v: %v", mountPath, err)
        return
    }
    ns.stopNFS(stateFile, pidFile)
}

func (ns *NodeServer) stopNFS(stateFile, pidFile string)
{
    err := killByPidFile(pidFile)
    if (err != nil)
    {
        klog.Errorf("failed to kill process with pid from %v: %v", pidFile, err)
    }
    os.Remove(pidFile)
    os.Remove(stateFile)
}

func (ns *NodeServer) listActiveNFS() (map[int][]string, error)
{
    mounts, err := mount.ParseMountInfo("/proc/self/mountinfo")
    if (err != nil)
    {
        klog.Errorf("failed to list mounts: %v", err)
        return nil, err
    }
    activeNFS := make(map[int][]string)
    for _, mount := range mounts
    {
        // Volume mounts always refer to subpaths
        if (mount.FsType == "nfs" && mount.Root != "/")
        {
            for _, opt := range mount.MountOptions
            {
                if (strings.HasPrefix(opt, "port="))
                {
                    port64, err := strconv.ParseUint(opt[5:], 10, 16)
                    if (err == nil)
                    {
                        activeNFS[int(port64)] = append(activeNFS[int(port64)], mount.MountPoint)
                    }
                }
            }
        }
    }
    return activeNFS, nil
}

// NodePublishVolume mounts the volume mounted to the staging path to the target path
func (ns *NodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error)
{
    klog.Infof("received node publish volume request %+v", protosanitizer.StripSecrets(req))

    ctxVars := make(map[string]string)
    err := json.Unmarshal([]byte(req.VolumeId), &ctxVars)
    if (err != nil)
    {
        return nil, status.Error(codes.Internal, "volume ID not in JSON format")
    }
    _, err = GetConnectionParams(ctxVars)
    if (err != nil)
    {
        return nil, err
    }
    volName := ctxVars["name"]

    if (ctxVars["vitastorfs"] != "")
    {
        ns.lockVolume(ctxVars["configPath"]+":fs:"+ctxVars["vitastorfs"])
        defer ns.unlockVolume(ctxVars["configPath"]+":fs:"+ctxVars["vitastorfs"])
    }
    else
    {
        ns.lockVolume(ctxVars["configPath"]+":block:"+volName)
        defer ns.unlockVolume(ctxVars["configPath"]+":block:"+volName)
    }

    stagingTargetPath := req.GetStagingTargetPath()
    targetPath := req.GetTargetPath()
    isBlock := req.GetVolumeCapability().GetBlock() != nil

    if (ctxVars["vitastorfs"] == "")
    {
        // Check that stagingTargetPath is mounted
        notmnt, err := mount.IsNotMountPoint(ns.mounter, stagingTargetPath)
        if (err != nil)
        {
            klog.Errorf("staging path %v is not mounted: %w", stagingTargetPath, err)
            return nil, fmt.Errorf("staging path %v is not mounted: %w", stagingTargetPath, err)
        }
        else if (notmnt)
        {
            klog.Errorf("staging path %v is not mounted", stagingTargetPath)
            return nil, fmt.Errorf("staging path %v is not mounted", stagingTargetPath)
        }
    }

    // Check that targetPath is not already mounted
    notmnt, err := mount.IsNotMountPoint(ns.mounter, targetPath)
    if (err != nil)
    {
        if (os.IsNotExist(err))
        {
            if (isBlock)
            {
                pathFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, 0o600)
                if (err != nil)
                {
                    klog.Errorf("failed to create block device mount target %s with error: %v", targetPath, err)
                    return nil, err
                }
                err = pathFile.Close()
                if (err != nil)
                {
                    klog.Errorf("failed to close %s with error: %v", targetPath, err)
                    return nil, err
                }
            }
            else
            {
                err := os.MkdirAll(targetPath, 0777)
                if (err != nil)
                {
                    klog.Errorf("failed to create fs mount target %s with error: %v", targetPath, err)
                    return nil, err
                }
            }
        }
        else
        {
            return nil, err
        }
    }
    else if (!notmnt)
    {
        klog.Errorf("target path %s is already mounted", targetPath)
        return nil, fmt.Errorf("target path %s is already mounted", targetPath)
    }

    if (ctxVars["vitastorfs"] != "")
    {
        nfspath, err := ns.mountNFS(ctxVars)
        if (err != nil)
        {
            ns.checkStopNFS(ctxVars)
            return nil, err
        }
        // volName should include prefix
        stagingTargetPath = nfspath+"/"+volName
        err = os.MkdirAll(stagingTargetPath, 0777)
        if (err != nil && !os.IsExist(err))
        {
            ns.checkStopNFS(ctxVars)
            return nil, err
        }
    }

    execArgs := []string{"--bind", stagingTargetPath, targetPath}
    if (req.GetReadonly())
    {
        execArgs = append(execArgs, "-o", "ro")
    }
    cmd := exec.Command("mount", execArgs...)
    cmd.Stderr = os.Stderr
    klog.Infof("binding volume %v (%v) from %v to %v", volName, ctxVars["configPath"], stagingTargetPath, targetPath)
    out, err := cmd.Output()
    if (err != nil)
    {
        if (ctxVars["vitastorfs"] != "")
        {
            ns.checkStopNFS(ctxVars)
        }
        return nil, fmt.Errorf("Error running mount %v: %s", strings.Join(execArgs, " "), out)
    }

    return &csi.NodePublishVolumeResponse{}, nil
}

// NodeUnpublishVolume unmounts the volume from the target path
func (ns *NodeServer) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error)
{
    klog.Infof("received node unpublish volume request %+v", protosanitizer.StripSecrets(req))

    ctxVars := make(map[string]string)
    err := json.Unmarshal([]byte(req.VolumeId), &ctxVars)
    if (err != nil)
    {
        return nil, status.Error(codes.Internal, "volume ID not in JSON format")
    }
    volName := ctxVars["name"]

    if (ctxVars["vitastorfs"] != "")
    {
        ns.lockVolume(ctxVars["configPath"]+":fs:"+ctxVars["vitastorfs"])
        defer ns.unlockVolume(ctxVars["configPath"]+":fs:"+ctxVars["vitastorfs"])
    }
    else
    {
        ns.lockVolume(ctxVars["configPath"]+":block:"+volName)
        defer ns.unlockVolume(ctxVars["configPath"]+":block:"+volName)
    }

    targetPath := req.GetTargetPath()
    devicePath, err := GetDeviceNameFromMount(targetPath)
    if (err != nil)
    {
        if (os.IsNotExist(err))
        {
            return nil, status.Error(codes.NotFound, "Target path not found")
        }
        return nil, err
    }
    if (devicePath == "")
    {
        // volume not mounted
        klog.Warningf("%s is not a mountpoint, deleting", targetPath)
        os.Remove(targetPath)
        return &csi.NodeUnpublishVolumeResponse{}, nil
    }

    // unmount
    err = mount.CleanupMountPoint(targetPath, ns.mounter, false)
    if (err != nil)
    {
        return nil, err
    }

    if (ctxVars["vitastorfs"] != "")
    {
        ns.checkStopNFS(ctxVars)
    }

    return &csi.NodeUnpublishVolumeResponse{}, nil
}

// NodeGetVolumeStats returns volume capacity statistics available for the volume
func (ns *NodeServer) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error)
{
    return nil, status.Error(codes.Unimplemented, "")
}

// NodeExpandVolume expanding the file system on the node
func (ns *NodeServer) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error)
{
    return nil, status.Error(codes.Unimplemented, "")
}

// NodeGetCapabilities returns the supported capabilities of the node server
func (ns *NodeServer) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error)
{
    return &csi.NodeGetCapabilitiesResponse{
        Capabilities: []*csi.NodeServiceCapability{
            &csi.NodeServiceCapability{
                Type: &csi.NodeServiceCapability_Rpc{
                    Rpc: &csi.NodeServiceCapability_RPC{
                        Type: csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME,
                    },
                },
            },
        },
    }, nil
}

// NodeGetInfo returns NodeGetInfoResponse for CO.
func (ns *NodeServer) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error)
{
    klog.Infof("received node get info request %+v", protosanitizer.StripSecrets(req))
    return &csi.NodeGetInfoResponse{
        NodeId: ns.NodeID,
    }, nil
}
