package main import ( "crypto/md5" "encoding/json" "fmt" "io/ioutil" "os" "path/filepath" "sync" "time" // Vendors "iscsi" "util" "github.com/Sirupsen/logrus" "github.com/docker/go-plugins-helpers/volume" ) var iSCSI iscsi.ISCSIPlugin type iscsiVolume struct { Host string TargetIQN string Options []string Mountpoint string connections int } type iscsiDriver struct { sync.RWMutex root string statePath string volumes map[string]*iscsiVolume } // Constructor func newiscsiDriver(root string) (*iscsiDriver, error) { logrus.WithField("method", "new driver").Debug(root) fi, err := os.Lstat(root) if os.IsNotExist(err) { if err := os.MkdirAll(root, 0755); err != nil { return nil, err } } else if err != nil { return nil, err } if fi != nil && !fi.IsDir() { return nil, util.LogError("%v already exist and it's not a directory", root) } stateFolder := filepath.Join(root, "plugins", "docker-iscsi") fi, err = os.Lstat(stateFolder) if os.IsNotExist(err) { if err := os.MkdirAll(stateFolder, 0755); err != nil { return nil, err } } else if err != nil { return nil, err } if fi != nil && !fi.IsDir() { return nil, util.LogError("%v already exist and it's not a directory", stateFolder) } d := &iscsiDriver{ root: filepath.Join(root, "volumes"), statePath: filepath.Join(stateFolder, "iscsi-state.json"), volumes: map[string]*iscsiVolume{}, } data, err := ioutil.ReadFile(d.statePath) if err != nil { if os.IsNotExist(err) { logrus.WithField("statePath", d.statePath).Debug("no state found") } else { return nil, err } } else { if err := json.Unmarshal(data, &d.volumes); err != nil { return nil, err } } return d, nil } // Docker API :- Create func (d *iscsiDriver) Create(r *volume.CreateRequest) error { logrus.WithField("method", "create").Debugf("%#v", r) d.Lock() defer d.Unlock() v := &iscsiVolume{} for key, val := range r.Options { switch key { case "targetiqn": v.TargetIQN = val case "host": v.Host = val default: if val != "" { v.Options = append(v.Options, key+"="+val) } else { v.Options = append(v.Options, key) } } } if v.Host == "" { return util.LogError("'host' option required") } if v.TargetIQN == "" { return util.LogError("'targetiqn' option required") } v.Mountpoint = filepath.Join(d.root, fmt.Sprintf("%x", md5.Sum([]byte(v.TargetIQN)))) d.volumes[r.Name] = v d.saveState() return nil } // Docker API :- Remove func (d *iscsiDriver) Remove(r *volume.RemoveRequest) error { logrus.WithField("method", "remove").Debugf("%#v", r) d.Lock() defer d.Unlock() v, ok := d.volumes[r.Name] if !ok { return util.LogError("volume %s not found", r.Name) } if v.connections != 0 { return util.LogError("volume %s is currently used by a container", r.Name) } if err := os.RemoveAll(v.Mountpoint); err != nil { return util.LogError(err.Error()) } delete(d.volumes, r.Name) d.saveState() return nil } // Docker API :- Mount func (d *iscsiDriver) Mount(r *volume.MountRequest) (*volume.MountResponse, error) { logrus.WithField("method", "mount").Debugf("%#v", r) d.Lock() defer d.Unlock() v, ok := d.volumes[r.Name] if !ok { return &volume.MountResponse{}, util.LogError("volume %s not found", r.Name) } if v.connections == 0 { fi, err := os.Lstat(v.Mountpoint) if os.IsNotExist(err) { if err := os.MkdirAll(v.Mountpoint, 0755); err != nil { return &volume.MountResponse{}, util.LogError(err.Error()) } } else if err != nil { return &volume.MountResponse{}, util.LogError(err.Error()) } if fi != nil && !fi.IsDir() { return &volume.MountResponse{}, util.LogError("%v already exist and it's not a directory", v.Mountpoint) } if err := d.mountVolume(v); err != nil { return &volume.MountResponse{}, util.LogError(err.Error()) } } v.connections++ return &volume.MountResponse{Mountpoint: v.Mountpoint}, nil } // Docker API :- Path func (d *iscsiDriver) Path(r *volume.PathRequest) (*volume.PathResponse, error) { logrus.WithField("method", "path").Debugf("%#v", r) d.RLock() defer d.RUnlock() v, ok := d.volumes[r.Name] if !ok { return &volume.PathResponse{}, util.LogError("volume %s not found", r.Name) } return &volume.PathResponse{Mountpoint: v.Mountpoint}, nil } // Docker API :- Unmount func (d *iscsiDriver) Unmount(r *volume.UnmountRequest) error { logrus.WithField("method", "unmount").Debugf("%#v", r) d.Lock() defer d.Unlock() v, ok := d.volumes[r.Name] if !ok { return util.LogError("volume %s not found", r.Name) } v.connections-- if v.connections <= 0 { if err := d.unmountVolume(v); err != nil { return util.LogError(err.Error()) } v.connections = 0 } return nil } // Docker API :- Get func (d *iscsiDriver) Get(r *volume.GetRequest) (*volume.GetResponse, error) { logrus.WithField("method", "get").Debugf("%#v", r) d.Lock() defer d.Unlock() v, ok := d.volumes[r.Name] if !ok { return &volume.GetResponse{}, util.LogError("volume %s not found", r.Name) } return &volume.GetResponse{Volume: &volume.Volume{Name: r.Name, Mountpoint: v.Mountpoint}}, nil } // Docker API :- List func (d *iscsiDriver) List() (*volume.ListResponse, error) { logrus.WithField("method", "list").Debugf("") d.Lock() defer d.Unlock() var vols []*volume.Volume for name, v := range d.volumes { vols = append(vols, &volume.Volume{Name: name, Mountpoint: v.Mountpoint}) } return &volume.ListResponse{Volumes: vols}, nil } // Docker API :- Capabilities func (d *iscsiDriver) Capabilities() *volume.CapabilitiesResponse { logrus.WithField("method", "capabilities").Debugf("") return &volume.CapabilitiesResponse{Capabilities: volume.Capability{Scope: "local"}} } // Local Functions func (d *iscsiDriver) mountVolume(v *iscsiVolume) error { // Discover iSCSI LUNs err := iSCSI.DiscoverLUNs(v.Host) if err != nil { return err } if !iSCSI.TargetLoggedIn(v.TargetIQN) { // Login to iSCSI Target err = iSCSI.LoginTarget(v.TargetIQN, v.Host) if err != nil { return err } // Wait for Physical Volume to appear time.Sleep(500 * time.Millisecond) } out := "" errMsg := "" diskPathFolder := "/dev/disk/by-path" fi, err := os.Lstat(diskPathFolder) if os.IsNotExist(err) { // Disk By Path doesn't exist iscsiPathFolder := "/host/tmp/iscsi" fi, err = os.Lstat(iscsiPathFolder) if os.IsNotExist(err) { // iSCSI Administration folder doesn't exist return err } else { // iSCSI Administration folder exists, seen on Synology systems // Get target device cmd := "ls /host/tmp/iscsi | grep \"" + v.TargetIQN + "\" | head -n 1" out, errMsg = util.ExecuteCommandString(cmd) if len(out) > 0 { logrus.Debug(out) } if len(errMsg) > 0 { err := fmt.Errorf("Unable to find device: %s", errMsg) return err } cmd = "readlink -nf /host/tmp/iscsi/" + out out, errMsg = util.ExecuteCommandString(cmd) if len(out) > 0 { logrus.Debug(out) } if len(errMsg) > 0 { err := fmt.Errorf("Unable to locate device: %s", errMsg) return err } } } else { // Disk By Path exists // Get target device cmd := "ls /dev/disk/by-path | grep \"" + v.TargetIQN + "\" | head -n 1" out, errMsg = util.ExecuteCommandString(cmd) if len(out) > 0 { logrus.Debug(out) } if len(errMsg) > 0 { err := fmt.Errorf("Unable to find device: %s", errMsg) return err } cmd = "readlink -nf /dev/disk/by-path/" + out out, errMsg = util.ExecuteCommandString(cmd) if len(out) > 0 { logrus.Debug(out) } if len(errMsg) > 0 { err := fmt.Errorf("Unable to locate device: %s", errMsg) return err } } _ = fi // Mount cmd := "mount " + out + " " + v.Mountpoint out, errMsg = util.ExecuteCommandString(cmd) if len(out) > 0 { logrus.Debug(out) } if len(errMsg) > 0 { err := fmt.Errorf("Unable to Mount Volume: %s", errMsg) return err } return nil } func (d *iscsiDriver) unmountVolume(v *iscsiVolume) error { // Unmount cmd := "umount " + v.Mountpoint out, errMsg := util.ExecuteCommandString(cmd) if len(out) > 0 { logrus.Debug(out) } if len(errMsg) > 0 { err := fmt.Errorf("Unable to Unmount Volume: %s", errMsg) logrus.Info(err) } // Logout from iSCSI Target err := iSCSI.LogoutTarget(v.TargetIQN, v.Host) if err != nil { return err } return nil } func (d *iscsiDriver) saveState() { data, err := json.Marshal(d.volumes) if err != nil { logrus.WithField("statePath", d.statePath).Error(err) return } if err := ioutil.WriteFile(d.statePath, data, 0644); err != nil { logrus.WithField("savestate", d.statePath).Error(err) } }