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

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