mirror of
https://github.com/cristicalin/synology-csi.git
synced 2026-03-26 19:03:12 +00:00
Initial commit
This commit is contained in:
493
pkg/driver/controllerserver.go
Normal file
493
pkg/driver/controllerserver.go
Normal file
@@ -0,0 +1,493 @@
|
||||
/*
|
||||
Copyright 2021 Synology Inc.
|
||||
|
||||
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 driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"strconv"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/webapi"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/interfaces"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/models"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/utils"
|
||||
)
|
||||
|
||||
type controllerServer struct {
|
||||
Driver *Driver
|
||||
dsmService interfaces.IDsmService
|
||||
Initiator *initiatorDriver
|
||||
}
|
||||
|
||||
func getSizeByCapacityRange(capRange *csi.CapacityRange) (int64, error) {
|
||||
if capRange == nil {
|
||||
return 1 * utils.UNIT_GB, nil
|
||||
}
|
||||
|
||||
minSize := capRange.GetRequiredBytes()
|
||||
maxSize := capRange.GetLimitBytes()
|
||||
if 0 < maxSize && maxSize < minSize {
|
||||
return 0, status.Error(codes.InvalidArgument, "Invalid input: limitBytes is smaller than requiredBytes")
|
||||
}
|
||||
if minSize < utils.UNIT_GB {
|
||||
return 0, status.Error(codes.InvalidArgument, "Invalid input: required bytes is smaller than 1G")
|
||||
}
|
||||
|
||||
return int64(minSize), nil
|
||||
}
|
||||
|
||||
func (cs *controllerServer) isVolumeAccessModeSupport(mode csi.VolumeCapability_AccessMode_Mode) bool {
|
||||
for _, accessMode := range cs.Driver.getVolumeCapabilityAccessModes() {
|
||||
if mode == accessMode.Mode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
|
||||
sizeInByte, err := getSizeByCapacityRange(req.GetCapacityRange())
|
||||
volName, volCap := req.GetName(), req.GetVolumeCapabilities()
|
||||
volContentSrc := req.GetVolumeContentSource()
|
||||
|
||||
var srcSnapshotId string = ""
|
||||
var srcVolumeId string = ""
|
||||
var multiSession bool = false
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if volName == "" {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "No name is provided")
|
||||
}
|
||||
|
||||
if volCap == nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "No volume capabilities are provided")
|
||||
}
|
||||
|
||||
for _, cap := range volCap {
|
||||
accessMode := cap.GetAccessMode().GetMode()
|
||||
|
||||
if !cs.isVolumeAccessModeSupport(accessMode) {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "Invalid volume capability access mode")
|
||||
}
|
||||
|
||||
if accessMode == csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER {
|
||||
multiSession = false
|
||||
} else if accessMode == csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER {
|
||||
multiSession = true
|
||||
}
|
||||
}
|
||||
|
||||
if volContentSrc != nil {
|
||||
if srcSnapshot := volContentSrc.GetSnapshot(); srcSnapshot != nil {
|
||||
srcSnapshotId = srcSnapshot.SnapshotId
|
||||
} else if srcVolume := volContentSrc.GetVolume(); srcVolume != nil {
|
||||
srcVolumeId = srcVolume.VolumeId
|
||||
} else {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "Invalid volume content source")
|
||||
}
|
||||
}
|
||||
|
||||
params := req.GetParameters()
|
||||
|
||||
isThin := true
|
||||
if params["thin_provisioning"] != "" {
|
||||
isThin = utils.StringToBoolean(params["thin_provisioning"])
|
||||
}
|
||||
|
||||
spec := &models.CreateK8sVolumeSpec{
|
||||
DsmIp: params["dsm"],
|
||||
K8sVolumeName: volName,
|
||||
LunName: fmt.Sprintf("%s-%s", models.LunPrefix, volName),
|
||||
Location: params["location"],
|
||||
Size: sizeInByte,
|
||||
Type: params["type"],
|
||||
ThinProvisioning: isThin,
|
||||
TargetName: fmt.Sprintf("%s-%s", models.LunPrefix, volName),
|
||||
MultipleSession: multiSession,
|
||||
SourceSnapshotId: srcSnapshotId,
|
||||
SourceVolumeId: srcVolumeId,
|
||||
}
|
||||
|
||||
lunInfo, dsmIp, err := cs.dsmService.CreateVolume(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if int64(lunInfo.Size) != sizeInByte {
|
||||
return nil , status.Errorf(codes.AlreadyExists, "Already existing volume name with different capacity")
|
||||
}
|
||||
|
||||
return &csi.CreateVolumeResponse{
|
||||
Volume: &csi.Volume{
|
||||
VolumeId: lunInfo.Uuid,
|
||||
CapacityBytes: int64(lunInfo.Size),
|
||||
ContentSource: volContentSrc,
|
||||
VolumeContext: map[string]string{
|
||||
"dsm": dsmIp,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cs *controllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) {
|
||||
volumeId := req.GetVolumeId()
|
||||
if volumeId == "" {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "No volume id is provided")
|
||||
}
|
||||
|
||||
if err := cs.dsmService.DeleteVolume(volumeId); err != nil {
|
||||
return nil, status.Errorf(codes.Internal,
|
||||
fmt.Sprintf("Failed to DeleteVolume(%s), err: %v", volumeId, err))
|
||||
}
|
||||
|
||||
return &csi.DeleteVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
func (cs *controllerServer) ControllerPublishVolume(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
func (cs *controllerServer) ControllerUnpublishVolume(ctx context.Context, req *csi.ControllerUnpublishVolumeRequest) (*csi.ControllerUnpublishVolumeResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
func (cs *controllerServer) ValidateVolumeCapabilities(ctx context.Context, req *csi.ValidateVolumeCapabilitiesRequest) (*csi.ValidateVolumeCapabilitiesResponse, error) {
|
||||
volumeId, volCap := req.GetVolumeId(), req.GetVolumeCapabilities()
|
||||
if volumeId == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request")
|
||||
}
|
||||
|
||||
if volCap == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "No volume capabilities are provided")
|
||||
}
|
||||
|
||||
if cs.dsmService.GetVolume(volumeId) == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "Volume[%s] does not exist", volumeId)
|
||||
}
|
||||
|
||||
for _, cap := range volCap {
|
||||
if !cs.isVolumeAccessModeSupport(cap.GetAccessMode().GetMode()) {
|
||||
return nil, status.Errorf(codes.NotFound, "Driver does not support volume capabilities:%v", volCap)
|
||||
}
|
||||
}
|
||||
|
||||
return &csi.ValidateVolumeCapabilitiesResponse{}, nil
|
||||
}
|
||||
|
||||
func (cs *controllerServer) ListVolumes(ctx context.Context, req *csi.ListVolumesRequest) (*csi.ListVolumesResponse, error) {
|
||||
maxEntries := req.GetMaxEntries()
|
||||
startingToken := req.GetStartingToken()
|
||||
|
||||
var entries []*csi.ListVolumesResponse_Entry
|
||||
var nextToken string = ""
|
||||
|
||||
if 0 > maxEntries {
|
||||
return nil, status.Error(codes.InvalidArgument, "Max entries can not be negative.")
|
||||
}
|
||||
|
||||
pagingSkip := ("" != startingToken)
|
||||
infos := cs.dsmService.ListVolumes()
|
||||
|
||||
var count int32 = 0
|
||||
for _, info := range infos {
|
||||
if info.Lun.Uuid == startingToken {
|
||||
pagingSkip = false
|
||||
}
|
||||
|
||||
if pagingSkip {
|
||||
continue
|
||||
}
|
||||
|
||||
if maxEntries > 0 && count >= maxEntries {
|
||||
nextToken = info.Lun.Uuid
|
||||
break
|
||||
}
|
||||
|
||||
entries = append(entries, &csi.ListVolumesResponse_Entry{
|
||||
Volume: &csi.Volume{
|
||||
VolumeId: info.Lun.Uuid,
|
||||
CapacityBytes: int64(info.Lun.Size),
|
||||
VolumeContext: map[string]string{
|
||||
"dsm": info.DsmIp,
|
||||
"lunName": info.Lun.Name,
|
||||
"targetIqn": info.Target.Iqn,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
count++
|
||||
}
|
||||
|
||||
if pagingSkip {
|
||||
return nil, status.Errorf(codes.Aborted, fmt.Sprintf("Invalid StartingToken(%s)", startingToken))
|
||||
}
|
||||
|
||||
return &csi.ListVolumesResponse{
|
||||
Entries: entries,
|
||||
NextToken: nextToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cs *controllerServer) GetCapacity(ctx context.Context, req *csi.GetCapacityRequest) (*csi.GetCapacityResponse, error) {
|
||||
params := req.GetParameters()
|
||||
|
||||
volInfos, err := cs.dsmService.ListDsmVolumes(params["dsm"])
|
||||
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "Failed to list dsm volumes")
|
||||
}
|
||||
|
||||
var availableCapacity int64 = 0
|
||||
|
||||
location := params["location"]
|
||||
for _, info := range volInfos {
|
||||
if location != "" && info.Path != location {
|
||||
continue
|
||||
}
|
||||
|
||||
freeSize, err := strconv.ParseInt(info.Free, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
availableCapacity += freeSize
|
||||
}
|
||||
|
||||
return &csi.GetCapacityResponse{
|
||||
AvailableCapacity: availableCapacity,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cs *controllerServer) ControllerGetCapabilities(ctx context.Context, req *csi.ControllerGetCapabilitiesRequest) (*csi.ControllerGetCapabilitiesResponse, error) {
|
||||
return &csi.ControllerGetCapabilitiesResponse{
|
||||
Capabilities: cs.Driver.csCap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cs *controllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) {
|
||||
srcVolId := req.GetSourceVolumeId()
|
||||
snapshotName := req.GetName()
|
||||
params := req.GetParameters()
|
||||
|
||||
if srcVolId == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "Source volume id is empty.")
|
||||
}
|
||||
|
||||
if snapshotName == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "Snapshot name is empty.")
|
||||
}
|
||||
|
||||
snapshotInfos, err := cs.dsmService.ListAllSnapshots()
|
||||
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, fmt.Sprintf("Failed to ListAllSnapshots(), err: %v", err))
|
||||
}
|
||||
|
||||
// idempotency
|
||||
for _, snapshotInfo := range snapshotInfos {
|
||||
if snapshotInfo.Name == snapshotName {
|
||||
if snapshotInfo.ParentUuid != srcVolId {
|
||||
return nil, status.Errorf(codes.AlreadyExists, fmt.Sprintf("Snapshot [%s] already exists but volume id is incompatible", snapshotName))
|
||||
}
|
||||
|
||||
createTime, err := ptypes.TimestampProto(time.Unix(snapshotInfo.CreateTime, 0))
|
||||
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, fmt.Sprintf("Failed to convert create time, err: %v", err))
|
||||
}
|
||||
|
||||
return &csi.CreateSnapshotResponse{
|
||||
Snapshot: &csi.Snapshot{
|
||||
SizeBytes: snapshotInfo.TotalSize,
|
||||
SnapshotId: snapshotInfo.Uuid,
|
||||
SourceVolumeId: snapshotInfo.ParentUuid,
|
||||
CreationTime: createTime,
|
||||
ReadyToUse: (snapshotInfo.Status == "Healthy"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
spec := &models.CreateK8sVolumeSnapshotSpec{
|
||||
K8sVolumeId: srcVolId,
|
||||
SnapshotName: snapshotName,
|
||||
Description: params["description"],
|
||||
TakenBy: models.K8sCsiName,
|
||||
IsLocked: utils.StringToBoolean(params["is_locked"]),
|
||||
}
|
||||
|
||||
snapshotId, err := cs.dsmService.CreateSnapshot(spec)
|
||||
|
||||
if err != nil {
|
||||
if err == utils.OutOfFreeSpaceError("") || err == utils.SnapshotReachMaxCountError("") {
|
||||
return nil,status.Errorf(codes.ResourceExhausted, fmt.Sprintf("Failed to CreateSnapshot(%s), err: %v", srcVolId, err))
|
||||
} else {
|
||||
return nil, status.Errorf(codes.Internal, fmt.Sprintf("Failed to CreateSnapshot(%s), err: %v", srcVolId, err))
|
||||
}
|
||||
}
|
||||
|
||||
snapshotInfo, err := cs.dsmService.GetSnapshot(snapshotId)
|
||||
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, fmt.Sprintf("Failed to GetSnapshot(%s), err: %v", snapshotId, err))
|
||||
}
|
||||
|
||||
createTime, err := ptypes.TimestampProto(time.Unix(snapshotInfo.CreateTime, 0))
|
||||
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, fmt.Sprintf("Failed to convert create time, err: %v", err))
|
||||
}
|
||||
|
||||
return &csi.CreateSnapshotResponse{
|
||||
Snapshot: &csi.Snapshot{
|
||||
SizeBytes: snapshotInfo.TotalSize,
|
||||
SnapshotId: snapshotInfo.Uuid,
|
||||
SourceVolumeId: snapshotInfo.ParentUuid,
|
||||
CreationTime: createTime,
|
||||
ReadyToUse: (snapshotInfo.Status == "Healthy"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cs *controllerServer) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) {
|
||||
snapshotId := req.GetSnapshotId()
|
||||
|
||||
if snapshotId == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "Snapshot id is empty.")
|
||||
}
|
||||
|
||||
err := cs.dsmService.DeleteSnapshot(snapshotId)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, fmt.Sprintf("Failed to DeleteSnapshot(%s), err: %v", snapshotId, err))
|
||||
}
|
||||
|
||||
return &csi.DeleteSnapshotResponse{}, nil
|
||||
}
|
||||
|
||||
func (cs *controllerServer) ListSnapshots(ctx context.Context, req *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) {
|
||||
srcVolId := req.GetSourceVolumeId()
|
||||
snapshotId := req.GetSnapshotId()
|
||||
maxEntries := req.GetMaxEntries()
|
||||
startingToken := req.GetStartingToken()
|
||||
|
||||
var entries []*csi.ListSnapshotsResponse_Entry
|
||||
var nextToken string = ""
|
||||
|
||||
if 0 > maxEntries {
|
||||
return nil, status.Error(codes.InvalidArgument, "Max entries can not be negative.")
|
||||
}
|
||||
|
||||
pagingSkip := ("" != startingToken)
|
||||
var snapshotInfos []webapi.SnapshotInfo
|
||||
var err error
|
||||
|
||||
if (srcVolId != "") {
|
||||
snapshotInfos, err = cs.dsmService.ListSnapshots(srcVolId)
|
||||
} else {
|
||||
snapshotInfos, err = cs.dsmService.ListAllSnapshots()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, fmt.Sprintf("Failed to ListSnapshots(%s), err: %v", srcVolId, err))
|
||||
}
|
||||
|
||||
var count int32 = 0
|
||||
for _, snapshotInfo := range snapshotInfos {
|
||||
if snapshotInfo.Uuid == startingToken {
|
||||
pagingSkip = false
|
||||
}
|
||||
|
||||
if pagingSkip {
|
||||
continue
|
||||
}
|
||||
|
||||
if snapshotId != "" && snapshotInfo.Uuid != snapshotId {
|
||||
continue
|
||||
}
|
||||
|
||||
if maxEntries > 0 && count >= maxEntries {
|
||||
nextToken = snapshotInfo.Uuid
|
||||
break
|
||||
}
|
||||
|
||||
createTime, err := ptypes.TimestampProto(time.Unix(snapshotInfo.CreateTime, 0))
|
||||
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, fmt.Sprintf("Failed to convert create time, err: %v", err))
|
||||
}
|
||||
|
||||
entries = append(entries, &csi.ListSnapshotsResponse_Entry{
|
||||
Snapshot: &csi.Snapshot{
|
||||
SizeBytes: snapshotInfo.TotalSize,
|
||||
SnapshotId: snapshotInfo.Uuid,
|
||||
SourceVolumeId: snapshotInfo.ParentUuid,
|
||||
CreationTime: createTime,
|
||||
ReadyToUse: (snapshotInfo.Status == "Healthy"),
|
||||
},
|
||||
})
|
||||
|
||||
count++
|
||||
}
|
||||
|
||||
if pagingSkip {
|
||||
return nil, status.Errorf(codes.Aborted, fmt.Sprintf("Invalid StartingToken(%s)", startingToken))
|
||||
}
|
||||
|
||||
return &csi.ListSnapshotsResponse{
|
||||
Entries: entries,
|
||||
NextToken: nextToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cs *controllerServer) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) {
|
||||
volumeId, capRange := req.GetVolumeId(), req.GetCapacityRange()
|
||||
|
||||
if volumeId == "" || capRange == nil {
|
||||
return nil, status.Error(codes.InvalidArgument,
|
||||
"InvalidArgument: Please check volume ID and capacity range.")
|
||||
}
|
||||
|
||||
sizeInByte, err := getSizeByCapacityRange(capRange)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument,
|
||||
"InvalidArgument: Please check CapacityRange[%v]", capRange)
|
||||
}
|
||||
|
||||
if err := cs.dsmService.ExpandLun(volumeId, sizeInByte); err != nil {
|
||||
return nil, status.Error(codes.Internal,
|
||||
fmt.Sprintf("Failed to expand volume [%s], err: %v", volumeId, err))
|
||||
}
|
||||
|
||||
return &csi.ControllerExpandVolumeResponse{
|
||||
CapacityBytes: sizeInByte,
|
||||
NodeExpansionRequired: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cs *controllerServer) ControllerGetVolume(ctx context.Context, req *csi.ControllerGetVolumeRequest) (*csi.ControllerGetVolumeResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
129
pkg/driver/driver.go
Normal file
129
pkg/driver/driver.go
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
Copyright 2021 Synology Inc.
|
||||
|
||||
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 driver
|
||||
|
||||
import (
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/interfaces"
|
||||
)
|
||||
|
||||
const (
|
||||
DriverName = "csi.san.synology.com" // CSI dirver name
|
||||
DriverVersion = "1.0.0"
|
||||
)
|
||||
|
||||
type IDriver interface {
|
||||
Activate()
|
||||
}
|
||||
|
||||
type Driver struct {
|
||||
// *csicommon.CSIDriver
|
||||
name string
|
||||
nodeID string
|
||||
version string
|
||||
endpoint string
|
||||
csCap []*csi.ControllerServiceCapability
|
||||
vCap []*csi.VolumeCapability_AccessMode
|
||||
nsCap []*csi.NodeServiceCapability
|
||||
DsmService interfaces.IDsmService
|
||||
}
|
||||
|
||||
func NewControllerAndNodeDriver(nodeID string, endpoint string, dsmService interfaces.IDsmService) (*Driver, error) {
|
||||
log.Debugf("NewControllerAndNodeDriver: DriverName: %v, DriverVersion: %v", DriverName, DriverVersion)
|
||||
|
||||
// TODO version format and validation
|
||||
d := &Driver{
|
||||
name: DriverName,
|
||||
version: DriverVersion,
|
||||
nodeID: nodeID,
|
||||
endpoint: endpoint,
|
||||
DsmService: dsmService,
|
||||
}
|
||||
|
||||
d.addControllerServiceCapabilities([]csi.ControllerServiceCapability_RPC_Type{
|
||||
csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
|
||||
csi.ControllerServiceCapability_RPC_LIST_VOLUMES,
|
||||
csi.ControllerServiceCapability_RPC_EXPAND_VOLUME,
|
||||
csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT,
|
||||
csi.ControllerServiceCapability_RPC_LIST_SNAPSHOTS,
|
||||
csi.ControllerServiceCapability_RPC_CLONE_VOLUME,
|
||||
csi.ControllerServiceCapability_RPC_GET_CAPACITY,
|
||||
})
|
||||
|
||||
d.addVolumeCapabilityAccessModes([]csi.VolumeCapability_AccessMode_Mode{
|
||||
csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
|
||||
csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,
|
||||
})
|
||||
|
||||
d.addNodeServiceCapabilities([]csi.NodeServiceCapability_RPC_Type{
|
||||
csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME,
|
||||
csi.NodeServiceCapability_RPC_EXPAND_VOLUME,
|
||||
})
|
||||
|
||||
log.Infof("New driver created: name=%s, nodeID=%s, version=%s, endpoint=%s", d.name, d.nodeID, d.version, d.endpoint)
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// TODO: func NewNodeDriver() {}
|
||||
// TODO: func NewControllerDriver() {}
|
||||
|
||||
func (d *Driver) Activate() {
|
||||
go func() {
|
||||
RunControllerandNodePublishServer(d.endpoint, d, NewControllerServer(d), NewNodeServer(d))
|
||||
}()
|
||||
}
|
||||
|
||||
func (d *Driver) addControllerServiceCapabilities(cl []csi.ControllerServiceCapability_RPC_Type) {
|
||||
var csc []*csi.ControllerServiceCapability
|
||||
|
||||
for _, c := range cl {
|
||||
log.Debugf("Enabling controller service capability: %v", c.String())
|
||||
csc = append(csc, NewControllerServiceCapability(c))
|
||||
}
|
||||
|
||||
d.csCap = csc
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Driver) addVolumeCapabilityAccessModes(vc []csi.VolumeCapability_AccessMode_Mode) {
|
||||
var vca []*csi.VolumeCapability_AccessMode
|
||||
|
||||
for _, c := range vc {
|
||||
log.Debugf("Enabling volume access mode: %v", c.String())
|
||||
vca = append(vca, NewVolumeCapabilityAccessMode(c))
|
||||
}
|
||||
|
||||
d.vCap = vca
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Driver) addNodeServiceCapabilities(nsc []csi.NodeServiceCapability_RPC_Type) {
|
||||
var nca []*csi.NodeServiceCapability
|
||||
|
||||
for _, c := range nsc {
|
||||
log.Debugf("Enabling node service capability: %v", c.String())
|
||||
nca = append(nca, NewNodeServiceCapability(c))
|
||||
}
|
||||
|
||||
d.nsCap = nca
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Driver) getVolumeCapabilityAccessModes() []*csi.VolumeCapability_AccessMode { // for debugging
|
||||
return d.vCap
|
||||
}
|
||||
114
pkg/driver/grpc.go
Normal file
114
pkg/driver/grpc.go
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright 2021 Synology Inc.
|
||||
|
||||
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 driver
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
)
|
||||
|
||||
// Defines Non blocking GRPC server interfaces
|
||||
type NonBlockingGRPCServer interface {
|
||||
// Start services at the endpoint
|
||||
Start(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer)
|
||||
// Waits for the service to stop
|
||||
Wait()
|
||||
// Stops the service gracefully
|
||||
Stop()
|
||||
// Stops the service forcefully
|
||||
ForceStop()
|
||||
}
|
||||
|
||||
func NewNonBlockingGRPCServer() NonBlockingGRPCServer {
|
||||
return &nonBlockingGRPCServer{}
|
||||
}
|
||||
|
||||
// NonBlocking server
|
||||
type nonBlockingGRPCServer struct {
|
||||
wg sync.WaitGroup
|
||||
server *grpc.Server
|
||||
}
|
||||
|
||||
func (s *nonBlockingGRPCServer) Start(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) {
|
||||
|
||||
s.wg.Add(1)
|
||||
|
||||
go s.serve(endpoint, ids, cs, ns)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *nonBlockingGRPCServer) Wait() {
|
||||
s.wg.Wait()
|
||||
}
|
||||
|
||||
func (s *nonBlockingGRPCServer) Stop() {
|
||||
s.server.GracefulStop()
|
||||
}
|
||||
|
||||
func (s *nonBlockingGRPCServer) ForceStop() {
|
||||
s.server.Stop()
|
||||
}
|
||||
|
||||
func (s *nonBlockingGRPCServer) serve(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) {
|
||||
|
||||
proto, addr, err := ParseEndpoint(endpoint)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if proto == "unix" {
|
||||
addr = "/" + addr
|
||||
if err := os.Remove(addr); err != nil && !os.IsNotExist(err) {
|
||||
log.Fatalf("Failed to remove %s, error: %s", addr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
listener, err := net.Listen(proto, addr)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to listen: %v", err)
|
||||
}
|
||||
|
||||
opts := []grpc.ServerOption{
|
||||
grpc.UnaryInterceptor(logGRPC),
|
||||
}
|
||||
server := grpc.NewServer(opts...)
|
||||
s.server = server
|
||||
|
||||
if ids != nil {
|
||||
csi.RegisterIdentityServer(server, ids)
|
||||
}
|
||||
if cs != nil {
|
||||
csi.RegisterControllerServer(server, cs)
|
||||
}
|
||||
if ns != nil {
|
||||
csi.RegisterNodeServer(server, ns)
|
||||
}
|
||||
|
||||
log.Infof("Listening for connections on address: %#v", listener.Addr())
|
||||
if err := server.Serve(listener); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
61
pkg/driver/identityserver.go
Normal file
61
pkg/driver/identityserver.go
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2021 Synology Inc.
|
||||
|
||||
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 driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
)
|
||||
|
||||
type identityServer struct {
|
||||
Driver *Driver
|
||||
}
|
||||
|
||||
func (ids *identityServer) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) {
|
||||
if ids.Driver.name == "" {
|
||||
return nil, status.Error(codes.Unavailable, "Driver name not configured")
|
||||
}
|
||||
|
||||
if ids.Driver.version == "" {
|
||||
return nil, status.Error(codes.Unavailable, "Driver is missing version")
|
||||
}
|
||||
|
||||
return &csi.GetPluginInfoResponse{
|
||||
Name: ids.Driver.name,
|
||||
VendorVersion: ids.Driver.version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ids *identityServer) Probe(ctx context.Context, req *csi.ProbeRequest) (*csi.ProbeResponse, error) {
|
||||
return &csi.ProbeResponse{}, nil
|
||||
}
|
||||
|
||||
func (ids *identityServer) GetPluginCapabilities(ctx context.Context, req *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) {
|
||||
return &csi.GetPluginCapabilitiesResponse{
|
||||
Capabilities: []*csi.PluginCapability{
|
||||
{
|
||||
Type: &csi.PluginCapability_Service_{
|
||||
Service: &csi.PluginCapability_Service{
|
||||
Type: csi.PluginCapability_Service_CONTROLLER_SERVICE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
162
pkg/driver/initiator.go
Normal file
162
pkg/driver/initiator.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// Copyright 2021 Synology Inc.
|
||||
|
||||
package driver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
utilexec "k8s.io/utils/exec"
|
||||
)
|
||||
|
||||
type initiatorDriver struct {
|
||||
chapUser string
|
||||
chapPassword string
|
||||
}
|
||||
|
||||
const (
|
||||
ISCSIPort = 3260
|
||||
)
|
||||
|
||||
func iscsiadm(cmdArgs ...string) utilexec.Cmd {
|
||||
executor := utilexec.New()
|
||||
|
||||
return executor.Command("iscsiadm", cmdArgs...)
|
||||
}
|
||||
|
||||
func iscsiadm_session() string {
|
||||
cmd := iscsiadm("-m", "session")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
exitErr, ok := err.(utilexec.ExitError)
|
||||
if ok && exitErr.ExitStatus() == 21 { // iscsiadm: No active sessions
|
||||
log.Info("No active iscsi session found.")
|
||||
} else {
|
||||
log.Errorf("Failed to run iscsiadm session: %v", err)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func iscsiadm_discovery(ip string) error {
|
||||
cmd := iscsiadm(
|
||||
"-m", "discoverydb",
|
||||
"--type", "sendtargets",
|
||||
"--portal", ip,
|
||||
"--discover")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s (%v)", string(out), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func iscsiadm_login(iqn, portal string) error {
|
||||
cmd := iscsiadm(
|
||||
"-m", "node",
|
||||
"--targetname", iqn,
|
||||
"--portal", portal,
|
||||
"--login")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s (%v)", string(out), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func iscsiadm_logout(iqn string) error {
|
||||
cmd := iscsiadm(
|
||||
"-m", "node",
|
||||
"--targetname", iqn,
|
||||
"--logout")
|
||||
if _, err := cmd.CombinedOutput(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func iscsiadm_rescan(iqn string) error {
|
||||
cmd := iscsiadm(
|
||||
"-m", "node",
|
||||
"--targetname", iqn,
|
||||
"-R")
|
||||
if _, err := cmd.CombinedOutput(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasSession(targetIqn string) bool{
|
||||
sessions := iscsiadm_session();
|
||||
|
||||
for _, line := range strings.Split(sessions, "\n") {
|
||||
if strings.Contains(line, targetIqn) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *initiatorDriver) login(targetIqn string, ip string) error{
|
||||
portal := fmt.Sprintf("%s:%d", ip, ISCSIPort)
|
||||
|
||||
if (hasSession(targetIqn)) {
|
||||
log.Infof("Session[%s] already exists.", targetIqn)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := iscsiadm_discovery(ip); err != nil {
|
||||
log.Errorf("Failed in discovery of the target: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := iscsiadm_login(targetIqn, portal); err != nil {
|
||||
log.Errorf("Failed in login of the target: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Login target portal [%s], iqn [%s].", portal, targetIqn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *initiatorDriver) logout(targetIqn string, ip string) error{
|
||||
if (!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 {
|
||||
log.Errorf("Failed in logout of the target.\nTarget [%s], Portal [%s], Err[%v]",
|
||||
targetIqn, portal, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Logout target portal [%s], iqn [%s].", portal, targetIqn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *initiatorDriver) rescan(targetIqn string) error{
|
||||
if (!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 {
|
||||
log.Errorf("Failed in rescan of the target.\nTarget [%s], Err[%v]",
|
||||
targetIqn, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Rescan target iqn [%s].", targetIqn)
|
||||
|
||||
return nil
|
||||
}
|
||||
403
pkg/driver/nodeserver.go
Normal file
403
pkg/driver/nodeserver.go
Normal file
@@ -0,0 +1,403 @@
|
||||
/*
|
||||
Copyright 2021 Synology Inc.
|
||||
|
||||
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 driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"k8s.io/mount-utils"
|
||||
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/interfaces"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/utils"
|
||||
)
|
||||
|
||||
type nodeServer struct {
|
||||
Driver *Driver
|
||||
Mounter *mount.SafeFormatAndMount
|
||||
dsmService interfaces.IDsmService
|
||||
Initiator *initiatorDriver
|
||||
}
|
||||
|
||||
func getExistedDevicePath(paths []string) string {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
timer := time.NewTimer(60 * time.Second)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
for _, path := range paths {
|
||||
exists, err := mount.PathExists(path)
|
||||
if err == nil && exists == true {
|
||||
return path
|
||||
} else {
|
||||
log.Errorf("Can't find device path [%s], err: %v", path, err)
|
||||
}
|
||||
}
|
||||
case <-timer.C:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *nodeServer) getVolumeMountPath(volumeId string) string {
|
||||
paths := []string{}
|
||||
|
||||
k8sVolume := ns.dsmService.GetVolume(volumeId)
|
||||
if k8sVolume == nil {
|
||||
log.Errorf("Failed to get Volume id:%d.")
|
||||
return ""
|
||||
}
|
||||
// Assume target and lun 1-1 mapping
|
||||
mappingIndex := k8sVolume.Target.MappedLuns[0].MappingIndex
|
||||
|
||||
ips, err := utils.LookupIPv4(k8sVolume.DsmIp)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to lookup ipv4 for host: %s", k8sVolume.DsmIp)
|
||||
paths = append(paths, fmt.Sprintf("%sip-%s:3260-iscsi-%s-lun-%d", "/dev/disk/by-path/", k8sVolume.DsmIp, k8sVolume.Target.Iqn, mappingIndex))
|
||||
} else {
|
||||
for _, ipv4 := range ips {
|
||||
paths = append(paths, fmt.Sprintf("%sip-%s:3260-iscsi-%s-lun-%d", "/dev/disk/by-path/", ipv4, k8sVolume.Target.Iqn, mappingIndex))
|
||||
}
|
||||
}
|
||||
|
||||
path := getExistedDevicePath(paths)
|
||||
if path == "" {
|
||||
log.Errorf("Volume mount path is not exist.")
|
||||
return ""
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func createTargetMountPath(mounter mount.Interface, mountPath string, isBlock bool) (bool, error) {
|
||||
notMount, err := mount.IsNotMountPoint(mounter, mountPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if isBlock {
|
||||
pathFile, err := os.OpenFile(mountPath, os.O_CREATE|os.O_RDWR, 0750)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create mountPath:%s with error: %v", mountPath, err)
|
||||
return notMount, err
|
||||
}
|
||||
if err = pathFile.Close(); err != nil {
|
||||
log.Errorf("Failed to close mountPath:%s with error: %v", mountPath, err)
|
||||
return notMount, err
|
||||
}
|
||||
} else {
|
||||
err = os.MkdirAll(mountPath, 0750)
|
||||
if err != nil {
|
||||
return notMount, err
|
||||
}
|
||||
}
|
||||
notMount = true
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return notMount, nil
|
||||
}
|
||||
|
||||
func (ns *nodeServer) loginTarget(volumeId string) error {
|
||||
k8sVolume := ns.dsmService.GetVolume(volumeId);
|
||||
|
||||
if k8sVolume == nil {
|
||||
return status.Error(codes.NotFound, fmt.Sprintf("Volume[%s] is not found", volumeId))
|
||||
}
|
||||
|
||||
if err := ns.Initiator.login(k8sVolume.Target.Iqn, k8sVolume.DsmIp); err != nil {
|
||||
return status.Errorf(codes.Internal,
|
||||
fmt.Sprintf("Failed to login with target iqn [%s], err: %v", k8sVolume.Target.Iqn, err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *nodeServer) logoutTarget(volumeId string) {
|
||||
k8sVolume := ns.dsmService.GetVolume(volumeId)
|
||||
|
||||
if k8sVolume == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ns.Initiator.logout(k8sVolume.Target.Iqn, k8sVolume.DsmIp)
|
||||
}
|
||||
|
||||
func (ns *nodeServer) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) {
|
||||
volumeId, stagingTargetPath, volumeCapability :=
|
||||
req.GetVolumeId(), req.GetStagingTargetPath(), req.GetVolumeCapability()
|
||||
|
||||
if volumeId == "" || stagingTargetPath == "" || volumeCapability == nil {
|
||||
return nil, status.Error(codes.InvalidArgument,
|
||||
"InvalidArgument: Please check volume ID, staging target path and volume capability.")
|
||||
}
|
||||
|
||||
// if block mode, skip mount
|
||||
if volumeCapability.GetBlock() != nil {
|
||||
return &csi.NodeStageVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
if err := ns.loginTarget(volumeId); err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
volumeMountPath := ns.getVolumeMountPath(volumeId)
|
||||
if volumeMountPath == "" {
|
||||
return nil, status.Error(codes.Internal, "Can't get volume mount path")
|
||||
}
|
||||
|
||||
notMount, err := ns.Mounter.Interface.IsLikelyNotMountPoint(stagingTargetPath)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
if !notMount {
|
||||
return &csi.NodeStageVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
fsType := volumeCapability.GetMount().GetFsType()
|
||||
mountFlags := volumeCapability.GetMount().GetMountFlags()
|
||||
options := append([]string{"rw"}, mountFlags...)
|
||||
|
||||
if err = ns.Mounter.FormatAndMount(volumeMountPath, stagingTargetPath, fsType, options); err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return &csi.NodeStageVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
func (ns *nodeServer) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) {
|
||||
if req.GetVolumeId() == "" { // Useless, just for sanity check
|
||||
return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request")
|
||||
}
|
||||
|
||||
stagingTargetPath := req.GetStagingTargetPath()
|
||||
|
||||
if stagingTargetPath == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "Target path missing in request")
|
||||
}
|
||||
|
||||
notMount, err := mount.IsNotMountPoint(ns.Mounter.Interface, stagingTargetPath)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
if !notMount {
|
||||
err = ns.Mounter.Interface.Unmount(stagingTargetPath)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return &csi.NodeUnstageVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) {
|
||||
volumeId, targetPath, stagingTargetPath := req.GetVolumeId(), req.GetTargetPath(), req.GetStagingTargetPath()
|
||||
isBlock := req.GetVolumeCapability().GetBlock() != nil
|
||||
|
||||
if volumeId == "" || targetPath == "" || stagingTargetPath == "" {
|
||||
return nil, status.Error(codes.InvalidArgument,
|
||||
"InvalidArgument: Please check volume ID, target path and staging target path.")
|
||||
}
|
||||
|
||||
notMount, err := createTargetMountPath(ns.Mounter.Interface, targetPath, isBlock)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
if !notMount {
|
||||
return &csi.NodePublishVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
if err := ns.loginTarget(volumeId); err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
volumeMountPath := ns.getVolumeMountPath(volumeId)
|
||||
if volumeMountPath == "" {
|
||||
return nil, status.Error(codes.Internal, "Can't get volume mount path")
|
||||
}
|
||||
|
||||
options := []string{"bind"}
|
||||
if req.GetReadonly() {
|
||||
options = append(options, "ro")
|
||||
}
|
||||
|
||||
if isBlock {
|
||||
err = ns.Mounter.Interface.Mount(volumeMountPath, targetPath, "", options)
|
||||
} else {
|
||||
fsType := req.GetVolumeCapability().GetMount().GetFsType()
|
||||
err = ns.Mounter.Interface.Mount(stagingTargetPath, targetPath, fsType, options)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return &csi.NodePublishVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
func (ns *nodeServer) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) {
|
||||
volumeId, targetPath := req.GetVolumeId(), req.GetTargetPath()
|
||||
if volumeId == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request")
|
||||
}
|
||||
if targetPath == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "Target path missing in request")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(targetPath); err != nil {
|
||||
if os.IsNotExist(err){
|
||||
return &csi.NodeUnpublishVolumeResponse{}, nil
|
||||
}
|
||||
return nil, status.Errorf(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
notMount, err := mount.IsNotMountPoint(ns.Mounter.Interface, targetPath)
|
||||
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
if notMount {
|
||||
return &csi.NodeUnpublishVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
needToLogout := true
|
||||
|
||||
list, err := ns.Mounter.Interface.GetMountRefs(targetPath)
|
||||
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
for _, path := range list {
|
||||
filePrefix := "/var/lib/kubelet/pods/"
|
||||
blkPrefix := "/var/lib/kubelet/plugins/kubernetes.io/csi/volumeDevices/publish/"
|
||||
|
||||
if strings.HasPrefix(path, filePrefix) || strings.HasPrefix(path, blkPrefix) {
|
||||
needToLogout = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := ns.Mounter.Interface.Unmount(targetPath); err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
if err := os.Remove(targetPath); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "Failed to remove target path.")
|
||||
}
|
||||
|
||||
if needToLogout {
|
||||
ns.logoutTarget(volumeId)
|
||||
}
|
||||
|
||||
return &csi.NodeUnpublishVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
func (ns *nodeServer) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) {
|
||||
log.Debugf("Using default NodeGetInfo, ns.Driver.nodeID = [%s]", ns.Driver.nodeID)
|
||||
|
||||
return &csi.NodeGetInfoResponse{
|
||||
NodeId: ns.Driver.nodeID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ns *nodeServer) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) {
|
||||
return &csi.NodeGetCapabilitiesResponse{
|
||||
Capabilities: ns.Driver.nsCap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ns *nodeServer) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) {
|
||||
volumeId, volumePath := req.GetVolumeId(), req.GetVolumePath()
|
||||
if volumeId == "" || volumePath == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "Invalid Argument")
|
||||
}
|
||||
|
||||
k8sVolume := ns.dsmService.GetVolume(volumeId)
|
||||
if k8sVolume == nil {
|
||||
return nil, status.Error(codes.NotFound,
|
||||
fmt.Sprintf("Volume[%s] is not found", volumeId))
|
||||
}
|
||||
|
||||
notMount, err := mount.IsNotMountPoint(ns.Mounter.Interface, volumePath)
|
||||
if err != nil || notMount {
|
||||
return nil, status.Error(codes.NotFound,
|
||||
fmt.Sprintf("Volume[%s] does not exist on the %s", volumeId, volumePath))
|
||||
}
|
||||
|
||||
lun := k8sVolume.Lun
|
||||
|
||||
return &csi.NodeGetVolumeStatsResponse{
|
||||
Usage: []*csi.VolumeUsage{
|
||||
&csi.VolumeUsage{
|
||||
Available: int64(lun.Size - lun.Used),
|
||||
Total: int64(lun.Size),
|
||||
Used: int64(lun.Used),
|
||||
Unit: csi.VolumeUsage_BYTES,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ns *nodeServer) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) {
|
||||
volumeId, volumePath := req.GetVolumeId(), req.GetVolumePath()
|
||||
sizeInByte, err := getSizeByCapacityRange(req.GetCapacityRange())
|
||||
if volumeId == "" || volumePath == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "InvalidArgument: Please check volume ID and volume path.")
|
||||
}
|
||||
|
||||
k8sVolume := ns.dsmService.GetVolume(volumeId)
|
||||
if k8sVolume == nil {
|
||||
return nil, status.Error(codes.NotFound, fmt.Sprintf("Volume[%s] is not found", volumeId))
|
||||
}
|
||||
|
||||
if err := ns.Initiator.rescan(k8sVolume.Target.Iqn); err != nil {
|
||||
return nil, status.Error(codes.Internal, fmt.Sprintf("Failed to rescan. err: %v", err))
|
||||
}
|
||||
|
||||
isBlock := req.GetVolumeCapability() != nil && req.GetVolumeCapability().GetBlock() != nil
|
||||
if isBlock {
|
||||
return &csi.NodeExpandVolumeResponse{
|
||||
CapacityBytes: sizeInByte}, nil
|
||||
}
|
||||
|
||||
volumeMountPath := ns.getVolumeMountPath(volumeId)
|
||||
if volumeMountPath == "" {
|
||||
return nil, status.Error(codes.Internal, "Can't get volume mount path")
|
||||
}
|
||||
|
||||
ok, err := mount.NewResizeFs(ns.Mounter.Exec).Resize(volumeMountPath, volumePath)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
if !ok {
|
||||
return nil, status.Error(codes.Internal, "Failed to expand volume filesystem")
|
||||
}
|
||||
return &csi.NodeExpandVolumeResponse{
|
||||
CapacityBytes: sizeInByte}, nil
|
||||
}
|
||||
113
pkg/driver/utils.go
Normal file
113
pkg/driver/utils.go
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
Copyright 2021 Synology Inc.
|
||||
|
||||
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 driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/kubernetes-csi/csi-lib-utils/protosanitizer"
|
||||
"google.golang.org/grpc"
|
||||
"k8s.io/utils/exec"
|
||||
"k8s.io/mount-utils"
|
||||
)
|
||||
|
||||
func ParseEndpoint(ep string) (string, string, error) {
|
||||
if strings.HasPrefix(strings.ToLower(ep), "unix://") || strings.HasPrefix(strings.ToLower(ep), "tcp://") {
|
||||
s := strings.SplitN(ep, "://", 2)
|
||||
if s[1] != "" {
|
||||
return s[0], s[1], nil
|
||||
}
|
||||
}
|
||||
return "", "", fmt.Errorf("Invalid endpoint: %v", ep)
|
||||
}
|
||||
|
||||
func NewControllerServer(d *Driver) *controllerServer {
|
||||
return &controllerServer{
|
||||
Driver: d,
|
||||
dsmService: d.DsmService,
|
||||
}
|
||||
}
|
||||
|
||||
func NewNodeServer(d *Driver) *nodeServer {
|
||||
return &nodeServer{
|
||||
Driver: d,
|
||||
dsmService: d.DsmService,
|
||||
Mounter: &mount.SafeFormatAndMount{
|
||||
Interface: mount.New(""),
|
||||
Exec: exec.New(),
|
||||
},
|
||||
Initiator: &initiatorDriver{
|
||||
chapUser: "",
|
||||
chapPassword: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewIdentityServer(d *Driver) *identityServer {
|
||||
return &identityServer{
|
||||
Driver: d,
|
||||
}
|
||||
}
|
||||
|
||||
func NewVolumeCapabilityAccessMode(mode csi.VolumeCapability_AccessMode_Mode) *csi.VolumeCapability_AccessMode {
|
||||
return &csi.VolumeCapability_AccessMode{Mode: mode}
|
||||
}
|
||||
|
||||
func NewControllerServiceCapability(cap csi.ControllerServiceCapability_RPC_Type) *csi.ControllerServiceCapability {
|
||||
return &csi.ControllerServiceCapability{
|
||||
Type: &csi.ControllerServiceCapability_Rpc{
|
||||
Rpc: &csi.ControllerServiceCapability_RPC{
|
||||
Type: cap,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewNodeServiceCapability(cap csi.NodeServiceCapability_RPC_Type) *csi.NodeServiceCapability {
|
||||
return &csi.NodeServiceCapability{
|
||||
Type: &csi.NodeServiceCapability_Rpc{
|
||||
Rpc: &csi.NodeServiceCapability_RPC{
|
||||
Type: cap,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunControllerandNodePublishServer(endpoint string, d *Driver, cs csi.ControllerServer, ns csi.NodeServer) {
|
||||
ids := NewIdentityServer(d)
|
||||
|
||||
s := NewNonBlockingGRPCServer()
|
||||
s.Start(endpoint, ids, cs, ns)
|
||||
}
|
||||
|
||||
func logGRPC(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
log.Infof("GRPC call: %s", info.FullMethod)
|
||||
log.Infof("GRPC request: %s", protosanitizer.StripSecrets(req))
|
||||
resp, err := handler(ctx, req)
|
||||
if err != nil {
|
||||
log.Errorf("GRPC error: %v", err)
|
||||
} else {
|
||||
log.Infof("GRPC response: %s", protosanitizer.StripSecrets(resp))
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
40
pkg/dsm/common/config.go
Normal file
40
pkg/dsm/common/config.go
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2021 Synology Inc.
|
||||
*/
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"gopkg.in/yaml.v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ClientInfo struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Https bool `yaml:"https"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
}
|
||||
|
||||
type SynoInfo struct {
|
||||
Clients []ClientInfo `yaml:"clients"`
|
||||
}
|
||||
|
||||
func LoadConfig(configPath string) (*SynoInfo, error) {
|
||||
file, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to open config file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := SynoInfo{}
|
||||
err = yaml.Unmarshal(file, &info)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to parse config: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &info, nil
|
||||
}
|
||||
624
pkg/dsm/service/dsm.go
Normal file
624
pkg/dsm/service/dsm.go
Normal file
@@ -0,0 +1,624 @@
|
||||
/*
|
||||
* Copyright 2021 Synology Inc.
|
||||
*/
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"strconv"
|
||||
"time"
|
||||
"strings"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/common"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/webapi"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/models"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/utils"
|
||||
)
|
||||
|
||||
type DsmService struct {
|
||||
dsms map[string]*webapi.DSM
|
||||
}
|
||||
|
||||
func NewDsmService() *DsmService {
|
||||
return &DsmService{
|
||||
dsms: make(map[string]*webapi.DSM),
|
||||
}
|
||||
}
|
||||
|
||||
func (service *DsmService) AddDsm(client common.ClientInfo) error {
|
||||
// TODO: use sn or other identifiers as key
|
||||
if _, ok := service.dsms[client.Host]; ok {
|
||||
log.Infof("Adding DSM [%s] already present.", client.Host)
|
||||
return nil
|
||||
}
|
||||
|
||||
dsm := &webapi.DSM{
|
||||
Ip: client.Host,
|
||||
Port: client.Port,
|
||||
Username: client.Username,
|
||||
Password: client.Password,
|
||||
Https: client.Https,
|
||||
}
|
||||
err := dsm.Login()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to login to DSM: [%s]. err: %v", dsm.Ip, err)
|
||||
}
|
||||
service.dsms[dsm.Ip] = dsm
|
||||
log.Infof("Add DSM [%s].", dsm.Ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *DsmService) RemoveAllDsms() {
|
||||
for _, dsm := range service.dsms {
|
||||
log.Infof("Going to logout DSM [%s]", dsm.Ip)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
err := dsm.Logout()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
log.Debugf("Retry to logout DSM [%s], retry: %d", dsm.Ip, i)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (service *DsmService) GetDsm(ip string) (*webapi.DSM, error) {
|
||||
dsm, ok := service.dsms[ip]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Requested dsm [%s] does not exist", ip)
|
||||
}
|
||||
return dsm, nil
|
||||
}
|
||||
|
||||
func (service *DsmService) GetDsmsCount() int {
|
||||
return len(service.dsms)
|
||||
}
|
||||
|
||||
func (service *DsmService) ListDsmVolumes(ip string) ([]webapi.VolInfo, error) {
|
||||
var allVolInfos []webapi.VolInfo
|
||||
|
||||
for _, dsm := range service.dsms {
|
||||
if ip != "" && dsm.Ip != ip {
|
||||
continue
|
||||
}
|
||||
|
||||
volInfos, err := dsm.VolumeList()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
allVolInfos = append(allVolInfos, volInfos...)
|
||||
}
|
||||
|
||||
return allVolInfos, nil
|
||||
}
|
||||
|
||||
func (service *DsmService) getFirstAvailableVolume(dsm *webapi.DSM, sizeInBytes int64) (webapi.VolInfo, error) {
|
||||
volInfos, err := dsm.VolumeList()
|
||||
if err != nil {
|
||||
return webapi.VolInfo{}, err
|
||||
}
|
||||
|
||||
for _, volInfo := range volInfos {
|
||||
free, err := strconv.ParseInt(volInfo.Free, 10, 64)
|
||||
if err != nil {
|
||||
return webapi.VolInfo{}, err
|
||||
}
|
||||
if free < utils.UNIT_GB {
|
||||
continue
|
||||
}
|
||||
if volInfo.Status == "crashed" || volInfo.Status == "read_only" || volInfo.Status == "deleting" || free <= sizeInBytes {
|
||||
continue
|
||||
}
|
||||
// ignore esata disk
|
||||
if volInfo.Container == "external" && volInfo.Location == "sata" {
|
||||
continue
|
||||
}
|
||||
return volInfo, nil
|
||||
}
|
||||
return webapi.VolInfo{}, fmt.Errorf("Cannot find any available volume")
|
||||
}
|
||||
|
||||
func getLunTypeByInputParams(lunType string, isThin bool, locationFsType string) (string, error) {
|
||||
log.Debugf("Input lunType: %v, isThin: %v, locationFsType: %v", lunType, isThin, locationFsType)
|
||||
if lunType != "" {
|
||||
return lunType, nil
|
||||
}
|
||||
|
||||
if locationFsType == models.FsTypeExt4 {
|
||||
if isThin {
|
||||
return models.LunTypeAdv, nil // ADV
|
||||
} else {
|
||||
return models.LunTypeFile, nil // FILE
|
||||
}
|
||||
} else if locationFsType == models.FsTypeBtrfs {
|
||||
if isThin {
|
||||
return models.LunTypeBlun, nil // BLUN
|
||||
} else {
|
||||
return models.LunTypeBlunThick, nil // BLUN_THICK
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Unknown volume fs type: %s", locationFsType)
|
||||
}
|
||||
|
||||
func (service *DsmService) createMappingTarget(dsm *webapi.DSM, spec *models.CreateK8sVolumeSpec, lunUuid string) error {
|
||||
dsmInfo, err := dsm.DsmInfoGet()
|
||||
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, fmt.Sprintf("Failed to get DSM[%s] info", dsm.Ip));
|
||||
}
|
||||
|
||||
genTargetIqn := func() string {
|
||||
iqn := models.IqnPrefix + fmt.Sprintf("%s.%s", dsmInfo.Hostname, spec.K8sVolumeName)
|
||||
iqn = strings.ReplaceAll(iqn, "_", "-")
|
||||
iqn = strings.ReplaceAll(iqn, "+", "p")
|
||||
|
||||
if len(iqn) > models.MaxIqnLen {
|
||||
return iqn[:models.MaxIqnLen]
|
||||
}
|
||||
return iqn
|
||||
}
|
||||
targetSpec := webapi.TargetCreateSpec{
|
||||
Name: spec.TargetName,
|
||||
Iqn: genTargetIqn(),
|
||||
}
|
||||
|
||||
log.Debugf("TargetCreate spec: %v", targetSpec)
|
||||
targetId, err := dsm.TargetCreate(targetSpec)
|
||||
|
||||
if err != nil && !errors.Is(err, utils.AlreadyExistError("")) {
|
||||
return status.Errorf(codes.Internal, fmt.Sprintf("Failed to create target with spec: %v, err: %v", targetSpec, err))
|
||||
}
|
||||
|
||||
if targetInfo, err := dsm.TargetGet(targetSpec.Name); err != nil {
|
||||
return status.Errorf(codes.Internal, fmt.Sprintf("Failed to get target with spec: %v, err: %v", targetSpec, err))
|
||||
} else {
|
||||
targetId = strconv.Itoa(targetInfo.TargetId);
|
||||
}
|
||||
|
||||
if spec.MultipleSession == true {
|
||||
if err := dsm.TargetSet(targetId, 0); err != nil {
|
||||
return status.Errorf(codes.Internal, fmt.Sprintf("Failed to set target [%s] max session, err: %v", spec.TargetName, err))
|
||||
}
|
||||
}
|
||||
|
||||
err = dsm.LunMapTarget([]string{targetId}, lunUuid)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, fmt.Sprintf("Failed to map target [%s] to lun [%s], err: %v", spec.TargetName, lunUuid, err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *DsmService) createVolumeByDsm(dsm *webapi.DSM, spec *models.CreateK8sVolumeSpec) (webapi.LunInfo, error) {
|
||||
// 1. Find a available location
|
||||
if spec.Location == "" {
|
||||
vol, err := service.getFirstAvailableVolume(dsm, spec.Size)
|
||||
if err != nil {
|
||||
return webapi.LunInfo{},
|
||||
status.Errorf(codes.Internal, fmt.Sprintf("Failed to get available location, err: %v", err))
|
||||
}
|
||||
spec.Location = vol.Path
|
||||
}
|
||||
|
||||
// 2. Check if location exists
|
||||
dsmVolInfo, err := dsm.VolumeGet(spec.Location)
|
||||
if err != nil {
|
||||
return webapi.LunInfo{},
|
||||
status.Errorf(codes.InvalidArgument, fmt.Sprintf("Unable to find location %s", spec.Location))
|
||||
}
|
||||
|
||||
lunType, err := getLunTypeByInputParams(spec.Type, spec.ThinProvisioning, dsmVolInfo.FsType)
|
||||
if err != nil {
|
||||
return webapi.LunInfo{},
|
||||
status.Errorf(codes.InvalidArgument, fmt.Sprintf("Unknown volume fs type: %s, location: %s", dsmVolInfo.FsType, spec.Location))
|
||||
}
|
||||
|
||||
// 3. Create LUN
|
||||
lunSpec := webapi.LunCreateSpec{
|
||||
Name: spec.LunName,
|
||||
Location: spec.Location,
|
||||
Size: spec.Size,
|
||||
Type: lunType,
|
||||
}
|
||||
|
||||
log.Debugf("LunCreate spec: %v", lunSpec)
|
||||
_, err = dsm.LunCreate(lunSpec)
|
||||
|
||||
if err != nil && !errors.Is(err, utils.AlreadyExistError("")) {
|
||||
return webapi.LunInfo{},
|
||||
status.Errorf(codes.Internal, fmt.Sprintf("Failed to create Volume with name: %s, err: %v", spec.K8sVolumeName, err))
|
||||
}
|
||||
|
||||
// No matter lun existed or not, Get Lun by name
|
||||
lunInfo, err := dsm.LunGet(spec.LunName)
|
||||
if err != nil {
|
||||
return webapi.LunInfo{},
|
||||
// discussion with log
|
||||
status.Errorf(codes.Internal, fmt.Sprintf("Failed to get existed LUN with name: %s, err: %v", spec.LunName, err))
|
||||
}
|
||||
|
||||
// 4. Create Target and Map to Lun
|
||||
err = service.createMappingTarget(dsm, spec, lunInfo.Uuid)
|
||||
if err != nil {
|
||||
// FIXME need to delete lun and target
|
||||
return webapi.LunInfo{},
|
||||
status.Errorf(codes.Internal, fmt.Sprintf("Failed to create and map target, err: %v", err))
|
||||
}
|
||||
|
||||
log.Debugf("[%s] CreateVolume Successfully. VolumeId: %s", dsm.Ip, lunInfo.Uuid);
|
||||
|
||||
return lunInfo, nil
|
||||
}
|
||||
|
||||
func waitCloneFinished(dsm *webapi.DSM, lunName string) error {
|
||||
cloneBackoff := backoff.NewExponentialBackOff()
|
||||
cloneBackoff.InitialInterval = 1 * time.Second
|
||||
cloneBackoff.Multiplier = 2
|
||||
cloneBackoff.RandomizationFactor = 0.1
|
||||
cloneBackoff.MaxElapsedTime = 20 * time.Second
|
||||
|
||||
checkFinished := func() error {
|
||||
lunInfo, err := dsm.LunGet(lunName)
|
||||
if err != nil {
|
||||
return backoff.Permanent(fmt.Errorf("Failed to get existed LUN with name: %s, err: %v", lunName, err))
|
||||
}
|
||||
|
||||
if lunInfo.IsActionLocked != false {
|
||||
return fmt.Errorf("Clone not yet completed. Lun: %s", lunName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
cloneNotify := func(err error, duration time.Duration) {
|
||||
log.Infof("Lun is being locked for lun clone, waiting %3.2f seconds .....", float64(duration.Seconds()))
|
||||
}
|
||||
|
||||
if err := backoff.RetryNotify(checkFinished, cloneBackoff, cloneNotify); err != nil {
|
||||
log.Errorf("Could not finish clone after %3.2f seconds. err: %v", float64(cloneBackoff.MaxElapsedTime.Seconds()), err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Clone successfully. Lun: %v", lunName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *DsmService) createVolumeBySnapshot(dsm *webapi.DSM, spec *models.CreateK8sVolumeSpec, snapshotInfo webapi.SnapshotInfo) (webapi.LunInfo, error) {
|
||||
if spec.Size != 0 && spec.Size != snapshotInfo.TotalSize {
|
||||
return webapi.LunInfo{}, status.Errorf(codes.OutOfRange, "Lun size [%d] is not equal to snapshot size [%d]", spec.Size, snapshotInfo.TotalSize)
|
||||
}
|
||||
|
||||
snapshotCloneSpec := webapi.SnapshotCloneSpec{
|
||||
Name: spec.LunName,
|
||||
SrcLunUuid: snapshotInfo.ParentUuid,
|
||||
SrcSnapshotUuid: snapshotInfo.Uuid,
|
||||
}
|
||||
|
||||
if _, err := dsm.SnapshotClone(snapshotCloneSpec); err != nil && !errors.Is(err, utils.AlreadyExistError("")) {
|
||||
return webapi.LunInfo{},
|
||||
status.Errorf(codes.Internal, fmt.Sprintf("Failed to create volume with source snapshot ID: %s, err: %v", snapshotInfo.Uuid, err))
|
||||
}
|
||||
|
||||
if err := waitCloneFinished(dsm, spec.LunName); err != nil {
|
||||
return webapi.LunInfo{}, status.Errorf(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
lunInfo, err := dsm.LunGet(spec.LunName)
|
||||
if err != nil {
|
||||
return webapi.LunInfo{},
|
||||
status.Errorf(codes.Internal, fmt.Sprintf("Failed to get existed LUN with name: %s, err: %v", spec.LunName, err))
|
||||
}
|
||||
|
||||
err = service.createMappingTarget(dsm, spec, lunInfo.Uuid)
|
||||
if err != nil {
|
||||
// FIXME need to delete lun and target
|
||||
return webapi.LunInfo{},
|
||||
status.Errorf(codes.Internal, fmt.Sprintf("Failed to create and map target, err: %v", err))
|
||||
}
|
||||
|
||||
log.Debugf("[%s] createVolumeBySnapshot Successfully. VolumeId: %s", dsm.Ip, lunInfo.Uuid);
|
||||
|
||||
return lunInfo, nil
|
||||
}
|
||||
|
||||
func (service *DsmService) createVolumeByVolume(dsm *webapi.DSM, spec *models.CreateK8sVolumeSpec, srcLunInfo webapi.LunInfo) (webapi.LunInfo, error) {
|
||||
if spec.Size != 0 && spec.Size != int64(srcLunInfo.Size) {
|
||||
return webapi.LunInfo{}, status.Errorf(codes.OutOfRange, "Lun size [%d] is not equal to src lun size [%d]", spec.Size, srcLunInfo.Size)
|
||||
}
|
||||
|
||||
if spec.Location == "" {
|
||||
spec.Location = srcLunInfo.Location
|
||||
}
|
||||
|
||||
lunCloneSpec := webapi.LunCloneSpec{
|
||||
Name: spec.LunName,
|
||||
SrcLunUuid: srcLunInfo.Uuid,
|
||||
Location: spec.Location,
|
||||
}
|
||||
|
||||
if _, err := dsm.LunClone(lunCloneSpec); err != nil && !errors.Is(err, utils.AlreadyExistError("")) {
|
||||
return webapi.LunInfo{},
|
||||
status.Errorf(codes.Internal, fmt.Sprintf("Failed to create volume with source volume ID: %s, err: %v", srcLunInfo.Uuid, err))
|
||||
}
|
||||
|
||||
if err := waitCloneFinished(dsm, spec.LunName); err != nil {
|
||||
return webapi.LunInfo{}, status.Errorf(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
lunInfo, err := dsm.LunGet(spec.LunName)
|
||||
if err != nil {
|
||||
return webapi.LunInfo{},
|
||||
status.Errorf(codes.Internal, fmt.Sprintf("Failed to get existed LUN with name: %s, err: %v", spec.LunName, err))
|
||||
}
|
||||
|
||||
err = service.createMappingTarget(dsm, spec, lunInfo.Uuid)
|
||||
if err != nil {
|
||||
// FIXME need to delete lun and target
|
||||
return webapi.LunInfo{},
|
||||
status.Errorf(codes.Internal, fmt.Sprintf("Failed to create and map target, err: %v", err))
|
||||
}
|
||||
|
||||
log.Debugf("[%s] createVolumeByVolume Successfully. VolumeId: %s", dsm.Ip, lunInfo.Uuid);
|
||||
|
||||
return lunInfo, nil
|
||||
}
|
||||
|
||||
func (service *DsmService) CreateVolume(spec *models.CreateK8sVolumeSpec) (webapi.LunInfo, string, error) {
|
||||
if spec.SourceVolumeId != "" {
|
||||
/* Create volume by exists volume (Clone) */
|
||||
k8sVolume := service.GetVolume(spec.SourceVolumeId)
|
||||
if k8sVolume == nil {
|
||||
return webapi.LunInfo{}, "", status.Errorf(codes.NotFound, fmt.Sprintf("No such volume id: %s", spec.SourceVolumeId))
|
||||
}
|
||||
|
||||
dsm, err := service.GetDsm(k8sVolume.DsmIp)
|
||||
if err != nil {
|
||||
return webapi.LunInfo{}, "", status.Errorf(codes.Internal, fmt.Sprintf("Failed to get DSM[%s]", k8sVolume.DsmIp))
|
||||
}
|
||||
|
||||
lunInfo, err := service.createVolumeByVolume(dsm, spec, k8sVolume.Lun)
|
||||
return lunInfo, dsm.Ip, err
|
||||
} else if spec.SourceSnapshotId != "" {
|
||||
/* Create volume by snapshot */
|
||||
for _, dsm := range service.dsms {
|
||||
snapshotInfo, err := dsm.SnapshotGet(spec.SourceSnapshotId)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
lunInfo, err := service.createVolumeBySnapshot(dsm, spec, snapshotInfo)
|
||||
return lunInfo, dsm.Ip, err
|
||||
}
|
||||
return webapi.LunInfo{}, "", status.Errorf(codes.NotFound, fmt.Sprintf("No such snapshot id: %s", spec.SourceSnapshotId))
|
||||
} else if spec.DsmIp != "" {
|
||||
/* Create volume by specific dsm ip */
|
||||
dsm, err := service.GetDsm(spec.DsmIp)
|
||||
if err != nil {
|
||||
return webapi.LunInfo{}, "", status.Errorf(codes.Internal, fmt.Sprintf("%v", err))
|
||||
}
|
||||
lunInfo, err := service.createVolumeByDsm(dsm, spec)
|
||||
return lunInfo, dsm.Ip, err
|
||||
} else {
|
||||
/* Find appropriate dsm to create volume */
|
||||
for _, dsm := range service.dsms {
|
||||
lunInfo, err := service.createVolumeByDsm(dsm, spec)
|
||||
if err != nil {
|
||||
log.Errorf("[%s] Failed to create Volume: %v", dsm.Ip, err)
|
||||
continue
|
||||
}
|
||||
return lunInfo, dsm.Ip, nil
|
||||
}
|
||||
return webapi.LunInfo{}, "", status.Errorf(codes.Internal, fmt.Sprintf("Couldn't find any host available to create Volume"))
|
||||
}
|
||||
}
|
||||
|
||||
func (service *DsmService) DeleteVolume(volumeId string) error {
|
||||
k8sVolume := service.GetVolume(volumeId)
|
||||
if k8sVolume == nil {
|
||||
log.Infof("Skip delete volume[%s] that is no exist", volumeId)
|
||||
return nil
|
||||
}
|
||||
|
||||
lun, target := k8sVolume.Lun, k8sVolume.Target
|
||||
dsm, err := service.GetDsm(k8sVolume.DsmIp)
|
||||
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, fmt.Sprintf("Failed to get DSM[%s]", k8sVolume.DsmIp))
|
||||
}
|
||||
|
||||
if err := dsm.LunDelete(lun.Uuid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(target.MappedLuns) != 1 {
|
||||
log.Infof("Skip deletes target[%s] that was mapped with lun. DSM[%s]", target.Name, dsm.Ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := dsm.TargetDelete(strconv.Itoa(target.TargetId)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *DsmService) listVolumesByDsm(dsm *webapi.DSM, infos *[]*models.ListK8sVolumeRespSpec) {
|
||||
targetInfos, err := dsm.TargetList()
|
||||
if err != nil {
|
||||
log.Errorf("[%s] Failed to list targets: %v", dsm.Ip, err)
|
||||
}
|
||||
|
||||
for _, target := range targetInfos {
|
||||
// TODO: use target.ConnectedSessions to filter targets
|
||||
for _, mapping := range target.MappedLuns {
|
||||
lun, err := dsm.LunGet(mapping.LunUuid)
|
||||
if err != nil {
|
||||
log.Errorf("[%s] Failed to get LUN(%s): %v", dsm.Ip, mapping.LunUuid, err)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(lun.Name, models.LunPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
*infos = append(*infos, &models.ListK8sVolumeRespSpec{
|
||||
DsmIp: dsm.Ip,
|
||||
Target: target,
|
||||
Lun: lun,
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (service *DsmService) ListVolumes() []*models.ListK8sVolumeRespSpec {
|
||||
var infos []*models.ListK8sVolumeRespSpec
|
||||
|
||||
for _, dsm := range service.dsms {
|
||||
service.listVolumesByDsm(dsm, &infos)
|
||||
}
|
||||
|
||||
return infos
|
||||
}
|
||||
|
||||
func (service *DsmService) GetVolume(lunUuid string) *models.ListK8sVolumeRespSpec {
|
||||
volumes := service.ListVolumes()
|
||||
|
||||
for _, volume := range volumes {
|
||||
if volume.Lun.Uuid == lunUuid {
|
||||
return volume
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *DsmService) ExpandLun(lunUuid string, newSize int64) error {
|
||||
k8sVolume := service.GetVolume(lunUuid);
|
||||
if k8sVolume == nil {
|
||||
return status.Errorf(codes.InvalidArgument, fmt.Sprintf("Can't find volume[%s].", lunUuid))
|
||||
}
|
||||
|
||||
if int64(k8sVolume.Lun.Size) > newSize {
|
||||
return status.Errorf(codes.InvalidArgument,
|
||||
fmt.Sprintf("Failed to expand volume[%s], because expand size[%d] bigger than before[%d].",
|
||||
lunUuid, newSize, k8sVolume.Lun.Size))
|
||||
}
|
||||
|
||||
spec := webapi.LunUpdateSpec{
|
||||
Uuid: lunUuid,
|
||||
NewSize: uint64(newSize),
|
||||
}
|
||||
|
||||
dsm, err := service.GetDsm(k8sVolume.DsmIp)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, fmt.Sprintf("Failed to get DSM[%s]", k8sVolume.DsmIp))
|
||||
}
|
||||
|
||||
if err := dsm.LunUpdate(spec); err != nil {
|
||||
return status.Errorf(codes.InvalidArgument,
|
||||
fmt.Sprintf("Failed to expand volume[%s]. err: %v", lunUuid, err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *DsmService) CreateSnapshot(spec *models.CreateK8sVolumeSnapshotSpec) (string, error) {
|
||||
k8sVolume := service.GetVolume(spec.K8sVolumeId);
|
||||
if k8sVolume == nil {
|
||||
return "", status.Errorf(codes.NotFound, fmt.Sprintf("Can't find volume[%s].", spec.K8sVolumeId))
|
||||
}
|
||||
|
||||
snapshotSpec := webapi.SnapshotCreateSpec{
|
||||
Name: spec.SnapshotName,
|
||||
LunUuid: spec.K8sVolumeId,
|
||||
Description: spec.Description,
|
||||
TakenBy: spec.TakenBy,
|
||||
IsLocked: spec.IsLocked,
|
||||
}
|
||||
|
||||
dsm, err := service.GetDsm(k8sVolume.DsmIp)
|
||||
if err != nil {
|
||||
return "", status.Errorf(codes.InvalidArgument, fmt.Sprintf("Failed to get dsm: %v", err))
|
||||
}
|
||||
|
||||
snapshotUuid, err := dsm.SnapshotCreate(snapshotSpec)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return snapshotUuid, nil
|
||||
}
|
||||
|
||||
func (service *DsmService) DeleteSnapshot(snapshotUuid string) error {
|
||||
for _, dsm := range service.dsms {
|
||||
_, err := dsm.SnapshotGet(snapshotUuid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
err = dsm.SnapshotDelete(snapshotUuid)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, fmt.Sprintf("Failed to delete snapshot [%s]. err: %v", snapshotUuid, err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *DsmService) ListAllSnapshots() ([]webapi.SnapshotInfo, error) {
|
||||
var allInfos []webapi.SnapshotInfo
|
||||
|
||||
for _, dsm := range service.dsms {
|
||||
infos, err := dsm.SnapshotList("")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
allInfos = append(allInfos, infos...)
|
||||
}
|
||||
|
||||
return allInfos, nil
|
||||
}
|
||||
|
||||
func (service *DsmService) ListSnapshots(lunUuid string) ([]webapi.SnapshotInfo, error) {
|
||||
k8sVolume := service.GetVolume(lunUuid);
|
||||
if k8sVolume == nil {
|
||||
return []webapi.SnapshotInfo{}, nil // return empty when the volume does not exist
|
||||
}
|
||||
|
||||
dsm, err := service.GetDsm(k8sVolume.DsmIp)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("Failed to get dsm: %v", err))
|
||||
}
|
||||
|
||||
infos, err := dsm.SnapshotList(lunUuid)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal,
|
||||
fmt.Sprintf("Failed to list snapshot on lun [%s]. err: %v", lunUuid, err))
|
||||
}
|
||||
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
func (service *DsmService) GetSnapshot(snapshotUuid string) (webapi.SnapshotInfo, error) {
|
||||
for _, dsm := range service.dsms {
|
||||
info, err := dsm.SnapshotGet(snapshotUuid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
return webapi.SnapshotInfo{}, status.Errorf(codes.Internal, fmt.Sprintf("No such snapshot uuid [%s]", snapshotUuid))
|
||||
}
|
||||
208
pkg/dsm/webapi/dsmwebapi.go
Normal file
208
pkg/dsm/webapi/dsmwebapi.go
Normal file
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright 2021 Synology Inc.
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/logger"
|
||||
)
|
||||
|
||||
type DSM struct {
|
||||
Ip string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
Sid string
|
||||
Https bool
|
||||
}
|
||||
|
||||
type errData struct {
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
type dsmApiResp struct {
|
||||
Success bool `json:"success"`
|
||||
Err errData `json:"error"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
ErrorCode int
|
||||
Success bool
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
func (dsm *DSM) sendRequest(data string, apiTemplate interface{}, params url.Values, cgiPath string) (Response, error) {
|
||||
resp, err := dsm.sendRequestWithoutConnectionCheck(data, apiTemplate, params, cgiPath)
|
||||
if err != nil && resp.ErrorCode == 119 { // WEBAPI_ERR_SID_NOT_FOUND
|
||||
// Re-login
|
||||
if err := dsm.Login(); err != nil {
|
||||
return Response{}, fmt.Errorf("Failed to re-login to DSM: [%s]. err: %v", dsm.Ip, err)
|
||||
}
|
||||
log.Info("Re-login succeeded.")
|
||||
return dsm.sendRequestWithoutConnectionCheck(data, apiTemplate, params, cgiPath);
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (dsm *DSM) sendRequestWithoutConnectionCheck(data string, apiTemplate interface{}, params url.Values, cgiPath string) (Response, error) {
|
||||
client := &http.Client{}
|
||||
var req *http.Request
|
||||
var err error
|
||||
var cgiUrl string
|
||||
|
||||
// Ex: http://10.12.12.14:5000/webapi/auth.cgi
|
||||
if dsm.Https {
|
||||
// TODO: input CA certificate and fill in tls config
|
||||
// Skip Verify when https
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client = &http.Client{Transport: tr}
|
||||
cgiUrl = fmt.Sprintf("https://%s:%d/%s", dsm.Ip, dsm.Port, cgiPath)
|
||||
} else {
|
||||
cgiUrl = fmt.Sprintf("http://%s:%d/%s", dsm.Ip, dsm.Port, cgiPath)
|
||||
}
|
||||
|
||||
baseUrl, err := url.Parse(cgiUrl)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
|
||||
baseUrl.RawQuery = params.Encode()
|
||||
|
||||
if logger.WebapiDebug {
|
||||
log.Debugln(baseUrl.RawQuery)
|
||||
}
|
||||
|
||||
if data != "" {
|
||||
req, err = http.NewRequest("POST", baseUrl.String(), nil)
|
||||
} else {
|
||||
req, err = http.NewRequest("GET", baseUrl.String(), nil)
|
||||
}
|
||||
|
||||
if dsm.Sid != "" {
|
||||
cookie := http.Cookie{Name: "id", Value: dsm.Sid}
|
||||
req.AddCookie(&cookie)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// For debug print text body
|
||||
var bodyText []byte
|
||||
if logger.WebapiDebug {
|
||||
bodyText, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
s := string(bodyText)
|
||||
log.Debugln(s)
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 && resp.StatusCode != 302 {
|
||||
return Response{}, fmt.Errorf("Bad response status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Strip data json data from response
|
||||
type envelop struct {
|
||||
dsmApiResp
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
e := envelop{}
|
||||
var outResp Response
|
||||
|
||||
if logger.WebapiDebug {
|
||||
if err := json.Unmarshal(bodyText, &e); err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
} else {
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
|
||||
if err := decoder.Decode(&e); err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
}
|
||||
outResp.Success = e.Success
|
||||
outResp.ErrorCode = e.Err.Code
|
||||
outResp.StatusCode = resp.StatusCode
|
||||
|
||||
if !e.Success {
|
||||
return outResp, fmt.Errorf("DSM Api error. Error code:%d", outResp.ErrorCode)
|
||||
}
|
||||
|
||||
if e.Data != nil {
|
||||
if err := json.Unmarshal(e.Data, apiTemplate); err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
}
|
||||
|
||||
outResp.Data = apiTemplate
|
||||
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
|
||||
return outResp, nil
|
||||
}
|
||||
|
||||
// Login by given user name and password
|
||||
func (dsm *DSM) Login() error {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.API.Auth")
|
||||
params.Add("method", "login")
|
||||
params.Add("version", "3")
|
||||
params.Add("account", dsm.Username)
|
||||
params.Add("passwd", dsm.Password)
|
||||
params.Add("format", "sid")
|
||||
|
||||
type LoginResp struct {
|
||||
Sid string `json:"sid"`
|
||||
}
|
||||
|
||||
resp, err := dsm.sendRequestWithoutConnectionCheck("", &LoginResp{}, params, "webapi/auth.cgi")
|
||||
if err != nil {
|
||||
r, _ := regexp.Compile("passwd=.*&")
|
||||
temp := r.ReplaceAllString(err.Error(), "")
|
||||
|
||||
return fmt.Errorf("%s", temp)
|
||||
}
|
||||
|
||||
loginResp, ok := resp.Data.(*LoginResp)
|
||||
if !ok {
|
||||
return fmt.Errorf("Failed to assert response to %T", &LoginResp{})
|
||||
}
|
||||
dsm.Sid = loginResp.Sid
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout on current IP and reset the synoToken
|
||||
func (dsm *DSM) Logout() error {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.API.Auth")
|
||||
params.Add("method", "logout")
|
||||
params.Add("version", "1")
|
||||
|
||||
_, err := dsm.sendRequestWithoutConnectionCheck("", &struct{}{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dsm.Sid = ""
|
||||
|
||||
return nil
|
||||
}
|
||||
500
pkg/dsm/webapi/iscsi.go
Normal file
500
pkg/dsm/webapi/iscsi.go
Normal file
@@ -0,0 +1,500 @@
|
||||
// Copyright 2021 Synology Inc.
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/logger"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/utils"
|
||||
)
|
||||
|
||||
type LunInfo struct {
|
||||
Name string `json:"name"`
|
||||
Uuid string `json:"uuid"`
|
||||
LunType int `json:"type"`
|
||||
Location string `json:"location"`
|
||||
Size uint64 `json:"size"`
|
||||
Used uint64 `json:"allocated_size"`
|
||||
Status string `json:"status"`
|
||||
FlashcacheStatus string `json:"flashcache_status"`
|
||||
IsActionLocked bool `json:"is_action_locked"`
|
||||
}
|
||||
|
||||
type MappedLun struct {
|
||||
LunUuid string `json:"lun_uuid"`
|
||||
MappingIndex int `json:"mapping_index"`
|
||||
}
|
||||
|
||||
type ConncetedSession struct {
|
||||
Iqn string `json:"iqn"`
|
||||
Ip string `json:"ip"`
|
||||
}
|
||||
|
||||
type NetworkPortal struct {
|
||||
ControllerId int `json:"controller_id"`
|
||||
InterfaceName string `json:"interface_name"`
|
||||
}
|
||||
|
||||
type TargetInfo struct {
|
||||
Name string `json:"name"`
|
||||
Iqn string `json:"iqn"`
|
||||
Status string `json:"status"`
|
||||
MaxSessions int `json:"max_sessions"`
|
||||
MappedLuns []MappedLun `json:"mapped_luns"`
|
||||
ConnectedSessions []ConncetedSession `json:"connected_sessions"`
|
||||
NetworkPortals []NetworkPortal `json:"network_portals"`
|
||||
TargetId int `json:"target_id"`
|
||||
}
|
||||
|
||||
type SnapshotInfo struct {
|
||||
Name string `json:"name"`
|
||||
Uuid string `json:"uuid"`
|
||||
ParentUuid string `json:"parent_uuid"`
|
||||
Status string `json:"status"`
|
||||
TotalSize int64 `json:"total_size"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
}
|
||||
|
||||
type LunDevAttrib struct {
|
||||
DevAttrib string `json:"dev_attrib"`
|
||||
Enable int `json:"enable"`
|
||||
}
|
||||
|
||||
type LunCreateSpec struct {
|
||||
Name string
|
||||
Location string
|
||||
Size int64
|
||||
Type string
|
||||
DevAttribs []LunDevAttrib
|
||||
}
|
||||
|
||||
type LunUpdateSpec struct {
|
||||
Uuid string
|
||||
NewSize uint64
|
||||
}
|
||||
|
||||
type LunCloneSpec struct {
|
||||
Name string
|
||||
SrcLunUuid string
|
||||
Location string
|
||||
}
|
||||
|
||||
type TargetCreateSpec struct {
|
||||
Name string
|
||||
Iqn string
|
||||
}
|
||||
|
||||
type SnapshotCreateSpec struct {
|
||||
Name string
|
||||
LunUuid string
|
||||
Description string
|
||||
TakenBy string
|
||||
IsLocked bool
|
||||
}
|
||||
|
||||
type SnapshotCloneSpec struct {
|
||||
Name string
|
||||
SrcLunUuid string
|
||||
SrcSnapshotUuid string
|
||||
}
|
||||
|
||||
func errCodeMapping(errCode int, oriErr error) error {
|
||||
switch errCode {
|
||||
case 18990002: // Out of free space
|
||||
return utils.OutOfFreeSpaceError("")
|
||||
case 18990538: // Duplicated LUN name
|
||||
return utils.AlreadyExistError("")
|
||||
case 18990541:
|
||||
return utils.LunReachMaxCountError("")
|
||||
case 18990542:
|
||||
return utils.TargetReachMaxCountError("")
|
||||
case 18990744: // Duplicated Target name
|
||||
return utils.AlreadyExistError("")
|
||||
case 18990532:
|
||||
return utils.NoSuchSnapshotError("")
|
||||
case 18990500:
|
||||
return utils.BadLunTypeError("")
|
||||
case 18990543:
|
||||
return utils.SnapshotReachMaxCountError("")
|
||||
}
|
||||
|
||||
if errCode > 18990000 {
|
||||
return utils.IscsiDefaultError("")
|
||||
}
|
||||
return oriErr
|
||||
}
|
||||
|
||||
func (dsm *DSM) LunList() ([]LunInfo, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.LUN")
|
||||
params.Add("method", "list")
|
||||
params.Add("version", "1")
|
||||
params.Add("types", "[\"BLOCK\", \"FILE\", \"THIN\", \"ADV\", \"SINK\", \"CINDER\", \"CINDER_BLUN\", \"CINDER_BLUN_THICK\", \"BLUN\", \"BLUN_THICK\", \"BLUN_SINK\", \"BLUN_THICK_SINK\"]")
|
||||
params.Add("additional", "[\"allocated_size\",\"status\",\"flashcache_status\", \"is_action_locked\"]")
|
||||
|
||||
type LunInfos struct {
|
||||
Luns []LunInfo `json:"luns"`
|
||||
}
|
||||
|
||||
resp, err := dsm.sendRequest("", &LunInfos{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lunInfos, ok := resp.Data.(*LunInfos)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Failed to assert response to %T", &LunInfos{})
|
||||
}
|
||||
return lunInfos.Luns, nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) LunCreate(spec LunCreateSpec) (string, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.LUN")
|
||||
params.Add("method", "create")
|
||||
params.Add("version", "1")
|
||||
params.Add("name", strconv.Quote(spec.Name))
|
||||
params.Add("size", strconv.FormatInt(int64(spec.Size), 10))
|
||||
params.Add("type", spec.Type)
|
||||
params.Add("location", spec.Location)
|
||||
|
||||
js, err := json.Marshal(spec.DevAttribs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
params.Add("dev_attribs", string(js))
|
||||
|
||||
type LunCreateResp struct {
|
||||
Uuid string `json:"uuid"`
|
||||
}
|
||||
|
||||
resp, err := dsm.sendRequest("", &LunCreateResp{}, params, "webapi/entry.cgi")
|
||||
err = errCodeMapping(resp.ErrorCode, err)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lunResp, ok := resp.Data.(*LunCreateResp)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Failed to assert response to %T", &LunCreateResp{})
|
||||
}
|
||||
|
||||
return lunResp.Uuid, nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) LunUpdate(spec LunUpdateSpec) error {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.LUN")
|
||||
params.Add("method", "set")
|
||||
params.Add("version", "1")
|
||||
params.Add("uuid", strconv.Quote(spec.Uuid))
|
||||
params.Add("new_size", strconv.FormatInt(int64(spec.NewSize), 10))
|
||||
|
||||
_, err := dsm.sendRequest("", &struct{}{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) LunGet(uuid string) (LunInfo, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.LUN")
|
||||
params.Add("method", "get")
|
||||
params.Add("version", "1")
|
||||
params.Add("uuid", strconv.Quote(uuid))
|
||||
params.Add("additional", "[\"allocated_size\",\"status\",\"flashcache_status\", \"is_action_locked\"]")
|
||||
|
||||
type Info struct {
|
||||
Lun LunInfo `json:"lun"`
|
||||
}
|
||||
info := Info{}
|
||||
|
||||
_, err := dsm.sendRequest("", &info, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return LunInfo{}, err
|
||||
}
|
||||
|
||||
return info.Lun, nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) LunClone(spec LunCloneSpec) (string, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.LUN")
|
||||
params.Add("method", "clone")
|
||||
params.Add("version", "1")
|
||||
params.Add("src_lun_uuid", strconv.Quote(spec.SrcLunUuid))
|
||||
params.Add("dst_lun_name", strconv.Quote(spec.Name))
|
||||
params.Add("dst_location", strconv.Quote(spec.Location))
|
||||
|
||||
type LunCloneResp struct {
|
||||
Uuid string `json:"dst_lun_uuid"`
|
||||
}
|
||||
|
||||
resp, err := dsm.sendRequest("", &LunCloneResp{}, params, "webapi/entry.cgi")
|
||||
err = errCodeMapping(resp.ErrorCode, err)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cloneLunResp, ok := resp.Data.(*LunCloneResp)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Failed to assert response to %T", &LunCloneResp{})
|
||||
}
|
||||
|
||||
return cloneLunResp.Uuid, nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) TargetList() ([]TargetInfo, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.Target")
|
||||
params.Add("method", "list")
|
||||
params.Add("version", "1")
|
||||
params.Add("additional", "[\"mapped_lun\", \"connected_sessions\"]")
|
||||
|
||||
type TargetInfos struct {
|
||||
Targets []TargetInfo `json:"targets"`
|
||||
}
|
||||
|
||||
resp, err := dsm.sendRequest("", &TargetInfos{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trgInfos, ok := resp.Data.(*TargetInfos)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Failed to assert response to %T", &TargetInfos{})
|
||||
}
|
||||
return trgInfos.Targets, nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) TargetGet(targetId string) (TargetInfo, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.Target")
|
||||
params.Add("method", "get")
|
||||
params.Add("version", "1")
|
||||
params.Add("target_id", strconv.Quote(targetId))
|
||||
params.Add("additional", "[\"mapped_lun\", \"connected_sessions\"]")
|
||||
|
||||
type Info struct {
|
||||
Target TargetInfo `json:"target"`
|
||||
}
|
||||
info := Info{}
|
||||
|
||||
_, err := dsm.sendRequest("", &info, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return TargetInfo{}, err
|
||||
}
|
||||
|
||||
return info.Target, nil
|
||||
}
|
||||
|
||||
// Enable muti session
|
||||
func (dsm *DSM) TargetSet(targetId string, maxSession int) error {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.Target")
|
||||
params.Add("method", "set")
|
||||
params.Add("version", "1")
|
||||
params.Add("target_id", strconv.Quote(targetId))
|
||||
params.Add("max_sessions", strconv.Itoa(maxSession))
|
||||
|
||||
_, err := dsm.sendRequest("", &struct{}{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) TargetCreate(spec TargetCreateSpec) (string, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.Target")
|
||||
params.Add("method", "create")
|
||||
params.Add("version", "1")
|
||||
params.Add("name", spec.Name)
|
||||
params.Add("auth_type", "0")
|
||||
params.Add("iqn", spec.Iqn)
|
||||
|
||||
type TrgCreateResp struct {
|
||||
TargetId int `json:"target_id"`
|
||||
}
|
||||
|
||||
resp, err := dsm.sendRequest("", &TrgCreateResp{}, params, "webapi/entry.cgi")
|
||||
err = errCodeMapping(resp.ErrorCode, err)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
trgResp, ok := resp.Data.(*TrgCreateResp)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Failed to assert response to %T", &TrgCreateResp{})
|
||||
}
|
||||
|
||||
return strconv.Itoa(trgResp.TargetId), nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) LunMapTarget(targetIds []string, lunUuid string) error {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.LUN")
|
||||
params.Add("method", "map_target")
|
||||
params.Add("version", "1")
|
||||
params.Add("uuid", strconv.Quote(lunUuid))
|
||||
params.Add("target_ids", fmt.Sprintf("[%s]", strings.Join(targetIds, ",")))
|
||||
|
||||
if logger.WebapiDebug {
|
||||
log.Debugln(params)
|
||||
}
|
||||
|
||||
_, err := dsm.sendRequest("", &struct{}{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) LunDelete(lunUuid string) error {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.LUN")
|
||||
params.Add("method", "delete")
|
||||
params.Add("version", "1")
|
||||
params.Add("uuid", strconv.Quote(lunUuid))
|
||||
|
||||
_, err := dsm.sendRequest("", &struct{}{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) TargetDelete(targetName string) error {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.Target")
|
||||
params.Add("method", "delete")
|
||||
params.Add("version", "1")
|
||||
params.Add("target_id", strconv.Quote(targetName))
|
||||
|
||||
_, err := dsm.sendRequest("", &struct{}{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) SnapshotCreate(spec SnapshotCreateSpec) (string, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.LUN")
|
||||
params.Add("method", "take_snapshot")
|
||||
params.Add("version", "1")
|
||||
params.Add("src_lun_uuid", strconv.Quote(spec.LunUuid))
|
||||
params.Add("snapshot_name", strconv.Quote(spec.Name))
|
||||
params.Add("description", strconv.Quote(spec.Description))
|
||||
params.Add("taken_by", strconv.Quote(spec.TakenBy))
|
||||
params.Add("is_locked", strconv.FormatBool(spec.IsLocked))
|
||||
params.Add("is_app_consistent", strconv.FormatBool(false))
|
||||
|
||||
type SnapshotCreateResp struct {
|
||||
Uuid string `json:"snapshot_uuid"`
|
||||
}
|
||||
|
||||
resp, err := dsm.sendRequest("", &SnapshotCreateResp{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return "", errCodeMapping(resp.ErrorCode, err)
|
||||
}
|
||||
|
||||
snapshotResp, ok := resp.Data.(*SnapshotCreateResp)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Failed to assert response to %T", &SnapshotCreateResp{})
|
||||
}
|
||||
|
||||
return snapshotResp.Uuid, nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) SnapshotDelete(snapshotUuid string) error {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.LUN")
|
||||
params.Add("method", "delete_snapshot")
|
||||
params.Add("version", "1")
|
||||
params.Add("snapshot_uuid", strconv.Quote(snapshotUuid))
|
||||
|
||||
resp, err := dsm.sendRequest("", &struct{}{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return errCodeMapping(resp.ErrorCode, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) SnapshotGet(snapshotUuid string) (SnapshotInfo, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.LUN")
|
||||
params.Add("method", "get_snapshot")
|
||||
params.Add("version", "1")
|
||||
params.Add("snapshot_uuid", strconv.Quote(snapshotUuid))
|
||||
|
||||
type Info struct {
|
||||
Snapshot SnapshotInfo `json:"snapshot"`
|
||||
}
|
||||
info := Info{}
|
||||
|
||||
resp, err := dsm.sendRequest("", &info, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return SnapshotInfo{}, errCodeMapping(resp.ErrorCode, err)
|
||||
}
|
||||
|
||||
return info.Snapshot, nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) SnapshotList(lunUuid string) ([]SnapshotInfo, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.LUN")
|
||||
params.Add("method", "list_snapshot")
|
||||
params.Add("version", "1")
|
||||
params.Add("src_lun_uuid", strconv.Quote(lunUuid))
|
||||
|
||||
type Infos struct {
|
||||
Snapshots []SnapshotInfo `json:"snapshots"`
|
||||
}
|
||||
|
||||
resp, err := dsm.sendRequest("", &Infos{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return nil, errCodeMapping(resp.ErrorCode, err)
|
||||
}
|
||||
|
||||
infos, ok := resp.Data.(*Infos)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Failed to assert response to %T", &Infos{})
|
||||
}
|
||||
|
||||
return infos.Snapshots, nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) SnapshotClone(spec SnapshotCloneSpec) (string, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.ISCSI.LUN")
|
||||
params.Add("method", "clone_snapshot")
|
||||
params.Add("version", "1")
|
||||
params.Add("src_lun_uuid", strconv.Quote(spec.SrcLunUuid))
|
||||
params.Add("snapshot_uuid", strconv.Quote(spec.SrcSnapshotUuid))
|
||||
params.Add("cloned_lun_name", strconv.Quote(spec.Name))
|
||||
|
||||
type SnapshotCloneResp struct {
|
||||
Uuid string `json:"cloned_lun_uuid"`
|
||||
}
|
||||
|
||||
resp, err := dsm.sendRequest("", &SnapshotCloneResp{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return "", errCodeMapping(resp.ErrorCode, err)
|
||||
}
|
||||
|
||||
snapshotCloneResp, ok := resp.Data.(*SnapshotCloneResp)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Failed to assert response to %T", &SnapshotCloneResp{})
|
||||
}
|
||||
|
||||
return snapshotCloneResp.Uuid, nil
|
||||
}
|
||||
87
pkg/dsm/webapi/share.go
Normal file
87
pkg/dsm/webapi/share.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright 2021 Synology Inc.
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ShareInfo struct {
|
||||
Name string `json:"name"`
|
||||
VolPath string `json:"vol_path"`
|
||||
Desc string `json:"desc"`
|
||||
EnableShareCow bool `json:"enable_share_cow"`
|
||||
EnableRecycleBin bool `json:"enable_recycle_bin"`
|
||||
RecycleBinAdminOnly bool `json:"recycle_bin_admin_only"`
|
||||
Encryption int `json:"encryption"`
|
||||
}
|
||||
|
||||
type ShareCreateSpec struct {
|
||||
Name string `json:"name"`
|
||||
ShareInfo ShareInfo `json:"shareinfo"`
|
||||
}
|
||||
|
||||
func (dsm *DSM) ShareList() ([]ShareInfo, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.Share")
|
||||
params.Add("method", "list")
|
||||
params.Add("version", "1")
|
||||
params.Add("additional", "[\"encryption\"]")
|
||||
|
||||
type ShareInfos struct {
|
||||
Shares []ShareInfo `json:"shares"`
|
||||
}
|
||||
|
||||
resp, err := dsm.sendRequest("", &ShareInfos{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infos, ok := resp.Data.(*ShareInfos)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Failed to assert response to %T", &ShareInfos{})
|
||||
}
|
||||
|
||||
return infos.Shares, nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) ShareCreate(spec ShareCreateSpec) error {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.Share")
|
||||
params.Add("method", "create")
|
||||
params.Add("version", "1")
|
||||
params.Add("name", strconv.Quote(spec.Name))
|
||||
|
||||
js, err := json.Marshal(spec.ShareInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params.Add("shareinfo", string(js))
|
||||
|
||||
// response : {"data":{"name":"Share-2"},"success":true}
|
||||
_, err = dsm.sendRequest("", &struct{}{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) ShareDelete(shareName string) error {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.Share")
|
||||
params.Add("method", "delete")
|
||||
params.Add("version", "1")
|
||||
params.Add("name", strconv.Quote(shareName))
|
||||
|
||||
// response : {"success":true}
|
||||
_, err := dsm.sendRequest("", &struct{}{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
67
pkg/dsm/webapi/storage.go
Normal file
67
pkg/dsm/webapi/storage.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright 2021 Synology Inc.
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type VolInfo struct {
|
||||
Name string `json:"display_name"`
|
||||
Path string `json:"volume_path"`
|
||||
Status string `json:"status"`
|
||||
FsType string `json:"fs_type"`
|
||||
Size string `json:"size_total_byte"`
|
||||
Free string `json:"size_free_byte"`
|
||||
Container string `json:"container"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
func (dsm *DSM) VolumeList() ([]VolInfo, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.Storage.Volume")
|
||||
params.Add("method", "list")
|
||||
params.Add("version", "1")
|
||||
params.Add("offset", "0")
|
||||
params.Add("limit", "-1")
|
||||
params.Add("location", "all")
|
||||
|
||||
type VolInfos struct {
|
||||
Vols []VolInfo `json:"volumes"`
|
||||
}
|
||||
|
||||
resp, err := dsm.sendRequest("", &VolInfos{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
volInfos, ok := resp.Data.(*VolInfos)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Failed to assert response to %T", &VolInfos{})
|
||||
}
|
||||
|
||||
return volInfos.Vols, nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) VolumeGet(name string) (VolInfo, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.Storage.Volume")
|
||||
params.Add("method", "get")
|
||||
params.Add("version", "1")
|
||||
params.Add("volume_path", strconv.Quote(name))
|
||||
|
||||
type Info struct {
|
||||
Volume VolInfo `json:"volume"`
|
||||
}
|
||||
info := Info{}
|
||||
|
||||
_, err := dsm.sendRequest("", &info, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return VolInfo{}, err
|
||||
}
|
||||
|
||||
return info.Volume, nil
|
||||
}
|
||||
|
||||
57
pkg/dsm/webapi/systeminfo.go
Normal file
57
pkg/dsm/webapi/systeminfo.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright 2021 Synology Inc.
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type DsmInfo struct {
|
||||
Hostname string `json:"hostname"`
|
||||
}
|
||||
|
||||
type DsmSysInfo struct {
|
||||
Model string `json:"model"`
|
||||
FirmwareVer string `json:"firmware_ver"`
|
||||
Serial string `json:"serial"`
|
||||
}
|
||||
|
||||
func (dsm *DSM) DsmInfoGet() (*DsmInfo, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.System")
|
||||
params.Add("method", "info")
|
||||
params.Add("version", "1")
|
||||
params.Add("type", "network")
|
||||
|
||||
resp, err := dsm.sendRequest("", &DsmInfo{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dsmInfo, ok := resp.Data.(*DsmInfo)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Failed to assert response to %T", &DsmInfo{})
|
||||
}
|
||||
|
||||
return dsmInfo, nil
|
||||
}
|
||||
|
||||
func (dsm *DSM) DsmSystemInfoGet() (*DsmSysInfo, error) {
|
||||
params := url.Values{}
|
||||
params.Add("api", "SYNO.Core.System")
|
||||
params.Add("method", "info")
|
||||
params.Add("version", "1")
|
||||
|
||||
resp, err := dsm.sendRequest("", &DsmSysInfo{}, params, "webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dsmInfo, ok := resp.Data.(*DsmSysInfo)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Failed to assert response to %T", &DsmSysInfo{})
|
||||
}
|
||||
|
||||
return dsmInfo, nil
|
||||
}
|
||||
1
pkg/dsm/webapi/utils.go
Normal file
1
pkg/dsm/webapi/utils.go
Normal file
@@ -0,0 +1 @@
|
||||
package webapi
|
||||
29
pkg/interfaces/IDsmService.go
Normal file
29
pkg/interfaces/IDsmService.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright 2021 Synology Inc.
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/common"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/webapi"
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/models"
|
||||
)
|
||||
|
||||
// An interface for DSM service
|
||||
|
||||
type IDsmService interface {
|
||||
AddDsm(client common.ClientInfo) error
|
||||
RemoveAllDsms()
|
||||
GetDsm(ip string) (*webapi.DSM, error)
|
||||
GetDsmsCount() int
|
||||
ListDsmVolumes(ip string) ([]webapi.VolInfo, error)
|
||||
CreateVolume(spec *models.CreateK8sVolumeSpec) (webapi.LunInfo, string, error)
|
||||
DeleteVolume(volumeId string) error
|
||||
ListVolumes() []*models.ListK8sVolumeRespSpec
|
||||
GetVolume(lunUuid string) *models.ListK8sVolumeRespSpec
|
||||
ExpandLun(lunUuid string, newSize int64) error
|
||||
CreateSnapshot(spec *models.CreateK8sVolumeSnapshotSpec) (string, error)
|
||||
DeleteSnapshot(snapshotUuid string) error
|
||||
ListAllSnapshots() ([]webapi.SnapshotInfo, error)
|
||||
ListSnapshots(lunUuid string) ([]webapi.SnapshotInfo, error)
|
||||
GetSnapshot(snapshotUuid string) (webapi.SnapshotInfo, error)
|
||||
}
|
||||
98
pkg/logger/logger.go
Normal file
98
pkg/logger/logger.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright 2021 Synology Inc.
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
nested "github.com/antonfisher/nested-logrus-formatter"
|
||||
)
|
||||
|
||||
var WebapiDebug = false
|
||||
|
||||
const (
|
||||
DefaultLogLevel = logrus.InfoLevel
|
||||
DefaultTimestampFormat = time.RFC3339
|
||||
)
|
||||
|
||||
type CallerHook struct {
|
||||
FileNameAndLine string
|
||||
Skip int
|
||||
}
|
||||
|
||||
func (hook *CallerHook) Fire(entry *logrus.Entry) error {
|
||||
fileName, line := findCaller(hook.Skip)
|
||||
entry.Data[hook.FileNameAndLine] = fileName + ":" + strconv.Itoa(line) // <filename>:<line>
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hook *CallerHook) Levels() []logrus.Level {
|
||||
return logrus.AllLevels
|
||||
}
|
||||
|
||||
func NewCallerHook() logrus.Hook {
|
||||
hook := CallerHook{
|
||||
FileNameAndLine: "filePath",
|
||||
Skip: 5,
|
||||
}
|
||||
return &hook
|
||||
}
|
||||
|
||||
func getCaller(skip int) (string, int) {
|
||||
_, file, line, ok := runtime.Caller(skip)
|
||||
if !ok{
|
||||
return "", 0
|
||||
}
|
||||
|
||||
n := 0
|
||||
for i := len(file) - 1; i > 0; i-- {
|
||||
if string(file[i]) == "/" {
|
||||
n++
|
||||
if n >= 2 {
|
||||
file = file[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return file, line
|
||||
}
|
||||
|
||||
func findCaller(skip int) (string, int) {
|
||||
var file string
|
||||
var line int
|
||||
for i := 0; i < 10; i++ {
|
||||
file, line = getCaller(skip+i)
|
||||
// if called by logrus functions, continue finding the upper caller
|
||||
if !strings.HasPrefix(file, "logrus"){
|
||||
break
|
||||
}
|
||||
}
|
||||
return file, line
|
||||
}
|
||||
|
||||
func setLogLevel(logLevel string) { // debug, info, warn, error, fatal
|
||||
level, err := logrus.ParseLevel(logLevel)
|
||||
if err == nil {
|
||||
logrus.SetLevel(level)
|
||||
} else {
|
||||
logrus.SetLevel(DefaultLogLevel)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Init(logLevel string) {
|
||||
logrus.AddHook(NewCallerHook())
|
||||
logrus.SetOutput(os.Stdout)
|
||||
setLogLevel(logLevel)
|
||||
logrus.SetFormatter(&nested.Formatter{
|
||||
HideKeys: true,
|
||||
TimestampFormat: DefaultTimestampFormat,
|
||||
ShowFullLevel: true,
|
||||
NoColors: true,
|
||||
})
|
||||
}
|
||||
22
pkg/models/dsm.go
Normal file
22
pkg/models/dsm.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright 2021 Synology Inc.
|
||||
|
||||
package models
|
||||
|
||||
const (
|
||||
K8sCsiName = "Kubernetes CSI"
|
||||
|
||||
// ISCSI definitions
|
||||
FsTypeExt4 = "ext4"
|
||||
FsTypeBtrfs = "btrfs"
|
||||
LunTypeFile = "FILE"
|
||||
LunTypeThin = "THIN"
|
||||
LunTypeAdv = "ADV"
|
||||
LunTypeBlun = "BLUN" // thin provision, mapped to type 263
|
||||
LunTypeBlunThick = "BLUN_THICK" // thick provision, mapped to type 259
|
||||
MaxIqnLen = 128
|
||||
|
||||
// CSI definitions
|
||||
TargetPrefix = "k8s-csi"
|
||||
LunPrefix = "k8s-csi"
|
||||
IqnPrefix = "iqn.2000-01.com.synology:"
|
||||
)
|
||||
36
pkg/models/dsm_req_spec.go
Normal file
36
pkg/models/dsm_req_spec.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright 2021 Synology Inc.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/SynologyOpenSource/synology-csi/pkg/dsm/webapi"
|
||||
)
|
||||
|
||||
type CreateK8sVolumeSpec struct {
|
||||
DsmIp string
|
||||
K8sVolumeName string
|
||||
LunName string
|
||||
Location string
|
||||
Size int64
|
||||
Type string
|
||||
ThinProvisioning bool
|
||||
TargetName string
|
||||
TargetIqn string
|
||||
MultipleSession bool
|
||||
SourceSnapshotId string
|
||||
SourceVolumeId string
|
||||
}
|
||||
|
||||
type ListK8sVolumeRespSpec struct {
|
||||
DsmIp string
|
||||
Lun webapi.LunInfo
|
||||
Target webapi.TargetInfo
|
||||
}
|
||||
|
||||
type CreateK8sVolumeSnapshotSpec struct {
|
||||
K8sVolumeId string
|
||||
SnapshotName string
|
||||
Description string
|
||||
TakenBy string
|
||||
IsLocked bool
|
||||
}
|
||||
43
pkg/utils/error.go
Normal file
43
pkg/utils/error.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2021 Synology Inc.
|
||||
|
||||
package utils
|
||||
|
||||
type OutOfFreeSpaceError string
|
||||
type AlreadyExistError string
|
||||
type LunReachMaxCountError string
|
||||
type TargetReachMaxCountError string
|
||||
type NoSuchSnapshotError string
|
||||
type BadLunTypeError string
|
||||
type SnapshotReachMaxCountError string
|
||||
type IscsiDefaultError string
|
||||
|
||||
func (_ OutOfFreeSpaceError) Error() string {
|
||||
return "Out of free space"
|
||||
}
|
||||
func (_ AlreadyExistError) Error() string {
|
||||
return "Already Existed"
|
||||
}
|
||||
|
||||
func (_ LunReachMaxCountError) Error() string {
|
||||
return "Number of LUN reach limit"
|
||||
}
|
||||
|
||||
func (_ TargetReachMaxCountError) Error() string {
|
||||
return "Number of target reach limit"
|
||||
}
|
||||
|
||||
func (_ NoSuchSnapshotError) Error() string {
|
||||
return "No such snapshot uuid"
|
||||
}
|
||||
|
||||
func (_ BadLunTypeError) Error() string {
|
||||
return "Bad LUN type"
|
||||
}
|
||||
|
||||
func (_ SnapshotReachMaxCountError) Error() string {
|
||||
return "Number of snapshot reach limit"
|
||||
}
|
||||
|
||||
func (_ IscsiDefaultError) Error() string {
|
||||
return "Default ISCSI error"
|
||||
}
|
||||
33
pkg/utils/utils.go
Normal file
33
pkg/utils/utils.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2021 Synology Inc.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const UNIT_GB = 1024 * 1024 * 1024
|
||||
|
||||
func StringToBoolean(value string) bool {
|
||||
value = strings.ToLower(value)
|
||||
return value == "yes" || value == "true" || value == "1"
|
||||
}
|
||||
|
||||
// Haven't supported IPv6 yet.
|
||||
func LookupIPv4(name string) ([]string, error) {
|
||||
ips, _ := net.LookupIP(name)
|
||||
|
||||
retIps := []string{}
|
||||
for _, ip := range ips {
|
||||
if ipv4 := ip.To4(); ipv4 != nil {
|
||||
retIps = append(retIps, ipv4.String())
|
||||
}
|
||||
}
|
||||
if len(retIps) > 0 {
|
||||
return retIps, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Failed to LookupIPv4 by local resolver for: %s", name)
|
||||
}
|
||||
Reference in New Issue
Block a user