Initial commit

This commit is contained in:
haochengkuo
2021-08-31 10:18:35 +08:00
parent 2ad6fb7b44
commit dc05a795b7
44 changed files with 5197 additions and 1 deletions

View 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
View 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
View 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())
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View File

@@ -0,0 +1 @@
package webapi

View 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
View 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
View 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:"
)

View 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
View 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
View 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)
}