mirror of
https://github.com/SynologyOpenSource/synology-csi.git
synced 2026-02-13 21:00:03 +00:00
Implement a tool executor
The tool executor is capable of running given tools in chroot either by automatically locating the tools in path using the `env` command or directly if the path is known. This removes the need for the shell script to wrap commands and reduces number of needed external binaries on the node.
This commit is contained in:
committed by
Sami Haahtinen
parent
e2cc8de2fa
commit
78e7a08c0e
@@ -28,14 +28,6 @@ RUN apk add --no-cache e2fsprogs e2fsprogs-extra xfsprogs xfsprogs-extra blkid u
|
||||
|
||||
# Create symbolic link for chroot.sh
|
||||
WORKDIR /
|
||||
RUN mkdir /csibin
|
||||
COPY chroot/chroot.sh /csibin
|
||||
RUN chmod 777 /csibin/chroot.sh \
|
||||
&& ln -s /csibin/chroot.sh /csibin/iscsiadm \
|
||||
&& ln -s /csibin/chroot.sh /csibin/multipath \
|
||||
&& ln -s /csibin/chroot.sh /csibin/multipathd
|
||||
|
||||
ENV PATH="/csibin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
# Copy and run CSI driver
|
||||
COPY --from=builder /go/src/synok8scsiplugin/bin/synology-csi-driver synology-csi-driver
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# This script is only used in the container, see Dockerfile.
|
||||
|
||||
DIR="/host" # csi-node mount / of the node to /host in the container
|
||||
BIN="$(basename "$0")"
|
||||
|
||||
if [ -d "$DIR" ]; then
|
||||
exec chroot $DIR /usr/bin/env -i PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" "$BIN" "$@"
|
||||
fi
|
||||
|
||||
echo -n "Couldn't find hostPath: $DIR in the CSI container"
|
||||
exit 1
|
||||
40
main.go
40
main.go
@@ -8,13 +8,15 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/driver"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/common"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/service"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/logger"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/utils/hostexec"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,16 +25,21 @@ var (
|
||||
csiEndpoint = "unix:///var/lib/kubelet/plugins/" + driver.DriverName + "/csi.sock"
|
||||
csiClientInfoPath = "/etc/synology/client-info.yml"
|
||||
// Logging
|
||||
logLevel = "info"
|
||||
webapiDebug = false
|
||||
logLevel = "info"
|
||||
webapiDebug = false
|
||||
multipathForUC = true
|
||||
// Locations is tools and directories
|
||||
chrootDir = "/host"
|
||||
iscsiadmPath = ""
|
||||
multipathPath = ""
|
||||
multipathdPath = ""
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "synology-csi-driver",
|
||||
Short: "Synology CSI Driver",
|
||||
Use: "synology-csi-driver",
|
||||
Short: "Synology CSI Driver",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
if webapiDebug {
|
||||
logger.WebapiDebug = true
|
||||
logLevel = "debug"
|
||||
@@ -72,8 +79,21 @@ func driverStart() error {
|
||||
}
|
||||
defer dsmService.RemoveAllDsms()
|
||||
|
||||
// 2. Create and Run the Driver
|
||||
drv, err := driver.NewControllerAndNodeDriver(csiNodeID, csiEndpoint, dsmService)
|
||||
// 2. Create command executor
|
||||
cmdMap := map[string]string{
|
||||
"iscsiadm": iscsiadmPath,
|
||||
"multipath": multipathPath,
|
||||
"multipathd": multipathdPath,
|
||||
}
|
||||
cmdExecutor, err := hostexec.New(cmdMap, chrootDir)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create command executor: %v", err)
|
||||
return err
|
||||
}
|
||||
tools := driver.NewTools(cmdExecutor)
|
||||
|
||||
// 3. Create and Run the Driver
|
||||
drv, err := driver.NewControllerAndNodeDriver(csiNodeID, csiEndpoint, dsmService, tools)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create driver: %v", err)
|
||||
return err
|
||||
@@ -105,6 +125,10 @@ func addFlags(cmd *cobra.Command) {
|
||||
cmd.PersistentFlags().StringVar(&logLevel, "log-level", logLevel, "Log level (debug, info, warn, error, fatal)")
|
||||
cmd.PersistentFlags().BoolVarP(&webapiDebug, "debug", "d", webapiDebug, "Enable webapi debugging logs")
|
||||
cmd.PersistentFlags().BoolVar(&multipathForUC, "multipath", multipathForUC, "Set to 'false' to disable multipath for UC")
|
||||
cmd.PersistentFlags().StringVar(&chrootDir, "chroot-dir", chrootDir, "Host directory to chroot into (empty disables chroot)")
|
||||
cmd.PersistentFlags().StringVar(&iscsiadmPath, "iscsiadm-path", iscsiadmPath, "Full path of iscsiadm executable")
|
||||
cmd.PersistentFlags().StringVar(&multipathPath, "multipath-path", multipathPath, "Full path of multipath executable")
|
||||
cmd.PersistentFlags().StringVar(&multipathdPath, "multipathd-path", multipathdPath, "Full path of multipathd executable")
|
||||
|
||||
cmd.MarkFlagRequired("endpoint")
|
||||
cmd.MarkFlagRequired("client-info")
|
||||
|
||||
@@ -17,19 +17,19 @@ limitations under the License.
|
||||
package driver
|
||||
|
||||
import (
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/interfaces"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/utils"
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
DriverName = "csi.san.synology.com" // CSI dirver name
|
||||
DriverName = "csi.san.synology.com" // CSI dirver name
|
||||
DriverVersion = "1.2.0"
|
||||
)
|
||||
|
||||
var (
|
||||
MultipathEnabled = true
|
||||
MultipathEnabled = true
|
||||
supportedProtocolList = []string{utils.ProtocolIscsi, utils.ProtocolSmb, utils.ProtocolNfs}
|
||||
allowedNfsVersionList = []string{"3", "4", "4.0", "4.1"}
|
||||
)
|
||||
@@ -44,13 +44,14 @@ type Driver struct {
|
||||
nodeID string
|
||||
version string
|
||||
endpoint string
|
||||
tools tools
|
||||
csCap []*csi.ControllerServiceCapability
|
||||
vCap []*csi.VolumeCapability_AccessMode
|
||||
nsCap []*csi.NodeServiceCapability
|
||||
DsmService interfaces.IDsmService
|
||||
}
|
||||
|
||||
func NewControllerAndNodeDriver(nodeID string, endpoint string, dsmService interfaces.IDsmService) (*Driver, error) {
|
||||
func NewControllerAndNodeDriver(nodeID string, endpoint string, dsmService interfaces.IDsmService, tools tools) (*Driver, error) {
|
||||
log.Debugf("NewControllerAndNodeDriver: DriverName: %v, DriverVersion: %v", DriverName, DriverVersion)
|
||||
|
||||
// TODO version format and validation
|
||||
@@ -60,6 +61,7 @@ func NewControllerAndNodeDriver(nodeID string, endpoint string, dsmService inter
|
||||
nodeID: nodeID,
|
||||
endpoint: endpoint,
|
||||
DsmService: dsmService,
|
||||
tools: tools,
|
||||
}
|
||||
|
||||
d.addControllerServiceCapabilities([]csi.ControllerServiceCapability_RPC_Type{
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/utils/hostexec"
|
||||
log "github.com/sirupsen/logrus"
|
||||
utilexec "k8s.io/utils/exec"
|
||||
)
|
||||
@@ -31,6 +32,7 @@ import (
|
||||
type initiatorDriver struct {
|
||||
chapUser string
|
||||
chapPassword string
|
||||
tools tools
|
||||
}
|
||||
|
||||
type iscsiSession struct {
|
||||
@@ -45,10 +47,19 @@ const (
|
||||
ISCSIPort = 3260
|
||||
)
|
||||
|
||||
func iscsiadm(cmdArgs ...string) utilexec.Cmd {
|
||||
executor := utilexec.New()
|
||||
type tools struct {
|
||||
executor hostexec.Executor
|
||||
}
|
||||
|
||||
return executor.Command("iscsiadm", cmdArgs...)
|
||||
// NewTools creates a new tools wrapper for calling utilities with given executor
|
||||
func NewTools(executor hostexec.Executor) tools {
|
||||
return tools{
|
||||
executor: executor,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tools) iscsiadm(cmdArgs ...string) utilexec.Cmd {
|
||||
return t.executor.Command("iscsiadm", cmdArgs...)
|
||||
}
|
||||
|
||||
// parseSession takes the raw stdout from the `iscsiadm -m session` command and encodes it into an iSCSI session type
|
||||
@@ -81,8 +92,8 @@ func parseSessions(lines string) []iscsiSession {
|
||||
return sessions
|
||||
}
|
||||
|
||||
func iscsiadm_session() []iscsiSession {
|
||||
cmd := iscsiadm("-m", "session")
|
||||
func (t *tools) iscsiadm_session() []iscsiSession {
|
||||
cmd := t.iscsiadm("-m", "session")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
exitErr, ok := err.(utilexec.ExitError)
|
||||
@@ -97,8 +108,8 @@ func iscsiadm_session() []iscsiSession {
|
||||
return parseSessions(string(out))
|
||||
}
|
||||
|
||||
func iscsiadm_discovery(portal string) error {
|
||||
cmd := iscsiadm(
|
||||
func (t *tools) iscsiadm_discovery(portal string) error {
|
||||
cmd := t.iscsiadm(
|
||||
"-m", "discoverydb",
|
||||
"--type", "sendtargets",
|
||||
"--portal", portal,
|
||||
@@ -110,8 +121,8 @@ func iscsiadm_discovery(portal string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func iscsiadm_login(iqn, portal string) error {
|
||||
cmd := iscsiadm(
|
||||
func (t *tools) iscsiadm_login(iqn, portal string) error {
|
||||
cmd := t.iscsiadm(
|
||||
"-m", "node",
|
||||
"--targetname", iqn,
|
||||
"--portal", portal,
|
||||
@@ -123,8 +134,8 @@ func iscsiadm_login(iqn, portal string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func iscsiadm_update_node_startup(iqn, portal string) error {
|
||||
cmd := iscsiadm(
|
||||
func (t *tools) iscsiadm_update_node_startup(iqn, portal string) error {
|
||||
cmd := t.iscsiadm(
|
||||
"-m", "node",
|
||||
"--targetname", iqn,
|
||||
"--portal", portal,
|
||||
@@ -138,8 +149,8 @@ func iscsiadm_update_node_startup(iqn, portal string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func iscsiadm_logout(iqn string) error {
|
||||
cmd := iscsiadm(
|
||||
func (t *tools) iscsiadm_logout(iqn string) error {
|
||||
cmd := t.iscsiadm(
|
||||
"-m", "node",
|
||||
"--targetname", iqn,
|
||||
"--logout")
|
||||
@@ -149,8 +160,8 @@ func iscsiadm_logout(iqn string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func iscsiadm_rescan(iqn string) error {
|
||||
cmd := iscsiadm(
|
||||
func (t *tools) iscsiadm_rescan(iqn string) error {
|
||||
cmd := t.iscsiadm(
|
||||
"-m", "node",
|
||||
"--targetname", iqn,
|
||||
"-R")
|
||||
@@ -160,8 +171,8 @@ func iscsiadm_rescan(iqn string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasSession(targetIqn string, portal string) bool {
|
||||
sessions := iscsiadm_session()
|
||||
func (t *tools) hasSession(targetIqn string, portal string) bool {
|
||||
sessions := t.iscsiadm_session()
|
||||
|
||||
for _, s := range sessions {
|
||||
if targetIqn == s.Iqn && (portal == s.Portal || portal == "") {
|
||||
@@ -172,8 +183,8 @@ func hasSession(targetIqn string, portal string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func listSessionsByIqn(targetIqn string) (matchedSessions []iscsiSession) {
|
||||
sessions := iscsiadm_session()
|
||||
func (t *tools) listSessionsByIqn(targetIqn string) (matchedSessions []iscsiSession) {
|
||||
sessions := t.iscsiadm_session()
|
||||
|
||||
for _, s := range sessions {
|
||||
if targetIqn == s.Iqn {
|
||||
@@ -185,22 +196,22 @@ func listSessionsByIqn(targetIqn string) (matchedSessions []iscsiSession) {
|
||||
}
|
||||
|
||||
func (d *initiatorDriver) login(targetIqn string, portal string) error {
|
||||
if hasSession(targetIqn, portal) {
|
||||
if d.tools.hasSession(targetIqn, portal) {
|
||||
log.Infof("Session[%s] already exists.", targetIqn)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := iscsiadm_discovery(portal); err != nil {
|
||||
if err := d.tools.iscsiadm_discovery(portal); err != nil {
|
||||
log.Errorf("Failed in discovery of the target: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := iscsiadm_login(targetIqn, portal); err != nil {
|
||||
if err := d.tools.iscsiadm_login(targetIqn, portal); err != nil {
|
||||
log.Errorf("Failed in login of the target: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := iscsiadm_update_node_startup(targetIqn, portal); err != nil {
|
||||
if err := d.tools.iscsiadm_update_node_startup(targetIqn, portal); err != nil {
|
||||
log.Warnf("Failed to update target node.startup to manual: %v", err)
|
||||
}
|
||||
|
||||
@@ -210,13 +221,13 @@ func (d *initiatorDriver) login(targetIqn string, portal string) error {
|
||||
}
|
||||
|
||||
func (d *initiatorDriver) logout(targetIqn string, ip string) error {
|
||||
if !hasSession(targetIqn, "") {
|
||||
if !d.tools.hasSession(targetIqn, "") {
|
||||
log.Infof("Session[%s] doesn't exist.", targetIqn)
|
||||
return nil
|
||||
}
|
||||
|
||||
portal := fmt.Sprintf("%s:%d", ip, ISCSIPort)
|
||||
if err := iscsiadm_logout(targetIqn); err != nil {
|
||||
if err := d.tools.iscsiadm_logout(targetIqn); err != nil {
|
||||
log.Errorf("Failed in logout of the target.\nTarget [%s], Portal [%s], Err[%v]",
|
||||
targetIqn, portal, err)
|
||||
return err
|
||||
@@ -228,13 +239,13 @@ func (d *initiatorDriver) logout(targetIqn string, ip string) error {
|
||||
}
|
||||
|
||||
func (d *initiatorDriver) rescan(targetIqn string) error {
|
||||
if !hasSession(targetIqn, "") {
|
||||
if !d.tools.hasSession(targetIqn, "") {
|
||||
msg := fmt.Sprintf("Session[%s] doesn't exist.", targetIqn)
|
||||
log.Error(msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
if err := iscsiadm_rescan(targetIqn); err != nil {
|
||||
if err := d.tools.iscsiadm_rescan(targetIqn); err != nil {
|
||||
log.Errorf("Failed in rescan of the target.\nTarget [%s], Err[%v]",
|
||||
targetIqn, err)
|
||||
return err
|
||||
|
||||
@@ -21,21 +21,23 @@ package driver
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
utilexec "k8s.io/utils/exec"
|
||||
)
|
||||
|
||||
func IsMultipathEnabled() bool {
|
||||
return MultipathEnabled && multipathd_is_running()
|
||||
// IsMultipathEnabled returns true if multipath is enabled
|
||||
|
||||
func (t *tools) IsMultipathEnabled() bool {
|
||||
return MultipathEnabled && t.multipathd_is_running()
|
||||
}
|
||||
|
||||
func multipathd_is_running() bool {
|
||||
executor := utilexec.New()
|
||||
cmd := executor.Command("multipathd", "show", "daemon")
|
||||
func (t *tools) multipathd_is_running() bool {
|
||||
cmd := t.executor.Command("multipathd", "show", "daemon")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
matched, _ := regexp.MatchString(`pid \d+ (running|idle)`, string(out))
|
||||
@@ -49,15 +51,14 @@ func multipathd_is_running() bool {
|
||||
}
|
||||
|
||||
// execute a command with a timeout and returns an error if timeout is exceeded
|
||||
func execWithTimeout(command string, args []string, timeout time.Duration) ([]byte, error) {
|
||||
func (t *tools) execWithTimeout(command string, args []string, timeout time.Duration) ([]byte, error) {
|
||||
log.Infof("Executing command '%v' with args: '%v'.", command, args)
|
||||
|
||||
// Create a new context and add a timeout to it
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
executor := utilexec.New()
|
||||
cmd := executor.CommandContext(ctx, command, args...)
|
||||
cmd := t.executor.CommandContext(ctx, command, args...)
|
||||
out, err := cmd.Output()
|
||||
log.Debug(err)
|
||||
|
||||
@@ -81,9 +82,8 @@ func execWithTimeout(command string, args []string, timeout time.Duration) ([]by
|
||||
}
|
||||
|
||||
// resize a multipath device based on its underlying devices
|
||||
func multipath_resize(devName string) error {
|
||||
executor := utilexec.New()
|
||||
cmd := executor.Command("multipathd", "resize", "map", devName) // use devName not devPath, or it'll fail
|
||||
func (t *tools) multipath_resize(devName string) error {
|
||||
cmd := t.executor.Command("multipathd", "resize", "map", devName) // use devName not devPath, or it'll fail
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s (%v)", string(out), err)
|
||||
@@ -92,9 +92,9 @@ func multipath_resize(devName string) error {
|
||||
}
|
||||
|
||||
// flushes a multipath device dm-x with command multipath -f /dev/dm-x
|
||||
func multipath_flush(devPath string) error {
|
||||
func (t *tools) multipath_flush(devPath string) error {
|
||||
timeout := 5 * time.Second
|
||||
out, err := execWithTimeout("multipath", []string{"-f", devPath}, timeout)
|
||||
out, err := t.execWithTimeout("multipath", []string{"-f", devPath}, timeout)
|
||||
if err != nil {
|
||||
if _, e := os.Stat(devPath); os.IsNotExist(e) {
|
||||
log.Debugf("Multipath device %v has been removed.", devPath)
|
||||
|
||||
@@ -28,12 +28,12 @@ import (
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"golang.org/x/sys/unix"
|
||||
"k8s.io/mount-utils"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/mount-utils"
|
||||
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/webapi"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/interfaces"
|
||||
@@ -47,6 +47,7 @@ type nodeServer struct {
|
||||
dsmService interfaces.IDsmService
|
||||
Initiator *initiatorDriver
|
||||
Client clientset.Interface
|
||||
tools tools
|
||||
}
|
||||
|
||||
func waitForDevicePathToExist(path string) error {
|
||||
@@ -73,10 +74,10 @@ func waitForDevicePathToExist(path string) error {
|
||||
}
|
||||
|
||||
// for unstage, resize volume
|
||||
func getExistedVolumeMountPath(targetIqn string, mappingIndex int) string {
|
||||
func (t *tools) getExistedVolumeMountPath(targetIqn string, mappingIndex int) string {
|
||||
paths := []string{}
|
||||
|
||||
sessions := listSessionsByIqn(targetIqn)
|
||||
sessions := t.listSessionsByIqn(targetIqn)
|
||||
for _, session := range sessions {
|
||||
paths = append(paths, fmt.Sprintf("%sip-%s-iscsi-%s-lun-%d", "/dev/disk/by-path/", session.Portal, targetIqn, mappingIndex))
|
||||
}
|
||||
@@ -175,7 +176,7 @@ func (ns *nodeServer) getPortals(dsmIp string) []string {
|
||||
portals = append(portals, fmt.Sprintf("%s:%d", ips[0], ISCSIPort)) //get the first ip
|
||||
}
|
||||
|
||||
if dsm.IsUC() && IsMultipathEnabled() {
|
||||
if dsm.IsUC() && ns.tools.IsMultipathEnabled() {
|
||||
dsm2, err := dsm.GetAnotherController()
|
||||
if err != nil {
|
||||
log.Errorf("[%s] UC failed to get another controller: %v", err)
|
||||
@@ -228,10 +229,10 @@ func (ns *nodeServer) logoutTarget(volumeId string) {
|
||||
|
||||
// Assume target and lun 1-1 mapping
|
||||
mappingIndex := k8sVolume.Target.MappedLuns[0].MappingIndex
|
||||
volumeMountPath := getExistedVolumeMountPath(k8sVolume.Target.Iqn, mappingIndex)
|
||||
volumeMountPath := ns.tools.getExistedVolumeMountPath(k8sVolume.Target.Iqn, mappingIndex)
|
||||
|
||||
if strings.Contains(volumeMountPath, "/dev/mapper") && IsMultipathEnabled() {
|
||||
if err := multipath_flush(volumeMountPath); err != nil {
|
||||
if strings.Contains(volumeMountPath, "/dev/mapper") && ns.tools.IsMultipathEnabled() {
|
||||
if err := ns.tools.multipath_flush(volumeMountPath); err != nil {
|
||||
log.Errorf("Failed to remove multipath device in path %s. err: %v", volumeMountPath, err)
|
||||
}
|
||||
}
|
||||
@@ -567,7 +568,7 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis
|
||||
if req.VolumeContext["protocol"] == utils.ProtocolNfs {
|
||||
options = append(options, req.GetVolumeCapability().GetMount().GetMountFlags()...)
|
||||
|
||||
var server, baseDir string //NFSTODO: subDir
|
||||
var server, baseDir string //NFSTODO: subDir
|
||||
var mountPermissionsUint uint64 = 0750 // default
|
||||
for k, v := range req.GetVolumeContext() {
|
||||
switch k {
|
||||
@@ -804,13 +805,13 @@ func (ns *nodeServer) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandV
|
||||
|
||||
// Assume target and lun 1-1 mapping
|
||||
mappingIndex := k8sVolume.Target.MappedLuns[0].MappingIndex
|
||||
volumeMountPath := getExistedVolumeMountPath(k8sVolume.Target.Iqn, mappingIndex)
|
||||
volumeMountPath := ns.tools.getExistedVolumeMountPath(k8sVolume.Target.Iqn, mappingIndex)
|
||||
if volumeMountPath == "" {
|
||||
return nil, status.Error(codes.Internal, "Can't get volume mount path")
|
||||
}
|
||||
|
||||
if strings.Contains(volumeMountPath, "/dev/mapper") && IsMultipathEnabled() {
|
||||
if err := multipath_resize(filepath.Base(volumeMountPath)); err != nil {
|
||||
if strings.Contains(volumeMountPath, "/dev/mapper") && ns.tools.IsMultipathEnabled() {
|
||||
if err := ns.tools.multipath_resize(filepath.Base(volumeMountPath)); err != nil {
|
||||
return nil, status.Error(codes.Internal, fmt.Sprintf("Failed to resize multipath device in %s. err: %v", volumeMountPath, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,13 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/kubernetes-csi/csi-lib-utils/protosanitizer"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/utils/exec"
|
||||
"k8s.io/mount-utils"
|
||||
"k8s.io/utils/exec"
|
||||
)
|
||||
|
||||
func ParseEndpoint(ep string) (string, string, error) {
|
||||
@@ -45,7 +45,7 @@ func ParseEndpoint(ep string) (string, string, error) {
|
||||
|
||||
func NewControllerServer(d *Driver) *controllerServer {
|
||||
return &controllerServer{
|
||||
Driver: d,
|
||||
Driver: d,
|
||||
dsmService: d.DsmService,
|
||||
}
|
||||
}
|
||||
@@ -65,17 +65,19 @@ func getK8sClient() clientset.Interface {
|
||||
|
||||
func NewNodeServer(d *Driver) *nodeServer {
|
||||
return &nodeServer{
|
||||
Driver: d,
|
||||
Driver: d,
|
||||
dsmService: d.DsmService,
|
||||
Mounter: &mount.SafeFormatAndMount{
|
||||
Interface: mount.New(""),
|
||||
Exec: exec.New(),
|
||||
Exec: exec.New(),
|
||||
},
|
||||
Initiator: &initiatorDriver{
|
||||
chapUser: "",
|
||||
chapUser: "",
|
||||
chapPassword: "",
|
||||
tools: d.tools,
|
||||
},
|
||||
Client: getK8sClient(),
|
||||
tools: d.tools,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
99
pkg/utils/hostexec/hostexec.go
Normal file
99
pkg/utils/hostexec/hostexec.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Package hostexec automatically wraps commands executed with kubernetes hostexec into
|
||||
// chrooted commands
|
||||
package hostexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"k8s.io/utils/exec"
|
||||
)
|
||||
|
||||
// defaultSearchPath for running commands without absolute paths
|
||||
var defaultSearchPath = []string{
|
||||
"/usr/local/sbin",
|
||||
"/usr/local/bin",
|
||||
"/usr/sbin",
|
||||
"/usr/bin",
|
||||
"/sbin",
|
||||
"/bin",
|
||||
}
|
||||
|
||||
// Executor is mostly k8s.io/utils/exec compatible interface for the portions
|
||||
// that synology-csi uses.
|
||||
type Executor interface {
|
||||
Command(string, ...string) exec.Cmd
|
||||
CommandContext(context.Context, string, ...string) exec.Cmd
|
||||
}
|
||||
|
||||
type hostexec struct {
|
||||
Executor
|
||||
commandMap map[string]string
|
||||
chrootDir string
|
||||
}
|
||||
|
||||
// New creates an instance of hostexec to execute commands in the given environment
|
||||
func New(cmdMap map[string]string, chrootDir string) (Executor, error) {
|
||||
// If chroot directory is defined, check that directory exists or return an error
|
||||
if chrootDir != "" {
|
||||
fileinfo, err := os.Stat(chrootDir)
|
||||
if err != nil || !fileinfo.IsDir() {
|
||||
return nil, errors.New("chroot directory does not exist or is not a directory")
|
||||
}
|
||||
}
|
||||
|
||||
return &hostexec{exec.New(), cmdMap, chrootDir}, nil
|
||||
}
|
||||
|
||||
func (h *hostexec) resolveCmd(cmd string, args ...string) (string, []string) {
|
||||
c, ok := h.commandMap[cmd]
|
||||
if !ok || c == "" {
|
||||
return cmd, args
|
||||
}
|
||||
|
||||
return c, args
|
||||
}
|
||||
|
||||
func (h *hostexec) wrapEnv(cmd string, args ...string) (string, []string) {
|
||||
if strings.ContainsAny(cmd, "/") {
|
||||
return cmd, args
|
||||
}
|
||||
|
||||
sp := fmt.Sprintf("PATH=%s", strings.Join(defaultSearchPath, ":"))
|
||||
args = append([]string{"-i", sp, cmd}, args...)
|
||||
cmd = "/usr/bin/env"
|
||||
|
||||
return cmd, args
|
||||
}
|
||||
|
||||
func (h *hostexec) wrapChroot(cmd string, args ...string) (string, []string) {
|
||||
if h.chrootDir == "" {
|
||||
return cmd, args
|
||||
}
|
||||
|
||||
args = append([]string{h.chrootDir, cmd}, args...)
|
||||
cmd = "/usr/sbin/chroot"
|
||||
|
||||
return cmd, args
|
||||
}
|
||||
|
||||
func (h *hostexec) wrap(cmd string, args ...string) (string, []string) {
|
||||
cmd, args = h.resolveCmd(cmd, args...)
|
||||
cmd, args = h.wrapEnv(cmd, args...)
|
||||
cmd, args = h.wrapChroot(cmd, args...)
|
||||
|
||||
return cmd, args
|
||||
}
|
||||
|
||||
func (h *hostexec) Command(cmd string, args ...string) exec.Cmd {
|
||||
cmd, args = h.wrap(cmd, args...)
|
||||
return h.Executor.Command(cmd, args...)
|
||||
}
|
||||
|
||||
func (h *hostexec) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd {
|
||||
cmd, args = h.wrap(cmd, args...)
|
||||
return h.Executor.CommandContext(ctx, cmd, args...)
|
||||
}
|
||||
393
pkg/utils/hostexec/hostexec_test.go
Normal file
393
pkg/utils/hostexec/hostexec_test.go
Normal file
@@ -0,0 +1,393 @@
|
||||
package hostexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/utils/exec"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "chroot_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Temporary directory creation failed: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cmdMap map[string]string
|
||||
chrootDir string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid chroot directory",
|
||||
cmdMap: nil,
|
||||
chrootDir: tmpDir,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid chroot directory",
|
||||
cmdMap: nil,
|
||||
chrootDir: "/invalid/path",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := New(tt.cmdMap, tt.chrootDir)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostexec_wrapEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cmd string
|
||||
args []string
|
||||
wantCmd string
|
||||
wantArgs []string
|
||||
}{
|
||||
{
|
||||
name: "empty args",
|
||||
cmd: "echo",
|
||||
args: nil,
|
||||
wantCmd: "/usr/bin/env",
|
||||
wantArgs: []string{"-i", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "echo"},
|
||||
},
|
||||
{
|
||||
name: "non-empty args",
|
||||
cmd: "echo",
|
||||
args: []string{"hello", "world"},
|
||||
wantCmd: "/usr/bin/env",
|
||||
wantArgs: []string{"-i", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "echo", "hello", "world"},
|
||||
},
|
||||
{
|
||||
name: "fully qualified cmd without args",
|
||||
cmd: "/bin/echo",
|
||||
args: nil,
|
||||
wantCmd: "/bin/echo",
|
||||
wantArgs: nil,
|
||||
},
|
||||
{
|
||||
name: "fully qualified cmd with args",
|
||||
cmd: "/bin/echo",
|
||||
args: []string{"hello", "world"},
|
||||
wantCmd: "/bin/echo",
|
||||
wantArgs: []string{"hello", "world"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := &hostexec{}
|
||||
|
||||
c, a := h.wrapEnv(tt.cmd, tt.args...)
|
||||
if c != tt.wantCmd {
|
||||
t.Errorf("wrapEnv() c = %v, want %v", c, tt.wantCmd)
|
||||
}
|
||||
if !reflect.DeepEqual(a, tt.wantArgs) {
|
||||
t.Errorf("wrapEnv() a = %v, want %v", a, tt.wantArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostexec_resolveCmd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cmd string
|
||||
cmdArgs []string
|
||||
cmdMap map[string]string
|
||||
wantCmd string
|
||||
wantCmdArgs []string
|
||||
}{
|
||||
{
|
||||
name: "empty cmd map",
|
||||
cmd: "echo",
|
||||
cmdArgs: []string{"hello", "world"},
|
||||
cmdMap: nil,
|
||||
wantCmd: "echo",
|
||||
wantCmdArgs: []string{"hello", "world"},
|
||||
},
|
||||
{
|
||||
name: "non-empty cmd map",
|
||||
cmd: "echo",
|
||||
cmdArgs: []string{"hello", "world"},
|
||||
cmdMap: map[string]string{
|
||||
"echo": "/bin/echo",
|
||||
},
|
||||
wantCmd: "/bin/echo",
|
||||
wantCmdArgs: []string{"hello", "world"},
|
||||
},
|
||||
{
|
||||
name: "non-empty cmd map without matching command",
|
||||
cmd: "echo",
|
||||
cmdArgs: []string{"hello", "world"},
|
||||
cmdMap: map[string]string{
|
||||
"cat": "/dummy/cat",
|
||||
},
|
||||
wantCmd: "echo",
|
||||
wantCmdArgs: []string{"hello", "world"},
|
||||
},
|
||||
{
|
||||
name: "empty string mapping",
|
||||
cmd: "echo",
|
||||
cmdArgs: []string{"hello", "world"},
|
||||
cmdMap: map[string]string{
|
||||
"echo": "",
|
||||
},
|
||||
wantCmd: "echo",
|
||||
wantCmdArgs: []string{"hello", "world"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := &hostexec{commandMap: tt.cmdMap}
|
||||
|
||||
c, a := h.resolveCmd(tt.cmd, tt.cmdArgs...)
|
||||
if c != tt.wantCmd {
|
||||
t.Errorf("resolveCmd() c = %v, want %v", c, tt.wantCmd)
|
||||
}
|
||||
if !reflect.DeepEqual(a, tt.wantCmdArgs) {
|
||||
t.Errorf("resolveCmd() a = %v, want %v", a, tt.wantCmdArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostexec_wrapChroot(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
chroot string
|
||||
cmd string
|
||||
args []string
|
||||
wantCmd string
|
||||
wantArgs []string
|
||||
}{
|
||||
{
|
||||
name: "empty chroot",
|
||||
chroot: "",
|
||||
cmd: "echo",
|
||||
args: []string{"hello", "world"},
|
||||
wantCmd: "echo",
|
||||
wantArgs: []string{"hello", "world"},
|
||||
},
|
||||
{
|
||||
name: "non-empty chroot",
|
||||
chroot: "/tmp",
|
||||
cmd: "echo",
|
||||
args: []string{"hello", "world"},
|
||||
wantCmd: "/usr/sbin/chroot",
|
||||
wantArgs: []string{"/tmp", "echo", "hello", "world"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := &hostexec{chrootDir: tt.chroot}
|
||||
|
||||
c, a := h.wrapChroot(tt.cmd, tt.args...)
|
||||
if c != tt.wantCmd {
|
||||
t.Errorf("wrapChroot() c = %v, want %v", c, tt.wantCmd)
|
||||
}
|
||||
if !reflect.DeepEqual(a, tt.wantArgs) {
|
||||
t.Errorf("wrapChroot() a = %v, want %v", a, tt.wantArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostexec_wrap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cmd string
|
||||
args []string
|
||||
chroot string
|
||||
cmdMap map[string]string
|
||||
wantCmd string
|
||||
wantArgs []string
|
||||
}{
|
||||
{
|
||||
name: "no wrappers",
|
||||
cmd: "/bin/echo",
|
||||
args: []string{
|
||||
"hello",
|
||||
"world",
|
||||
},
|
||||
chroot: "",
|
||||
cmdMap: nil,
|
||||
wantCmd: "/bin/echo",
|
||||
wantArgs: []string{
|
||||
"hello",
|
||||
"world",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "env wrapper",
|
||||
cmd: "echo",
|
||||
args: []string{
|
||||
"hello",
|
||||
"world",
|
||||
},
|
||||
chroot: "",
|
||||
cmdMap: nil,
|
||||
wantCmd: "/usr/bin/env",
|
||||
wantArgs: []string{
|
||||
"-i", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "echo", "hello",
|
||||
"world",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "chroot wrapper",
|
||||
cmd: "/bin/echo",
|
||||
args: []string{"hello", "world"},
|
||||
chroot: "/tmp",
|
||||
cmdMap: nil,
|
||||
wantCmd: "/usr/sbin/chroot",
|
||||
wantArgs: []string{
|
||||
"/tmp",
|
||||
"/bin/echo",
|
||||
"hello",
|
||||
"world",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cmd path wrapper",
|
||||
cmd: "echo",
|
||||
args: []string{
|
||||
"hello",
|
||||
"world",
|
||||
},
|
||||
chroot: "",
|
||||
cmdMap: map[string]string{
|
||||
"echo": "/bin/echo",
|
||||
},
|
||||
wantCmd: "/bin/echo",
|
||||
wantArgs: []string{
|
||||
"hello", "world",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "chain wrappers",
|
||||
cmd: "echo",
|
||||
args: []string{"hello", "world"},
|
||||
chroot: "/tmp",
|
||||
cmdMap: map[string]string{"echo": "/bin/echo"},
|
||||
wantCmd: "/usr/sbin/chroot",
|
||||
wantArgs: []string{
|
||||
"/tmp",
|
||||
"/bin/echo",
|
||||
"hello", "world",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := &hostexec{
|
||||
commandMap: tt.cmdMap,
|
||||
chrootDir: tt.chroot,
|
||||
}
|
||||
|
||||
c, a := h.wrap(tt.cmd, tt.args...)
|
||||
if c != tt.wantCmd {
|
||||
t.Errorf("wrap() c = %v, want %v", c, tt.wantCmd)
|
||||
}
|
||||
if !reflect.DeepEqual(a, tt.wantArgs) {
|
||||
t.Errorf("wrap() a = %v, want %v", a, tt.wantArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// dummyCmd statisfies the interface for exec.Cmd for testing purposes
|
||||
type dummyCmd struct{}
|
||||
|
||||
func (dummyCmd) Run() error { return nil }
|
||||
func (dummyCmd) CombinedOutput() ([]byte, error) { return nil, nil }
|
||||
func (dummyCmd) Output() ([]byte, error) { return nil, nil }
|
||||
func (dummyCmd) SetDir(string) {}
|
||||
func (dummyCmd) SetStdin(io.Reader) {}
|
||||
func (dummyCmd) SetStdout(io.Writer) {}
|
||||
func (dummyCmd) SetStderr(io.Writer) {}
|
||||
func (dummyCmd) SetEnv([]string) {}
|
||||
func (dummyCmd) StdoutPipe() (io.ReadCloser, error) { return nil, nil }
|
||||
func (dummyCmd) StderrPipe() (io.ReadCloser, error) { return nil, nil }
|
||||
func (dummyCmd) Start() error { return nil }
|
||||
func (dummyCmd) Wait() error { return nil }
|
||||
func (dummyCmd) Stop() {}
|
||||
|
||||
// dummyInterface satisfies the interface for exec.Interface for testing purposes
|
||||
type dummyInterface struct {
|
||||
commandFunc func(string, ...string) exec.Cmd
|
||||
commandContextFunc func(context.Context, string, ...string) exec.Cmd
|
||||
lookPathFunc func(string) (string, error)
|
||||
}
|
||||
|
||||
func (d *dummyInterface) Command(cmd string, args ...string) exec.Cmd {
|
||||
return d.commandFunc(cmd, args...)
|
||||
}
|
||||
func (d *dummyInterface) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd {
|
||||
return d.commandContextFunc(ctx, cmd, args...)
|
||||
}
|
||||
func (d *dummyInterface) LookPath(path string) (string, error) { return d.lookPathFunc(path) }
|
||||
|
||||
func TestHostexec_Command(t *testing.T) {
|
||||
successCmd := dummyCmd{}
|
||||
|
||||
i := &dummyInterface{
|
||||
commandFunc: func(cmd string, args ...string) exec.Cmd {
|
||||
if cmd != "/bin/echo" {
|
||||
t.Errorf("Command() cmd = %v, want %v", cmd, "echo")
|
||||
}
|
||||
if !reflect.DeepEqual(args, []string{"hello", "world"}) {
|
||||
t.Errorf("Command() args = %v, want %v", args, []string{"hello", "world"})
|
||||
}
|
||||
return successCmd
|
||||
},
|
||||
}
|
||||
|
||||
h := &hostexec{
|
||||
Executor: i,
|
||||
commandMap: map[string]string{
|
||||
"echo": "/bin/echo",
|
||||
},
|
||||
}
|
||||
|
||||
c := h.Command("echo", "hello", "world")
|
||||
if c != successCmd {
|
||||
t.Errorf("Command() c = %v, want %v", c, successCmd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostexec_CommandContext(t *testing.T) {
|
||||
successCmd := dummyCmd{}
|
||||
|
||||
i := &dummyInterface{
|
||||
commandContextFunc: func(_ context.Context, cmd string, args ...string) exec.Cmd {
|
||||
if cmd != "/bin/echo" {
|
||||
t.Errorf("Command() cmd = %v, want %v", cmd, "echo")
|
||||
}
|
||||
if !reflect.DeepEqual(args, []string{"hello", "world"}) {
|
||||
t.Errorf("Command() args = %v, want %v", args, []string{"hello", "world"})
|
||||
}
|
||||
return successCmd
|
||||
},
|
||||
}
|
||||
|
||||
h := &hostexec{
|
||||
Executor: i,
|
||||
commandMap: map[string]string{
|
||||
"echo": "/bin/echo",
|
||||
},
|
||||
}
|
||||
|
||||
c := h.CommandContext(context.Background(), "echo", "hello", "world")
|
||||
if c != successCmd {
|
||||
t.Errorf("Command() c = %v, want %v", c, successCmd)
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,11 @@ import (
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/driver"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/common"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/service"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/utils/hostexec"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigPath = "./../../config/client-info.yml"
|
||||
ConfigPath = "./../../config/client-info.yml"
|
||||
SecretsFilePath = "./sanity-test-secret-file.yaml"
|
||||
)
|
||||
|
||||
@@ -58,8 +59,14 @@ func TestSanity(t *testing.T) {
|
||||
}
|
||||
defer dsmService.RemoveAllDsms()
|
||||
|
||||
cmdExecutor, err := hostexec.New(nil, "")
|
||||
if err != nil {
|
||||
t.Fatal(fmt.Sprintf("Failed to create command executor: %v\n", err))
|
||||
}
|
||||
tools := driver.NewTools(cmdExecutor)
|
||||
|
||||
endpoint := "unix://" + endpointFile.Name()
|
||||
drv, err := driver.NewControllerAndNodeDriver(nodeID, endpoint, dsmService)
|
||||
drv, err := driver.NewControllerAndNodeDriver(nodeID, endpoint, dsmService, tools)
|
||||
if err != nil {
|
||||
t.Fatal(fmt.Sprintf("Failed to create driver: %v\n", err))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user