| /* |
| Copyright 2017 The Kubernetes Authors. |
| |
| 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 scaleio |
| |
| import ( |
| "errors" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "path" |
| "path/filepath" |
| "regexp" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| |
| "k8s.io/kubernetes/pkg/util/mount" |
| |
| sio "github.com/codedellemc/goscaleio" |
| siotypes "github.com/codedellemc/goscaleio/types/v1" |
| "k8s.io/klog" |
| ) |
| |
| var ( |
| sioDiskIDPath = "/dev/disk/by-id" |
| ) |
| |
| type sioVolumeID string |
| |
| type sioInterface interface { |
| FindVolume(name string) (*siotypes.Volume, error) |
| Volume(sioVolumeID) (*siotypes.Volume, error) |
| CreateVolume(name string, sizeGB int64) (*siotypes.Volume, error) |
| AttachVolume(sioVolumeID, bool) error |
| DetachVolume(sioVolumeID) error |
| DeleteVolume(sioVolumeID) error |
| IID() (string, error) |
| Devs() (map[string]string, error) |
| WaitForAttachedDevice(token string) (string, error) |
| WaitForDetachedDevice(token string) error |
| GetVolumeRefs(sioVolumeID) (int, error) |
| } |
| |
| type sioClient struct { |
| client *sio.Client |
| gateway string |
| username string |
| password string |
| insecure bool |
| certsEnabled bool |
| system *siotypes.System |
| sysName string |
| sysClient *sio.System |
| protectionDomain *siotypes.ProtectionDomain |
| pdName string |
| pdClient *sio.ProtectionDomain |
| storagePool *siotypes.StoragePool |
| spName string |
| spClient *sio.StoragePool |
| provisionMode string |
| sdcPath string |
| sdcGUID string |
| instanceID string |
| inited bool |
| diskRegex *regexp.Regexp |
| mtx sync.Mutex |
| exec mount.Exec |
| } |
| |
| func newSioClient(gateway, username, password string, sslEnabled bool, exec mount.Exec) (*sioClient, error) { |
| client := new(sioClient) |
| client.gateway = gateway |
| client.username = username |
| client.password = password |
| client.exec = exec |
| if sslEnabled { |
| client.insecure = false |
| client.certsEnabled = true |
| } else { |
| client.insecure = true |
| client.certsEnabled = false |
| } |
| r, err := regexp.Compile(`^emc-vol-\w*-\w*$`) |
| if err != nil { |
| klog.Error(log("failed to compile regex: %v", err)) |
| return nil, err |
| } |
| client.diskRegex = r |
| |
| // delay client setup/login until init() |
| return client, nil |
| } |
| |
| // init setups client and authenticate |
| func (c *sioClient) init() error { |
| c.mtx.Lock() |
| defer c.mtx.Unlock() |
| if c.inited { |
| return nil |
| } |
| klog.V(4).Infoln(log("initializing scaleio client")) |
| client, err := sio.NewClientWithArgs(c.gateway, "", c.insecure, c.certsEnabled) |
| if err != nil { |
| klog.Error(log("failed to create client: %v", err)) |
| return err |
| } |
| c.client = client |
| if _, err = c.client.Authenticate( |
| &sio.ConfigConnect{ |
| Endpoint: c.gateway, |
| Version: "", |
| Username: c.username, |
| Password: c.password}, |
| ); err != nil { |
| klog.Error(log("client authentication failed: %v", err)) |
| return err |
| } |
| |
| // retrieve system |
| if c.system, err = c.findSystem(c.sysName); err != nil { |
| klog.Error(log("unable to find system %s: %v", c.sysName, err)) |
| return err |
| } |
| |
| // retrieve protection domain |
| if c.protectionDomain, err = c.findProtectionDomain(c.pdName); err != nil { |
| klog.Error(log("unable to find protection domain %s: %v", c.protectionDomain, err)) |
| return err |
| } |
| // retrieve storage pool |
| if c.storagePool, err = c.findStoragePool(c.spName); err != nil { |
| klog.Error(log("unable to find storage pool %s: %v", c.storagePool, err)) |
| return err |
| } |
| c.inited = true |
| return nil |
| } |
| |
| func (c *sioClient) Volumes() ([]*siotypes.Volume, error) { |
| if err := c.init(); err != nil { |
| return nil, err |
| } |
| vols, err := c.getVolumes() |
| if err != nil { |
| klog.Error(log("failed to retrieve volumes: %v", err)) |
| return nil, err |
| } |
| return vols, nil |
| } |
| |
| func (c *sioClient) Volume(id sioVolumeID) (*siotypes.Volume, error) { |
| if err := c.init(); err != nil { |
| return nil, err |
| } |
| |
| vols, err := c.getVolumesByID(id) |
| if err != nil { |
| klog.Error(log("failed to retrieve volume by id: %v", err)) |
| return nil, err |
| } |
| vol := vols[0] |
| if vol == nil { |
| klog.V(4).Info(log("volume not found, id %s", id)) |
| return nil, errors.New("volume not found") |
| } |
| return vol, nil |
| } |
| |
| func (c *sioClient) FindVolume(name string) (*siotypes.Volume, error) { |
| if err := c.init(); err != nil { |
| return nil, err |
| } |
| |
| klog.V(4).Info(log("searching for volume %s", name)) |
| volumes, err := c.getVolumesByName(name) |
| if err != nil { |
| klog.Error(log("failed to find volume by name %v", err)) |
| return nil, err |
| } |
| |
| for _, volume := range volumes { |
| if volume.Name == name { |
| klog.V(4).Info(log("found volume %s", name)) |
| return volume, nil |
| } |
| } |
| klog.V(4).Info(log("volume not found, name %s", name)) |
| return nil, errors.New("volume not found") |
| } |
| |
| func (c *sioClient) CreateVolume(name string, sizeGB int64) (*siotypes.Volume, error) { |
| if err := c.init(); err != nil { |
| return nil, err |
| } |
| |
| params := &siotypes.VolumeParam{ |
| Name: name, |
| VolumeSizeInKb: strconv.Itoa(int(sizeGB) * 1024 * 1024), |
| VolumeType: c.provisionMode, |
| } |
| createResponse, err := c.client.CreateVolume(params, c.storagePool.Name) |
| if err != nil { |
| klog.Error(log("failed to create volume %s: %v", name, err)) |
| return nil, err |
| } |
| return c.Volume(sioVolumeID(createResponse.ID)) |
| } |
| |
| // AttachVolume maps the scaleio volume to an sdc node. If the multipleMappings flag |
| // is true, ScaleIO will allow other SDC to map to that volume. |
| func (c *sioClient) AttachVolume(id sioVolumeID, multipleMappings bool) error { |
| if err := c.init(); err != nil { |
| klog.Error(log("failed to init'd client in attach volume: %v", err)) |
| return err |
| } |
| |
| iid, err := c.IID() |
| if err != nil { |
| klog.Error(log("failed to get instanceIID for attach volume: %v", err)) |
| return err |
| } |
| |
| params := &siotypes.MapVolumeSdcParam{ |
| SdcID: iid, |
| AllowMultipleMappings: strconv.FormatBool(multipleMappings), |
| AllSdcs: "", |
| } |
| volClient := sio.NewVolume(c.client) |
| volClient.Volume = &siotypes.Volume{ID: string(id)} |
| |
| if err := volClient.MapVolumeSdc(params); err != nil { |
| klog.Error(log("failed to attach volume id %s: %v", id, err)) |
| return err |
| } |
| |
| klog.V(4).Info(log("volume %s attached successfully", id)) |
| return nil |
| } |
| |
| // DetachVolume detaches the volume with specified id. |
| func (c *sioClient) DetachVolume(id sioVolumeID) error { |
| if err := c.init(); err != nil { |
| return err |
| } |
| |
| iid, err := c.IID() |
| if err != nil { |
| return err |
| } |
| params := &siotypes.UnmapVolumeSdcParam{ |
| SdcID: "", |
| IgnoreScsiInitiators: "true", |
| AllSdcs: iid, |
| } |
| volClient := sio.NewVolume(c.client) |
| volClient.Volume = &siotypes.Volume{ID: string(id)} |
| if err := volClient.UnmapVolumeSdc(params); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // DeleteVolume deletes the volume with the specified id |
| func (c *sioClient) DeleteVolume(id sioVolumeID) error { |
| if err := c.init(); err != nil { |
| return err |
| } |
| |
| vol, err := c.Volume(id) |
| if err != nil { |
| return err |
| } |
| volClient := sio.NewVolume(c.client) |
| volClient.Volume = vol |
| if err := volClient.RemoveVolume("ONLY_ME"); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // IID returns the scaleio instance id for node |
| func (c *sioClient) IID() (string, error) { |
| if err := c.init(); err != nil { |
| return "", err |
| } |
| |
| // if instanceID not set, retrieve it |
| if c.instanceID == "" { |
| guid, err := c.getGUID() |
| if err != nil { |
| return "", err |
| } |
| sdc, err := c.sysClient.FindSdc("SdcGUID", guid) |
| if err != nil { |
| klog.Error(log("failed to retrieve sdc info %s", err)) |
| return "", err |
| } |
| c.instanceID = sdc.Sdc.ID |
| klog.V(4).Info(log("retrieved instanceID %s", c.instanceID)) |
| } |
| return c.instanceID, nil |
| } |
| |
| // getGUID returns instance GUID, if not set using resource labels |
| // it attempts to fallback to using drv_cfg binary |
| func (c *sioClient) getGUID() (string, error) { |
| if c.sdcGUID == "" { |
| klog.V(4).Info(log("sdc guid label not set, falling back to using drv_cfg")) |
| cmd := c.getSdcCmd() |
| output, err := c.exec.Run(cmd, "--query_guid") |
| if err != nil { |
| klog.Error(log("drv_cfg --query_guid failed: %v", err)) |
| return "", err |
| } |
| c.sdcGUID = strings.TrimSpace(string(output)) |
| } |
| return c.sdcGUID, nil |
| } |
| |
| // getSioDiskPaths traverse local disk devices to retrieve device path |
| // The path is extracted from /dev/disk/by-id; each sio device path has format: |
| // emc-vol-<mdmID-volID> e.g.: |
| // emc-vol-788d9efb0a8f20cb-a2b8419300000000 |
| func (c *sioClient) getSioDiskPaths() ([]os.FileInfo, error) { |
| files, err := ioutil.ReadDir(sioDiskIDPath) |
| if err != nil { |
| if os.IsNotExist(err) { |
| // sioDiskIDPath may not exist yet which is fine |
| return []os.FileInfo{}, nil |
| } |
| klog.Error(log("failed to ReadDir %s: %v", sioDiskIDPath, err)) |
| return nil, err |
| |
| } |
| result := []os.FileInfo{} |
| for _, file := range files { |
| if c.diskRegex.MatchString(file.Name()) { |
| result = append(result, file) |
| } |
| } |
| |
| return result, nil |
| |
| } |
| |
| // GetVolumeRefs counts the number of references an SIO volume has a disk device. |
| // This is useful in preventing premature detach. |
| func (c *sioClient) GetVolumeRefs(volID sioVolumeID) (refs int, err error) { |
| files, err := c.getSioDiskPaths() |
| if err != nil { |
| return 0, err |
| } |
| for _, file := range files { |
| if strings.Contains(file.Name(), string(volID)) { |
| refs++ |
| } |
| } |
| return |
| } |
| |
| // Devs returns a map of local devices as map[<volume.id>]<deviceName> |
| func (c *sioClient) Devs() (map[string]string, error) { |
| volumeMap := make(map[string]string) |
| |
| files, err := c.getSioDiskPaths() |
| if err != nil { |
| return nil, err |
| } |
| |
| for _, f := range files { |
| // split emc-vol-<mdmID>-<volumeID> to pull out volumeID |
| parts := strings.Split(f.Name(), "-") |
| if len(parts) != 4 { |
| return nil, errors.New("unexpected ScaleIO device name format") |
| } |
| volumeID := parts[3] |
| devPath, err := filepath.EvalSymlinks(fmt.Sprintf("%s/%s", sioDiskIDPath, f.Name())) |
| if err != nil { |
| klog.Error(log("devicepath-to-volID mapping error: %v", err)) |
| return nil, err |
| } |
| // map volumeID to devicePath |
| volumeMap[volumeID] = devPath |
| } |
| return volumeMap, nil |
| } |
| |
| // WaitForAttachedDevice sets up a timer to wait for an attached device to appear in the instance's list. |
| func (c *sioClient) WaitForAttachedDevice(token string) (string, error) { |
| if token == "" { |
| return "", fmt.Errorf("invalid attach token") |
| } |
| |
| // wait for device to show up in local device list |
| ticker := time.NewTicker(time.Second) |
| defer ticker.Stop() |
| timer := time.NewTimer(30 * time.Second) |
| defer timer.Stop() |
| |
| for { |
| select { |
| case <-ticker.C: |
| devMap, err := c.Devs() |
| if err != nil { |
| klog.Error(log("failed while waiting for volume to attach: %v", err)) |
| return "", err |
| } |
| go func() { |
| klog.V(4).Info(log("waiting for volume %s to be mapped/attached", token)) |
| }() |
| if path, ok := devMap[token]; ok { |
| klog.V(4).Info(log("device %s mapped to vol %s", path, token)) |
| return path, nil |
| } |
| case <-timer.C: |
| klog.Error(log("timed out while waiting for volume to be mapped to a device")) |
| return "", fmt.Errorf("volume attach timeout") |
| } |
| } |
| } |
| |
| // waitForDetachedDevice waits for device to be detached |
| func (c *sioClient) WaitForDetachedDevice(token string) error { |
| if token == "" { |
| return fmt.Errorf("invalid detach token") |
| } |
| |
| // wait for attach.Token to show up in local device list |
| ticker := time.NewTicker(time.Second) |
| defer ticker.Stop() |
| timer := time.NewTimer(30 * time.Second) |
| defer timer.Stop() |
| |
| for { |
| select { |
| case <-ticker.C: |
| devMap, err := c.Devs() |
| if err != nil { |
| klog.Error(log("failed while waiting for volume to unmap/detach: %v", err)) |
| return err |
| } |
| go func() { |
| klog.V(4).Info(log("waiting for volume %s to be unmapped/detached", token)) |
| }() |
| // cant find vol id, then ok. |
| if _, ok := devMap[token]; !ok { |
| return nil |
| } |
| case <-timer.C: |
| klog.Error(log("timed out while waiting for volume %s to be unmapped/detached", token)) |
| return fmt.Errorf("volume detach timeout") |
| } |
| } |
| } |
| |
| // *********************************************************************** |
| // Little Helpers! |
| // *********************************************************************** |
| func (c *sioClient) findSystem(sysname string) (sys *siotypes.System, err error) { |
| if c.sysClient, err = c.client.FindSystem("", sysname, ""); err != nil { |
| return nil, err |
| } |
| systems, err := c.client.GetInstance("") |
| if err != nil { |
| klog.Error(log("failed to retrieve instances: %v", err)) |
| return nil, err |
| } |
| for _, sys = range systems { |
| if sys.Name == sysname { |
| return sys, nil |
| } |
| } |
| klog.Error(log("system %s not found", sysname)) |
| return nil, errors.New("system not found") |
| } |
| |
| func (c *sioClient) findProtectionDomain(pdname string) (*siotypes.ProtectionDomain, error) { |
| c.pdClient = sio.NewProtectionDomain(c.client) |
| if c.sysClient != nil { |
| protectionDomain, err := c.sysClient.FindProtectionDomain("", pdname, "") |
| if err != nil { |
| klog.Error(log("failed to retrieve protection domains: %v", err)) |
| return nil, err |
| } |
| c.pdClient.ProtectionDomain = protectionDomain |
| return protectionDomain, nil |
| } |
| klog.Error(log("protection domain %s not set", pdname)) |
| return nil, errors.New("protection domain not set") |
| } |
| |
| func (c *sioClient) findStoragePool(spname string) (*siotypes.StoragePool, error) { |
| c.spClient = sio.NewStoragePool(c.client) |
| if c.pdClient != nil { |
| sp, err := c.pdClient.FindStoragePool("", spname, "") |
| if err != nil { |
| klog.Error(log("failed to retrieve storage pool: %v", err)) |
| return nil, err |
| } |
| c.spClient.StoragePool = sp |
| return sp, nil |
| } |
| klog.Error(log("storage pool %s not set", spname)) |
| return nil, errors.New("storage pool not set") |
| } |
| |
| func (c *sioClient) getVolumes() ([]*siotypes.Volume, error) { |
| return c.client.GetVolume("", "", "", "", true) |
| } |
| func (c *sioClient) getVolumesByID(id sioVolumeID) ([]*siotypes.Volume, error) { |
| return c.client.GetVolume("", string(id), "", "", true) |
| } |
| |
| func (c *sioClient) getVolumesByName(name string) ([]*siotypes.Volume, error) { |
| return c.client.GetVolume("", "", "", name, true) |
| } |
| |
| func (c *sioClient) getSdcPath() string { |
| return sdcRootPath |
| } |
| |
| func (c *sioClient) getSdcCmd() string { |
| return path.Join(c.getSdcPath(), "drv_cfg") |
| } |