diff --git a/Gopkg.toml b/Gopkg.toml index b70428fdfc..b7e4b146fe 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -110,6 +110,14 @@ name = "github.com/360EntSecGroup-Skylar/excelize" version = "v1.3.0" +[[constraint]] + name = "github.com/tencentcloud/tencentcloud-sdk-go" + version = "=v3.0.28" + +[[constraint]] + branch = "master" + name = "github.com/nelsonken/cos-go-sdk-v5" + [[constraint]] branch = "master" name = "github.com/tredoe/osutil" @@ -117,3 +125,7 @@ [[constraint]] branch = "master" name = "golang.org/x/crypto" + +[[constraint]] + branch = "master" + name = "github.com/forhappy/cos-go-sdk" diff --git a/cmd/climc/shell/cloudaccounts.go b/cmd/climc/shell/cloudaccounts.go index 22ffde1d49..8c004dae37 100644 --- a/cmd/climc/shell/cloudaccounts.go +++ b/cmd/climc/shell/cloudaccounts.go @@ -37,6 +37,7 @@ func init() { PROVIDER string `help:"Driver for cloud account" choices:"VMware|Aliyun|Azure|Qcloud"` AccessURL string `helo:"hello" metavar:"Azure choices: "` Desc string `help:"Description"` + Enabled bool `help:"Enabled the account automatically"` Import bool `help:"Import all sub account automatically"` AutoSync bool `help:"Enabled the account automatically"` @@ -49,6 +50,10 @@ func init() { params.Add(jsonutils.NewString(args.SECRET), "secret") params.Add(jsonutils.NewString(args.PROVIDER), "provider") + if args.Enabled { + params.Add(jsonutils.JSONTrue, "enabled") + } + if args.Import { params.Add(jsonutils.JSONTrue, "import") if args.AutoSync { diff --git a/cmd/qcloudcli/main.go b/cmd/qcloudcli/main.go new file mode 100644 index 0000000000..6764f03e00 --- /dev/null +++ b/cmd/qcloudcli/main.go @@ -0,0 +1,127 @@ +package main + +import ( + "fmt" + "os" + + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/util/qcloud" + "yunion.io/x/onecloud/pkg/util/shellutils" + "yunion.io/x/structarg" + + _ "yunion.io/x/onecloud/pkg/util/qcloud/shell" +) + +type BaseOptions struct { + Help bool `help:"Show help"` + AppID string `help:"AppID" default:"$QCLOUD_APPID"` + SecretID string `help:"Secret" default:"$QCLOUD_SECRET_ID"` + SecretKey string `help:"Access key" default:"$QCLOUD_SECRET_KEY"` + RegionId string `help:"RegionId" default:"$QCLOUD_REGION"` + SUBCOMMAND string `help:"azurecli subcommand" subcommand:"true"` +} + +func getSubcommandParser() (*structarg.ArgumentParser, error) { + parse, e := structarg.NewArgumentParser(&BaseOptions{}, + "qcloudcli", + "Command-line interface to tencentcloud API.", + `See "tencentcli help COMMAND" for help on a specific command.`) + + if e != nil { + return nil, e + } + + subcmd := parse.GetSubcommand() + if subcmd == nil { + return nil, fmt.Errorf("No subcommand argument.") + } + type HelpOptions struct { + SUBCOMMAND string `help:"sub-command name"` + } + shellutils.R(&HelpOptions{}, "help", "Show help of a subcommand", func(args *HelpOptions) error { + helpstr, e := subcmd.SubHelpString(args.SUBCOMMAND) + if e != nil { + return e + } else { + fmt.Print(helpstr) + return nil + } + }) + for _, v := range shellutils.CommandTable { + _, e := subcmd.AddSubParser(v.Options, v.Command, v.Desc, v.Callback) + if e != nil { + return nil, e + } + } + return parse, nil +} + +func showErrorAndExit(e error) { + log.Errorf("%s", e) + os.Exit(1) +} + +func newClient(options *BaseOptions) (*qcloud.SRegion, error) { + if len(options.SecretKey) == 0 { + return nil, fmt.Errorf("Missing SecretKey") + } + + if len(options.SecretID) == 0 { + return nil, fmt.Errorf("Missing SecretID") + } + + account := options.SecretID + if len(options.AppID) > 0 { + account = fmt.Sprintf("%s/%s", account, options.AppID) + } + + if cli, err := qcloud.NewQcloudClient("", "", account, options.SecretKey); err != nil { + return nil, err + } else if region := cli.GetRegion(options.RegionId); region == nil { + return nil, fmt.Errorf("No such region %s", options.RegionId) + } else { + return region, nil + } +} + +func main() { + parser, e := getSubcommandParser() + if e != nil { + showErrorAndExit(e) + } + e = parser.ParseArgs(os.Args[1:], false) + options := parser.Options().(*BaseOptions) + + if options.Help { + fmt.Print(parser.HelpString()) + } else { + subcmd := parser.GetSubcommand() + subparser := subcmd.GetSubParser() + if e != nil { + if subparser != nil { + fmt.Print(subparser.Usage()) + } else { + fmt.Print(parser.Usage()) + } + showErrorAndExit(e) + } else { + suboptions := subparser.Options() + if options.SUBCOMMAND == "help" { + e = subcmd.Invoke(suboptions) + } else { + var region *qcloud.SRegion + if len(options.RegionId) == 0 { + options.RegionId = qcloud.QCLOUD_DEFAULT_REGION + } + region, e = newClient(options) + if e != nil { + showErrorAndExit(e) + } + e = subcmd.Invoke(region, suboptions) + } + if e != nil { + showErrorAndExit(e) + } + } + } +} diff --git a/pkg/compute/guestdrivers/qcloud.go b/pkg/compute/guestdrivers/qcloud.go new file mode 100644 index 0000000000..9229ab1fe3 --- /dev/null +++ b/pkg/compute/guestdrivers/qcloud.go @@ -0,0 +1,345 @@ +package guestdrivers + +import ( + "context" + "fmt" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/util/seclib2" +) + +type SQcloudGuestDriver struct { + SManagedVirtualizedGuestDriver +} + +func init() { + driver := SQcloudGuestDriver{} + models.RegisterGuestDriver(&driver) +} + +func (self *SQcloudGuestDriver) GetHypervisor() string { + return models.HYPERVISOR_QCLOUD +} + +func (self *SQcloudGuestDriver) ChooseHostStorage(host *models.SHost, backend string) *models.SStorage { + storages := host.GetAttachedStorages("") + for i := 0; i < len(storages); i++ { + if storages[i].StorageType == backend { + return &storages[i] + } + } + for _, stype := range []string{"cloud_basic", "cloud_premium", "cloud_ssd", "local_basic", "local_ssd"} { + for i := 0; i < len(storages); i++ { + if storages[i].StorageType == stype { + return &storages[i] + } + } + } + return nil +} + +func (self *SQcloudGuestDriver) GetDetachDiskStatus() ([]string, error) { + return []string{models.VM_READY, models.VM_RUNNING}, nil +} + +func (self *SQcloudGuestDriver) RequestDetachDisk(ctx context.Context, guest *models.SGuest, task taskman.ITask) error { + return guest.StartSyncTask(ctx, task.GetUserCred(), false, task.GetTaskId()) +} + +func (self *SQcloudGuestDriver) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { + data, err := self.SManagedVirtualizedGuestDriver.ValidateCreateData(ctx, userCred, data) + if err != nil { + return nil, err + } + if data.Contains("net.0") && data.Contains("net.1") { + return nil, httperrors.NewInputParameterError("cannot support more than 1 nic") + } + return data, nil +} + +func (self *SQcloudGuestDriver) RequestDeployGuestOnHost(ctx context.Context, guest *models.SGuest, host *models.SHost, task taskman.ITask) error { + config := guest.GetDeployConfigOnHost(ctx, host, task.GetParams()) + log.Debugf("RequestDeployGuestOnHost: %s", config) + /* onfinish, err := config.GetString("on_finish") + if err != nil { + return err + } */ + + action, err := config.GetString("action") + if err != nil { + return err + } + + publicKey, _ := config.GetString("public_key") + + adminPublicKey, _ := config.GetString("admin_public_key") + projectPublicKey, _ := config.GetString("project_public_key") + oUserData, _ := config.GetString("user_data") + + userData := generateUserData(adminPublicKey, projectPublicKey, oUserData) + + resetPassword := jsonutils.QueryBoolean(config, "reset_password", false) + passwd, _ := config.GetString("password") + if resetPassword && len(passwd) == 0 { + passwd = seclib2.RandomPassword2(12) + } + + ihost, err := host.GetIHost() + if err != nil { + return err + } + + desc := SManagedVMCreateConfig{} + err = config.Unmarshal(&desc, "desc") + if err != nil { + return err + } + + if action == "create" { + taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { + + nets := guest.GetNetworks() + net := nets[0].GetNetwork() + vpc := net.GetVpc() + + ivpc, err := vpc.GetIVpc() + if err != nil { + log.Errorf("getIVPC fail %s", err) + return nil, err + } + + secgrpId, err := ivpc.SyncSecurityGroup(desc.SecGroupId, desc.SecGroupName, desc.SecRules) + if err != nil { + log.Errorf("SyncSecurityGroup fail %s", err) + return nil, err + } + + iVM, err := ihost.CreateVM(desc.Name, desc.ExternalImageId, desc.SysDiskSize, desc.Cpu, desc.Memory, desc.ExternalNetworkId, + desc.IpAddr, desc.Description, passwd, desc.StorageType, desc.DataDisks, publicKey, secgrpId, userData) + if err != nil { + return nil, err + } + log.Debugf("VMcreated %s, wait status running ...", iVM.GetGlobalId()) + err = cloudprovider.WaitStatus(iVM, models.VM_RUNNING, time.Second*5, time.Second*1800) + if err != nil { + return nil, err + } + log.Debugf("VMcreated %s, and status is ready", iVM.GetGlobalId()) + + iVM, err = ihost.GetIVMById(iVM.GetGlobalId()) + if err != nil { + log.Errorf("cannot find vm %s", err) + return nil, err + } + data := fetchIVMinfo(desc, iVM, guest.Id, "root", passwd) + return data, nil + }) + } else if action == "deploy" { + iVM, err := ihost.GetIVMById(guest.GetExternalId()) + if err != nil || iVM == nil { + log.Errorf("cannot find vm %s", err) + return fmt.Errorf("cannot find vm") + } + + params := task.GetParams() + log.Debugf("Deploy VM params %s", params.String()) + + name, _ := params.GetString("name") + description, _ := params.GetString("description") + publicKey, _ := config.GetString("public_key") + deleteKeypair := jsonutils.QueryBoolean(params, "__delete_keypair__", false) + taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { + + //腾讯云暂不支持更新自定义用户数据 + // if len(userData) > 0 { + // err := iVM.UpdateUserData(userData) + // if err != nil { + // log.Errorf("update userdata fail %s", err) + // } + // } + + err := iVM.DeployVM(name, passwd, publicKey, deleteKeypair, description) + if err != nil { + return nil, err + } + + data := fetchIVMinfo(desc, iVM, guest.Id, "root", passwd) + return data, nil + }) + } else if action == "rebuild" { + + iVM, err := ihost.GetIVMById(guest.GetExternalId()) + if err != nil || iVM == nil { + log.Errorf("cannot find vm %s", err) + return fmt.Errorf("cannot find vm") + } + + taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { + //腾讯云暂不支持更新自定义用户数据 + // if len(userData) > 0 { + // err := iVM.UpdateUserData(userData) + // if err != nil { + // log.Errorf("update userdata fail %s", err) + // } + // } + + diskId, err := iVM.RebuildRoot(desc.ExternalImageId, passwd, publicKey, desc.SysDiskSize) + if err != nil { + return nil, err + } + + log.Debugf("VMrebuildRoot %s new diskID %s, wait status ready ...", iVM.GetGlobalId(), diskId) + + err = cloudprovider.WaitStatus(iVM, models.VM_READY, time.Second*5, time.Second*1800) + if err != nil { + return nil, err + } + log.Debugf("VMrebuildRoot %s, and status is ready", iVM.GetGlobalId()) + + maxWaitSecs := 300 + waited := 0 + + for { + // hack, wait disk number consistent + idisks, err := iVM.GetIDisks() + if err != nil { + log.Errorf("fail to find VM idisks %s", err) + return nil, err + } + if len(idisks) < len(desc.DataDisks)+1 { + if waited > maxWaitSecs { + log.Errorf("inconsistent disk number, wait timeout, must be something wrong on remote") + return nil, cloudprovider.ErrTimeout + } + log.Debugf("inconsistent disk number???? %d != %d", len(idisks), len(desc.DataDisks)+1) + time.Sleep(time.Second * 5) + waited += 5 + } else { + if idisks[0].GetGlobalId() != diskId { + log.Errorf("system disk id inconsistent %s != %s", idisks[0].GetGlobalId(), diskId) + return nil, fmt.Errorf("inconsistent sys disk id after rebuild root") + } + + break + } + } + + data := fetchIVMinfo(desc, iVM, guest.Id, "root", passwd) + + return data, nil + }) + + } else { + log.Errorf("RequestDeployGuestOnHost: Action %s not supported", action) + return fmt.Errorf("Action %s not supported", action) + } + + return nil +} + +func (self *SQcloudGuestDriver) OnGuestDeployTaskDataReceived(ctx context.Context, guest *models.SGuest, task taskman.ITask, data jsonutils.JSONObject) error { + + if data.Contains("disks") { + diskInfo := make([]SDiskInfo, 0) + err := data.Unmarshal(&diskInfo, "disks") + if err != nil { + return err + } + disks := guest.GetDisks() + if len(disks) != len(diskInfo) { + msg := fmt.Sprintf("inconsistent disk number: have %d want %d", len(disks), len(diskInfo)) + log.Errorf(msg) + return fmt.Errorf(msg) + } + for i := 0; i < len(diskInfo); i += 1 { + disk := disks[i].GetDisk() + _, err = disk.GetModelManager().TableSpec().Update(disk, func() error { + disk.DiskSize = diskInfo[i].Size + disk.ExternalId = diskInfo[i].Uuid + disk.DiskType = diskInfo[i].DiskType + disk.Status = models.DISK_READY + disk.BillingType = diskInfo[i].BillingType + disk.FsFormat = diskInfo[i].FsFromat + disk.AutoDelete = true + disk.TemplateId = diskInfo[i].TemplateId + disk.DiskFormat = diskInfo[i].DiskFormat + disk.ExpiredAt = diskInfo[i].ExpiredAt + if len(diskInfo[i].Metadata) > 0 { + for key, value := range diskInfo[i].Metadata { + if err := disk.SetMetadata(ctx, key, value, task.GetUserCred()); err != nil { + log.Errorf("set disk %s mata %s => %s error: %v", disk.Name, key, value, err) + } + } + } + return nil + }) + if err != nil { + msg := fmt.Sprintf("save disk info failed %s", err) + log.Errorf(msg) + break + } else { + db.OpsLog.LogEvent(disk, db.ACT_ALLOCATE, disk.GetShortDesc(), task.GetUserCred()) + } + } + } + uuid, _ := data.GetString("uuid") + if len(uuid) > 0 { + guest.SetExternalId(uuid) + } + + if metaData, _ := data.Get("metadata"); metaData != nil { + meta := make(map[string]string, 0) + if err := metaData.Unmarshal(meta); err != nil { + log.Errorf("Get guest %s metadata error: %v", guest.Name, err) + } else { + for key, value := range meta { + if err := guest.SetMetadata(ctx, key, value, task.GetUserCred()); err != nil { + log.Errorf("set guest %s mata %s => %s error: %v", guest.Name, key, value, err) + } + } + } + } + + guest.SaveDeployInfo(ctx, task.GetUserCred(), data) + return nil +} + +func (self *SQcloudGuestDriver) AllowReconfigGuest() bool { + return true +} + +func (self *SQcloudGuestDriver) RequestDiskSnapshot(ctx context.Context, guest *models.SGuest, task taskman.ITask, snapshotId, diskId string) error { + iDisk, _ := models.DiskManager.FetchById(diskId) + disk := iDisk.(*models.SDisk) + providerDisk, err := disk.GetIDisk() + if err != nil { + return err + } + iSnapshot, _ := models.SnapshotManager.FetchById(snapshotId) + snapshot := iSnapshot.(*models.SSnapshot) + taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { + cloudSnapshot, err := providerDisk.CreateISnapshot(snapshot.Name, "") + if err != nil { + return nil, err + } + res := jsonutils.NewDict() + res.Set("snapshot_id", jsonutils.NewString(cloudSnapshot.GetId())) + res.Set("manager_id", jsonutils.NewString(cloudSnapshot.GetManagerId())) + cloudRegion, err := models.CloudregionManager.FetchByExternalId("Aliyun/" + cloudSnapshot.GetRegionId()) + if err != nil { + return nil, fmt.Errorf("Cloud region not found? %s", err) + } + res.Set("cloudregion_id", jsonutils.NewString(cloudRegion.GetId())) + return res, nil + }) + return nil +} diff --git a/pkg/compute/hostdrivers/qcloud.go b/pkg/compute/hostdrivers/qcloud.go new file mode 100644 index 0000000000..536c620c64 --- /dev/null +++ b/pkg/compute/hostdrivers/qcloud.go @@ -0,0 +1,209 @@ +package hostdrivers + +import ( + "context" + "fmt" + "os" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudcommon/db/lockman" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/compute/options" + "yunion.io/x/onecloud/pkg/httperrors" +) + +type SQcloudHostDriver struct { + SBaseHostDriver +} + +func init() { + driver := SQcloudHostDriver{} + models.RegisterHostDriver(&driver) +} + +func (self *SQcloudHostDriver) GetHostType() string { + return models.HOST_TYPE_QCLOUD +} + +func (self *SQcloudHostDriver) CheckAndSetCacheImage(ctx context.Context, host *models.SHost, storageCache *models.SStoragecache, task taskman.ITask) error { + params := task.GetParams() + imageId, err := params.GetString("image_id") + if err != nil { + return err + } + + osArch, _ := params.GetString("os_arch") + osType, _ := params.GetString("os_type") + osDist, _ := params.GetString("os_distribution") + + isForce := jsonutils.QueryBoolean(params, "is_force", false) + userCred := task.GetUserCred() + taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { + lockman.LockRawObject(ctx, "cachedimages", fmt.Sprintf("%s-%s", storageCache.Id, imageId)) + defer lockman.ReleaseRawObject(ctx, "cachedimages", fmt.Sprintf("%s-%s", storageCache.Id, imageId)) + + scimg := models.StoragecachedimageManager.Register(ctx, task.GetUserCred(), storageCache.Id, imageId) + iStorageCache, err := storageCache.GetIStorageCache() + if err != nil { + return nil, err + } + + extImgId, err := iStorageCache.UploadImage(userCred, imageId, osArch, osType, osDist, scimg.ExternalId, isForce) + if err != nil { + return nil, err + } + scimg.SetExternalId(extImgId) + + ret := jsonutils.NewDict() + ret.Add(jsonutils.NewString(extImgId), "image_id") + return ret, nil + }) + return nil +} + +func (self *SQcloudHostDriver) RequestAllocateDiskOnStorage(ctx context.Context, host *models.SHost, storage *models.SStorage, disk *models.SDisk, task taskman.ITask, content *jsonutils.JSONDict) error { + iCloudStorage, err := storage.GetIStorage() + if err != nil { + return err + } + size, err := content.Int("size") + if err != nil { + return err + } + size = size >> 10 + iDisk, err := iCloudStorage.CreateIDisk(disk.GetName(), int(size), "") + if err != nil { + return err + } + _, err = disk.GetModelManager().TableSpec().Update(disk, func() error { + disk.ExternalId = iDisk.GetGlobalId() + + if metaData := iDisk.GetMetadata(); metaData != nil { + meta := make(map[string]string) + if err := metaData.Unmarshal(meta); err != nil { + log.Errorf("Get disk %s Metadata error: %v", disk.Name, err) + } else { + for key, value := range meta { + if err := disk.SetMetadata(ctx, key, value, task.GetUserCred()); err != nil { + log.Errorf("set disk %s mata %s => %s error: %v", disk.Name, key, value, err) + } + } + } + } + + return nil + }) + if err != nil { + log.Errorf("Update disk externalId err: %v", err) + return err + } + data := jsonutils.NewDict() + data.Add(jsonutils.NewInt(int64(iDisk.GetDiskSizeMB())), "disk_size") + data.Add(jsonutils.NewString(iDisk.GetDiskFormat()), "disk_format") + task.ScheduleRun(data) + return nil +} + +func (self *SQcloudHostDriver) RequestDeallocateDiskOnHost(host *models.SHost, storage *models.SStorage, disk *models.SDisk, task taskman.ITask) error { + data := jsonutils.NewDict() + if iCloudStorage, err := storage.GetIStorage(); err != nil { + return err + } else if iDisk, err := iCloudStorage.GetIDisk(disk.GetExternalId()); err != nil { + if err == cloudprovider.ErrNotFound { + task.ScheduleRun(data) + return nil + } + return err + } else if err := iDisk.Delete(); err != nil { + return err + } + task.ScheduleRun(data) + return nil +} + +func (self *SQcloudHostDriver) RequestResizeDiskOnHostOnline(host *models.SHost, storage *models.SStorage, disk *models.SDisk, size int64, task taskman.ITask) error { + return self.RequestResizeDiskOnHost(host, storage, disk, size, task) +} + +func (self *SQcloudHostDriver) RequestResizeDiskOnHost(host *models.SHost, storage *models.SStorage, disk *models.SDisk, size int64, task taskman.ITask) error { + iCloudStorage, err := storage.GetIStorage() + if err != nil { + return err + } + iDisk, err := iCloudStorage.GetIDisk(disk.GetExternalId()) + if err != nil { + return err + } + err = iDisk.Resize(size >> 10) + if err != nil { + return err + } + task.ScheduleRun(jsonutils.Marshal(map[string]int64{"disk_size": size})) + return nil +} + +func (self *SQcloudHostDriver) RequestPrepareSaveDiskOnHost(ctx context.Context, host *models.SHost, disk *models.SDisk, imageId string, task taskman.ITask) error { + task.ScheduleRun(nil) + return nil +} + +func (self *SQcloudHostDriver) RequestSaveUploadImageOnHost(ctx context.Context, host *models.SHost, disk *models.SDisk, imageId string, task taskman.ITask, data jsonutils.JSONObject) error { + iDisk, err := disk.GetIDisk() + if err != nil { + return err + } + iStorage, err := disk.GetIStorage() + if err != nil { + return err + } + iStoragecache := iStorage.GetIStoragecache() + if iStoragecache == nil { + return httperrors.NewResourceNotFoundError("fail to find iStoragecache for storage: %s", iStorage.GetName()) + } + taskman.LocalTaskRun(task, func() (jsonutils.JSONObject, error) { + snapshot, err := iDisk.CreateISnapshot(fmt.Sprintf("Snapshot-%s", imageId), "PrepareSaveImage") + if err != nil { + return nil, err + } + params := task.GetParams() + osType, _ := params.GetString("properties", "os_type") + + scimg := models.StoragecachedimageManager.Register(ctx, task.GetUserCred(), iStoragecache.GetId(), imageId) + if scimg.Status != models.CACHED_IMAGE_STATUS_READY { + scimg.SetStatus(task.GetUserCred(), models.CACHED_IMAGE_STATUS_CACHING, "request_prepare_save_disk_on_host") + } + iImage, err := iStoragecache.CreateIImage(snapshot.GetId(), fmt.Sprintf("Image-%s", imageId), osType, "") + if err != nil { + log.Errorf("fail to create iImage: %v", err) + scimg.SetStatus(task.GetUserCred(), models.CACHED_IMAGE_STATUS_CACHE_FAILED, err.Error()) + return nil, err + } + scimg.SetExternalId(iImage.GetId()) + if _, err := os.Stat(options.Options.TempPath); os.IsNotExist(err) { + if err = os.MkdirAll(options.Options.TempPath, 0755); err != nil { + return nil, err + } + } + result, err := iStoragecache.DownloadImage(task.GetUserCred(), imageId, iImage.GetId(), options.Options.TempPath) + if err != nil { + scimg.SetStatus(task.GetUserCred(), models.CACHED_IMAGE_STATUS_CACHE_FAILED, err.Error()) + return nil, err + } + if err := iImage.Delete(); err != nil { + log.Errorf("Delete iImage %s failed: %v", iImage.GetId(), err) + } + if err := snapshot.Delete(); err != nil { + log.Errorf("Delete snapshot %s failed: %v", snapshot.GetId(), err) + } + scimg.SetStatus(task.GetUserCred(), models.CACHED_IMAGE_STATUS_READY, "") + return result, nil + }) + return nil +} + +func (self *SQcloudHostDriver) RequestDeleteSnapshotWithStorage(ctx context.Context, host *models.SHost, snapshot *models.SSnapshot, task taskman.ITask) error { + return httperrors.NewNotImplementedError("not implement") +} diff --git a/pkg/compute/models/cloudproviders.go b/pkg/compute/models/cloudproviders.go index d8d1977f5e..d673ba9259 100644 --- a/pkg/compute/models/cloudproviders.go +++ b/pkg/compute/models/cloudproviders.go @@ -31,6 +31,7 @@ const ( CLOUD_PROVIDER_VMWARE = "VMware" CLOUD_PROVIDER_ALIYUN = "Aliyun" + CLOUD_PROVIDER_QCLOUD = "Qcloud" CLOUD_PROVIDER_AZURE = "Azure" ) diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index 6ece588ea9..d9f6d88fd8 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -123,6 +123,7 @@ const ( HYPERVISOR_ESXI = "esxi" HYPERVISOR_HYPERV = "hyperv" HYPERVISOR_ALIYUN = "aliyun" + HYPERVISOR_QCLOUD = "qcloud" HYPERVISOR_AZURE = "azure" // HYPERVISOR_DEFAULT = HYPERVISOR_KVM @@ -132,7 +133,7 @@ const ( var VM_RUNNING_STATUS = []string{VM_START_START, VM_STARTING, VM_RUNNING, VM_SNAPSHOT_STREAM} var VM_CREATING_STATUS = []string{VM_CREATE_NETWORK, VM_CREATE_DISK, VM_START_DEPLOY, VM_DEPLOYING} -var HYPERVISORS = []string{HYPERVISOR_KVM, HYPERVISOR_BAREMETAL, HYPERVISOR_ESXI, HYPERVISOR_CONTAINER, HYPERVISOR_ALIYUN, HYPERVISOR_AZURE} +var HYPERVISORS = []string{HYPERVISOR_KVM, HYPERVISOR_BAREMETAL, HYPERVISOR_ESXI, HYPERVISOR_CONTAINER, HYPERVISOR_ALIYUN, HYPERVISOR_AZURE, HYPERVISOR_QCLOUD} var PUBLIC_CLOUD_HYPERVISORS = []string{HYPERVISOR_ALIYUN, HYPERVISOR_AZURE} @@ -145,6 +146,7 @@ var HYPERVISOR_HOSTTYPE = map[string]string{ HYPERVISOR_CONTAINER: HOST_TYPE_KUBELET, HYPERVISOR_ALIYUN: HOST_TYPE_ALIYUN, HYPERVISOR_AZURE: HOST_TYPE_AZURE, + HYPERVISOR_QCLOUD: HOST_TYPE_QCLOUD, } var HOSTTYPE_HYPERVISOR = map[string]string{ @@ -154,6 +156,7 @@ var HOSTTYPE_HYPERVISOR = map[string]string{ HOST_TYPE_KUBELET: HYPERVISOR_CONTAINER, HOST_TYPE_ALIYUN: HYPERVISOR_ALIYUN, HOST_TYPE_AZURE: HYPERVISOR_AZURE, + HOST_TYPE_QCLOUD: HYPERVISOR_QCLOUD, } type SGuestManager struct { diff --git a/pkg/compute/models/hosts.go b/pkg/compute/models/hosts.go index 18ce57c38e..06869f44aa 100644 --- a/pkg/compute/models/hosts.go +++ b/pkg/compute/models/hosts.go @@ -43,6 +43,7 @@ const ( HOST_TYPE_HYPERV = "hyperv" // # Microsoft Hyper-V HOST_TYPE_XEN = "xen" // # XenServer HOST_TYPE_ALIYUN = "aliyun" + HOST_TYPE_QCLOUD = "qcloud" HOST_TYPE_AZURE = "azure" HOST_TYPE_DEFAULT = HOST_TYPE_HYPERVISOR @@ -81,7 +82,7 @@ const ( HOST_STATUS_UNKNOWN = BAREMETAL_UNKNOWN ) -var HOST_TYPES = []string{HOST_TYPE_BAREMETAL, HOST_TYPE_HYPERVISOR, HOST_TYPE_ESXI, HOST_TYPE_KUBELET, HOST_TYPE_XEN, HOST_TYPE_ALIYUN, HOST_TYPE_AZURE} +var HOST_TYPES = []string{HOST_TYPE_BAREMETAL, HOST_TYPE_HYPERVISOR, HOST_TYPE_ESXI, HOST_TYPE_KUBELET, HOST_TYPE_XEN, HOST_TYPE_ALIYUN, HOST_TYPE_AZURE, HOST_TYPE_QCLOUD} var NIC_TYPES = []string{NIC_TYPE_IPMI, NIC_TYPE_ADMIN} type SHostManager struct { diff --git a/pkg/compute/models/snapshots.go b/pkg/compute/models/snapshots.go index dec7787f66..ff942013b8 100644 --- a/pkg/compute/models/snapshots.go +++ b/pkg/compute/models/snapshots.go @@ -25,11 +25,12 @@ const ( MANUAL = "manual" AUTO = "auto" - SNAPSHOT_CREATING = "creating" - SNAPSHOT_FAILED = "create_failed" - SNAPSHOT_READY = "ready" - SNAPSHOT_DELETING = "deleting" - SNAPSHOT_UNKNOWN = "unknown" + SNAPSHOT_CREATING = "creating" + SNAPSHOT_ROLLBACKING = "rollbacking" + SNAPSHOT_FAILED = "create_failed" + SNAPSHOT_READY = "ready" + SNAPSHOT_DELETING = "deleting" + SNAPSHOT_UNKNOWN = "unknown" ) type SSnapshotManager struct { diff --git a/pkg/compute/service/service.go b/pkg/compute/service/service.go index 0f0bcf950f..7698c22628 100644 --- a/pkg/compute/service/service.go +++ b/pkg/compute/service/service.go @@ -14,6 +14,7 @@ import ( _ "yunion.io/x/onecloud/pkg/util/aliyun/provider" _ "yunion.io/x/onecloud/pkg/util/azure/provider" _ "yunion.io/x/onecloud/pkg/util/esxi/provider" + _ "yunion.io/x/onecloud/pkg/util/qcloud/provider" "yunion.io/x/onecloud/pkg/cloudcommon" "yunion.io/x/onecloud/pkg/cloudcommon/consts" diff --git a/pkg/mcclient/options/servers.go b/pkg/mcclient/options/servers.go index 30c9f17c76..69793506e6 100644 --- a/pkg/mcclient/options/servers.go +++ b/pkg/mcclient/options/servers.go @@ -115,7 +115,7 @@ type ServerCreateOptions struct { Project string `help:"'Owner project ID or Name" json:"tenant"` User string `help:"Owner user ID or Name"` System *bool `help:"Create a system VM, sysadmin ONLY option" json:"is_system"` - Hypervisor string `help:"Hypervisor type" choices:"kvm|esxi|baremetal|container|aliyun|azure"` + Hypervisor string `help:"Hypervisor type" choices:"kvm|esxi|baremetal|container|aliyun|azure|qcloud"` TaskNotify *bool `help:"Setup task notify" json:"-"` Count *int `help:"Create multiple simultaneously" default:"1" json:"-"` DryRun *bool `help:"Dry run to test scheduler" json:"-"` diff --git a/pkg/scheduler/api/types.go b/pkg/scheduler/api/types.go index 8ad5257f97..9123041abd 100644 --- a/pkg/scheduler/api/types.go +++ b/pkg/scheduler/api/types.go @@ -18,6 +18,7 @@ const ( HostHypervisorForKvm = "hypervisor" HostTypeAliyun = "aliyun" HostTypeAzure = "azure" + HostTypeQcloud = "qcloud" HostTypeKubelet = "kubelet" AggregateStrategyRequire = "require" @@ -50,6 +51,7 @@ var ( PublicCloudProviders = sets.NewString( HostTypeAliyun, HostTypeAzure, + HostTypeQcloud, ) ValidGpuTypes = sets.NewString( diff --git a/pkg/util/aliyun/host.go b/pkg/util/aliyun/host.go index c79b40d1bc..ad1e28e7a4 100644 --- a/pkg/util/aliyun/host.go +++ b/pkg/util/aliyun/host.go @@ -2,6 +2,7 @@ package aliyun import ( "fmt" + "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/onecloud/pkg/cloudprovider" diff --git a/pkg/util/aliyun/shell/image.go b/pkg/util/aliyun/shell/image.go index e73a758878..b02ff69774 100644 --- a/pkg/util/aliyun/shell/image.go +++ b/pkg/util/aliyun/shell/image.go @@ -1,6 +1,8 @@ package shell import ( + "fmt" + "yunion.io/x/onecloud/pkg/util/aliyun" "yunion.io/x/onecloud/pkg/util/shellutils" ) @@ -29,4 +31,47 @@ func init() { shellutils.R(&ImageDeleteOptions{}, "image-delete", "Delete image", func(cli *aliyun.SRegion, args *ImageDeleteOptions) error { return cli.DeleteImage(args.ID) }) + + type ImageCreateOptions struct { + SNAPSHOT string `help:"Snapshot id"` + NAME string `help:"Image name"` + Desc string `help:"Image desc"` + } + shellutils.R(&ImageCreateOptions{}, "image-create", "Create image", func(cli *aliyun.SRegion, args *ImageCreateOptions) error { + imageId, err := cli.CreateImage(args.SNAPSHOT, args.NAME, args.Desc) + if err != nil { + return err + } + fmt.Println(imageId) + return nil + }) + + type ImageExportOptions struct { + ID string `help:"ID or Name to export"` + BUCKET string `help:"Bucket name"` + } + + shellutils.R(&ImageExportOptions{}, "image-export", "Export image", func(cli *aliyun.SRegion, args *ImageExportOptions) error { + oss, err := cli.GetOssClient() + if err != nil { + return err + } + exist, err := oss.IsBucketExist(args.BUCKET) + if err != nil { + return err + } + if !exist { + return fmt.Errorf("not exist bucket %s", args.BUCKET) + } + bucket, err := oss.Bucket(args.BUCKET) + if err != nil { + return err + } + task, err := cli.ExportImage(args.ID, bucket) + if err != nil { + return err + } + printObject(task) + return nil + }) } diff --git a/pkg/util/aliyun/shell/snapshot.go b/pkg/util/aliyun/shell/snapshot.go index 5bfe219871..f1ca285ade 100644 --- a/pkg/util/aliyun/shell/snapshot.go +++ b/pkg/util/aliyun/shell/snapshot.go @@ -1,6 +1,8 @@ package shell import ( + "fmt" + "yunion.io/x/onecloud/pkg/util/aliyun" "yunion.io/x/onecloud/pkg/util/shellutils" ) @@ -38,8 +40,12 @@ func init() { } shellutils.R(&SnapshotCreateOptions{}, "snapshot-create", "Create snapshot", func(cli *aliyun.SRegion, args *SnapshotCreateOptions) error { - _, err := cli.CreateSnapshot(args.DiskId, args.Name, args.Desc) - return err + snapshotId, err := cli.CreateSnapshot(args.DiskId, args.Name, args.Desc) + if err != nil { + return err + } + fmt.Println(snapshotId) + return nil }) } diff --git a/pkg/util/aliyun/storagecache.go b/pkg/util/aliyun/storagecache.go index 960e9cab88..231d5d3623 100644 --- a/pkg/util/aliyun/storagecache.go +++ b/pkg/util/aliyun/storagecache.go @@ -245,6 +245,10 @@ func (self *SRegion) checkBucket(bucketName string) (*oss.Bucket, error) { } } +func (self *SRegion) CreateImage(snapshoutId, imageName, imageDesc string) (string, error) { + return self.createIImage(snapshoutId, imageName, imageDesc) +} + func (self *SRegion) createIImage(snapshoutId, imageName, imageDesc string) (string, error) { params := make(map[string]string) params["RegionId"] = self.RegionId diff --git a/pkg/util/qcloud/disk.go b/pkg/util/qcloud/disk.go new file mode 100644 index 0000000000..61377bc936 --- /dev/null +++ b/pkg/util/qcloud/disk.go @@ -0,0 +1,375 @@ +package qcloud + +import ( + "fmt" + "sort" + "strings" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/pkg/utils" +) + +type Placement struct { + ProjectId int + Zone string +} + +type SDisk struct { + storage *SStorage + + Attached bool + AutoRenewFlagError bool + CreateTime time.Time + DeadlineError bool + DeadlineTime time.Time + DifferDaysOfDeadline int + DiskChargeType string + DiskId string + DiskName string + DiskSize int + DiskState string + DiskType string + DiskUsage string + Encrypt bool + InstanceId string + IsReturnable bool + Placement Placement + Portable bool + RenewFlag string + ReturnFailCode int + RollbackPercent int + Rollbacking bool + SnapshotAbility bool + DeleteWithInstance bool +} + +type SDiskSet []SDisk + +func (v SDiskSet) Len() int { + return len(v) +} + +func (v SDiskSet) Swap(i, j int) { + v[i], v[j] = v[j], v[i] +} + +func (v SDiskSet) Less(i, j int) bool { + if v[i].DiskUsage == "SYSTEM_DISK" || v[j].DiskUsage == "DATA_DISK" { + return true + } + return false +} + +func (self *SDisk) GetMetadata() *jsonutils.JSONDict { + // data := jsonutils.NewDict() + + // // The pricingInfo key structure is 'RegionId::DiskCategory::DiskType + // priceKey := fmt.Sprintf("%s::%s::%s", self.RegionId, self.Category, self.Type) + // data.Add(jsonutils.NewString(priceKey), "price_key") + + // data.Add(jsonutils.NewString(models.HYPERVISOR_ALIYUN), "hypervisor") + + // return data + return nil +} + +func (self *SRegion) GetDisks(instanceId string, zoneId string, category string, diskIds []string, offset int, limit int) ([]SDisk, int, error) { + if limit > 50 || limit <= 0 { + limit = 50 + } + params := make(map[string]string) + params["Limit"] = fmt.Sprintf("%d", limit) + params["Offset"] = fmt.Sprintf("%d", offset) + filter := 0 + + if len(zoneId) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "zone" + params[fmt.Sprintf("Filters.%d.Values.0", filter)] = zoneId + filter++ + } + + if len(instanceId) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "instance-id" + params[fmt.Sprintf("Filters.%d.Values.0", filter)] = instanceId + filter++ + } + + if len(category) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "disk-type" + params[fmt.Sprintf("Filters.%d.Values.0", filter)] = category + filter++ + } + if diskIds != nil && len(diskIds) > 0 { + for index, diskId := range diskIds { + params[fmt.Sprintf("DiskIds.%d", index)] = diskId + } + } + + body, err := self.cbsRequest("DescribeDisks", params) + if err != nil { + log.Errorf("GetDisks fail %s", err) + return nil, 0, err + } + + disks := make([]SDisk, 0) + err = body.Unmarshal(&disks, "DiskSet") + if err != nil { + log.Errorf("Unmarshal disk details fail %s", err) + return nil, 0, err + } + total, _ := body.Float("TotalCount") + sort.Sort(SDiskSet(disks)) + return disks, int(total), nil +} + +func (self *SRegion) GetDisk(diskId string) (*SDisk, error) { + disks, total, err := self.GetDisks("", "", "", []string{diskId}, 0, 1) + if err != nil { + return nil, err + } + if total != 1 { + return nil, cloudprovider.ErrNotFound + } + return &disks[0], nil +} + +func (self *SDisk) GetId() string { + return self.DiskId +} + +func (self *SRegion) DeleteDisk(diskId string) error { + params := make(map[string]string) + params["Region"] = self.Region + params["DiskIds.0"] = diskId + + _, err := self.cbsRequest("TerminateDisks", params) + return err +} + +func (self *SDisk) Delete() error { + return self.storage.zone.region.DeleteDisk(self.DiskId) +} + +func (self *SRegion) ResizeDisk(diskId string, sizeGb int64) error { + params := make(map[string]string) + params["DiskId"] = diskId + params["DiskSize"] = fmt.Sprintf("%d", sizeGb) + startTime := time.Now() + for { + _, err := self.cbsRequest("ResizeDisk", params) + if err != nil { + if strings.Index(err.Error(), "Code=InvalidDisk.Busy") > 0 { + log.Infof("The disk is busy, try later ...") + time.Sleep(10 * time.Second) + if time.Now().Sub(startTime) > time.Minute*20 { + return cloudprovider.ErrTimeout + } + continue + } + } + return err + } +} + +func (self *SDisk) Resize(size int64) error { + return self.storage.zone.region.ResizeDisk(self.DiskId, size) +} + +func (self *SDisk) GetName() string { + if len(self.DiskName) > 0 { + return self.DiskName + } + return self.DiskId +} + +func (self *SDisk) GetGlobalId() string { + return self.DiskId +} + +func (self *SDisk) IsEmulated() bool { + return false +} + +func (self *SDisk) GetIStorge() cloudprovider.ICloudStorage { + return self.storage +} + +func (self *SDisk) GetStatus() string { + switch self.DiskState { + case "ATTACHING", "DETACHING", "EXPANDING", "ROLLBACKING": + return models.DISK_ALLOCATING + default: + return models.DISK_READY + } +} + +func (self *SDisk) Refresh() error { + new, err := self.storage.zone.region.GetDisk(self.DiskId) + if err != nil { + return err + } + return jsonutils.Update(self, new) +} + +func (self *SDisk) CreateISnapshot(name, desc string) (cloudprovider.ICloudSnapshot, error) { + snapshotId, err := self.storage.zone.region.CreateSnapshot(self.DiskId, name, desc) + if err != nil { + log.Errorf("createSnapshot fail %s", err) + return nil, err + } + snapshots, total, err := self.storage.zone.region.GetSnapshots("", "", "", []string{snapshotId}, 0, 1) + if err != nil { + return nil, err + } + if total == 1 { + snapshot := &snapshots[0] + err := cloudprovider.WaitStatus(snapshot, string(SnapshotStatusAccomplished), 15*time.Second, 3600*time.Second) + if err != nil { + return nil, err + } + return snapshot, nil + } + return nil, nil +} + +func (self *SDisk) GetDiskType() string { + switch self.DiskUsage { + case "SYSTEM_DISK": + return models.DISK_TYPE_SYS + case "DATA_DISK": + return models.DISK_TYPE_DATA + default: + return models.DISK_TYPE_DATA + } +} + +func (self *SDisk) GetFsFormat() string { + return "" +} + +func (self *SDisk) GetIsNonPersistent() bool { + return false +} + +func (self *SDisk) GetDriver() string { + return "scsi" +} + +func (self *SDisk) GetCacheMode() string { + return "none" +} + +func (self *SDisk) GetMountpoint() string { + return "" +} + +func (self *SDisk) GetBillingType() string { + switch self.DiskChargeType { + case "PREPAID": + return models.BILLING_TYPE_PREPAID + case "POSTPAID_BY_HOUR": + return models.BILLING_TYPE_POSTPAID + default: + return models.BILLING_TYPE_PREPAID + } +} + +func (self *SDisk) GetDiskFormat() string { + return "vhd" +} + +func (self *SDisk) GetDiskSizeMB() int { + return self.DiskSize * 1024 +} + +func (self *SDisk) GetIsAutoDelete() bool { + return self.DeleteWithInstance +} + +func (self *SDisk) GetExpiredAt() time.Time { + return self.DeadlineTime +} + +func (self *SDisk) GetISnapshot(snapshotId string) (cloudprovider.ICloudSnapshot, error) { + snapshots, total, err := self.storage.zone.region.GetSnapshots("", "", "", []string{snapshotId}, 0, 1) + if err != nil { + return nil, err + } + if total == 1 { + return &snapshots[0], nil + } + return nil, cloudprovider.ErrNotFound +} + +func (self *SDisk) GetISnapshots() ([]cloudprovider.ICloudSnapshot, error) { + snapshots := make([]SSnapshot, 0) + for { + parts, total, err := self.storage.zone.region.GetSnapshots("", self.DiskId, "", []string{}, 0, 20) + if err != nil { + log.Errorf("GetDisks fail %s", err) + return nil, err + } + snapshots = append(snapshots, parts...) + if len(snapshots) >= total { + break + } + } + isnapshots := make([]cloudprovider.ICloudSnapshot, len(snapshots)) + for i := 0; i < len(snapshots); i++ { + snapshots[i].region = self.storage.zone.region + isnapshots[i] = &snapshots[i] + } + return isnapshots, nil +} + +func (self *SDisk) GetTemplateId() string { + //return self.ImageId + return "" +} + +func (self *SRegion) ResetDisk(diskId, snapshotId string) error { + params := make(map[string]string) + params["Region"] = self.Region + params["DiskId"] = diskId + params["SnapshotId"] = snapshotId + _, err := self.cbsRequest("ApplySnapshot", params) + if err != nil { + log.Errorf("ResetDisk %s to snapshot %s fail %s", diskId, snapshotId, err) + return err + } + return nil +} + +func (self *SDisk) Reset(snapshotId string) error { + return self.storage.zone.region.ResetDisk(self.DiskId, snapshotId) +} + +func (self *SRegion) CreateDisk(zoneId string, category string, name string, sizeGb int, desc string) (string, error) { + params := make(map[string]string) + params["Region"] = self.Region + params["DiskType"] = category + params["DiskChargeType"] = "POSTPAID_BY_HOUR" + params["DiskName"] = name + params["Placement.Zone"] = zoneId + //params["Encrypted"] = "false" + params["DiskSize"] = fmt.Sprintf("%d", sizeGb) + params["ClientToken"] = utils.GenRequestId(20) + + body, err := self.cbsRequest("CreateDisks", params) + if err != nil { + return "", err + } + diskIDSet := []string{} + err = body.Unmarshal(&diskIDSet, "DiskIdSet") + if err != nil { + return "", err + } + if len(diskIDSet) < 1 { + return "", fmt.Errorf("Create Disk error") + } + return diskIDSet[0], nil +} diff --git a/pkg/util/qcloud/eip.go b/pkg/util/qcloud/eip.go new file mode 100644 index 0000000000..e3d6501687 --- /dev/null +++ b/pkg/util/qcloud/eip.go @@ -0,0 +1,295 @@ +package qcloud + +import ( + "fmt" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" +) + +type TInternetChargeType string + +const ( + InternetChargeByTraffic = TInternetChargeType("PayByTraffic") + InternetChargeByBandwidth = TInternetChargeType("PayByBandwidth") +) + +const ( + EIP_STATUS_ASSOCIATING = "Associating" + EIP_STATUS_UNASSOCIATING = "Unassociating" + EIP_STATUS_INUSE = "InUse" + EIP_STATUS_AVAILABLE = "Available" + + EIP_OPERATION_LOCK_FINANCIAL = "financial" + EIP_OPERATION_LOCK_SECURITY = "security" + + EIP_INSTANCE_TYPE_ECS = "EcsInstance" // (默认值):VPC类型的ECS实例 + EIP_INTANNCE_TYPE_SLB = "SlbInstance" // :VPC类型的SLB实例 + EIP_INSTANCE_TYPE_NAT = "Nat" // :NAT网关 + EIP_INSTANCE_TYPE_HAVIP = "HaVip" // :HAVIP +) + +type SEipAddress struct { + region *SRegion + + AddressId string // EIP的ID,是EIP的唯一标识。 + AddressName string // EIP名称。 + AddressStatus string // EIP状态。 + AddressIp string // 外网IP地址 + InstanceId string // 绑定的资源实例ID。可能是一个CVM,NAT。 + CreatedTime time.Time // 创建时间。按照ISO8601标准表示,并且使用UTC时间。格式为:YYYY-MM-DDThh:mm:ssZ。 + NetworkInterfaceId string // 绑定的弹性网卡ID + PrivateAddressIp string // 绑定的资源内网ip + IsArrears bool // 资源隔离状态。true表示eip处于隔离状态,false表示资源处于未隔离装填 + IsBlocked bool // 资源封堵状态。true表示eip处于封堵状态,false表示eip处于未封堵状态 + IsEipDirectConnection bool // eip是否支持直通模式。true表示eip支持直通模式,false表示资源不支持直通模式 + AddressType string // eip资源类型,包括"CalcIP","WanIP","EIP","AnycastEIP"。其中"CalcIP"表示设备ip,“WanIP”表示普通公网ip,“EIP”表示弹性公网ip,“AnycastEip”表示加速EIP + CascadeRelease bool // eip是否在解绑后自动释放。true表示eip将会在解绑后自动释放,false表示eip在解绑后不会自动释放 +} + +func (self *SEipAddress) GetId() string { + return self.AddressId +} + +func (self *SEipAddress) GetName() string { + return self.AddressName +} + +func (self *SEipAddress) GetGlobalId() string { + return self.AddressId +} + +func (self *SEipAddress) GetStatus() string { + switch self.AddressStatus { + case EIP_STATUS_AVAILABLE, EIP_STATUS_INUSE: + return models.EIP_STATUS_READY + case EIP_STATUS_ASSOCIATING: + return models.EIP_STATUS_ASSOCIATE + case EIP_STATUS_UNASSOCIATING: + return models.EIP_STATUS_DISSOCIATE + default: + return models.EIP_STATUS_UNKNOWN + } +} + +func (self *SEipAddress) Refresh() error { + if self.IsEmulated() { + return nil + } + new, err := self.region.GetEip(self.AddressId) + if err != nil { + return err + } + return jsonutils.Update(self, new) +} + +func (self *SEipAddress) IsEmulated() bool { + if self.AddressId == self.InstanceId { + // fixed Public IP + return true + } else { + return false + } +} + +func (self *SEipAddress) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SEipAddress) GetIpAddr() string { + return self.AddressIp +} + +func (self *SEipAddress) GetMode() string { + if self.InstanceId == self.AddressId { + return models.EIP_MODE_INSTANCE_PUBLICIP + } + return models.EIP_MODE_STANDALONE_EIP +} + +func (self *SEipAddress) GetAssociationType() string { + switch self.AddressType { + case "EIP", "AnycastEIP", "WanIP": + return "server" + case "CalcIP": + return "server" + default: + log.Fatalf("unsupported type: %s", self.AddressType) + return "unsupported" + } +} + +func (self *SEipAddress) GetAssociationExternalId() string { + return self.InstanceId +} + +func (self *SEipAddress) GetManagerId() string { + return self.region.client.providerId +} + +func (self *SEipAddress) Delete() error { + return self.region.DeallocateEIP(self.AddressId) +} + +func (self *SEipAddress) GetBandwidth() int { + return 100 + //return self.Bandwidth +} + +func (self *SEipAddress) GetInternetChargeType() string { + // switch self.InternetChargeType { + // case string(InternetChargeByTraffic): + // return models.EIP_CHARGE_TYPE_BY_TRAFFIC + // case string(InternetChargeByBandwidth): + // return models.EIP_CHARGE_TYPE_BY_BANDWIDTH + // default: + // return models.EIP_CHARGE_TYPE_BY_TRAFFIC + // } + return "unkonw" +} + +func (self *SEipAddress) Associate(instanceId string) error { + err := self.region.AssociateEip(self.AddressId, instanceId) + if err != nil { + return err + } + return cloudprovider.WaitStatus(self, models.EIP_STATUS_READY, 10*time.Second, 180*time.Second) +} + +func (self *SEipAddress) Dissociate() error { + err := self.region.DissociateEip(self.AddressId) + if err != nil { + return err + } + return cloudprovider.WaitStatus(self, models.EIP_STATUS_READY, 10*time.Second, 180*time.Second) +} + +func (self *SEipAddress) ChangeBandwidth(bw int) error { + return self.region.UpdateEipBandwidth(self.AddressId, bw) +} + +func (region *SRegion) GetEips(eipId string, offset int, limit int) ([]SEipAddress, int, error) { + if limit > 50 || limit <= 0 { + limit = 50 + } + + params := make(map[string]string) + params["Limit"] = fmt.Sprintf("%d", limit) + params["Offset"] = fmt.Sprintf("%d", offset) + + if len(eipId) > 0 { + params["AddressIds.0"] = eipId + } + + body, err := region.vpcRequest("DescribeAddresses", params) + if err != nil { + log.Errorf("DescribeEipAddresses fail %s", err) + return nil, 0, err + } + + eips := make([]SEipAddress, 0) + err = body.Unmarshal(&eips, "AddressSet") + if err != nil { + log.Errorf("Unmarshal EipAddress details fail %s", err) + return nil, 0, err + } + total, _ := body.Int("TotalCount") + for i := 0; i < len(eips); i++ { + eips[i].region = region + } + return eips, int(total), nil +} + +func (region *SRegion) GetEip(eipId string) (*SEipAddress, error) { + eips, total, err := region.GetEips(eipId, 0, 1) + if err != nil { + return nil, err + } + if total != 1 { + return nil, cloudprovider.ErrNotFound + } + return &eips[0], nil +} + +func (region *SRegion) AllocateEIP(name string, bwMbps int, chargeType TInternetChargeType) (*SEipAddress, error) { + params := make(map[string]string) + params["Region"] = region.Region + addRessSet := []string{} + body, err := region.vpcRequest("AllocateAddresses", params) + if err != nil { + return nil, err + } + if err := body.Unmarshal(&addRessSet, "AddressSet"); err == nil && len(addRessSet) > 0 { + params["AddressId"] = addRessSet[0] + params["AddressName"] = name + if _, err := region.vpcRequest("ModifyAddressAttribute", params); err != nil { + return nil, err + } else if err := region.UpdateEipBandwidth(addRessSet[0], bwMbps); err != nil { + return nil, err + } + return region.GetEip(addRessSet[0]) + } + return nil, cloudprovider.ErrNotFound +} + +func (region *SRegion) CreateEIP(name string, bwMbps int, chargeType string) (cloudprovider.ICloudEIP, error) { + var ctype TInternetChargeType + switch chargeType { + case models.EIP_CHARGE_TYPE_BY_TRAFFIC: + ctype = InternetChargeByTraffic + case models.EIP_CHARGE_TYPE_BY_BANDWIDTH: + ctype = InternetChargeByBandwidth + } + return region.AllocateEIP(name, bwMbps, ctype) +} + +func (region *SRegion) DeallocateEIP(eipId string) error { + params := make(map[string]string) + params["Region"] = region.Region + params["AddressIds.0"] = eipId + + _, err := region.vpcRequest("ReleaseAddresses", params) + if err != nil { + log.Errorf("ReleaseAddresses fail %s", err) + } + return err +} + +func (region *SRegion) AssociateEip(eipId string, instanceId string) error { + params := make(map[string]string) + params["AllocationId"] = eipId + params["InstanceId"] = instanceId + + _, err := region.vpcRequest("AssociateEipAddress", params) + if err != nil { + log.Errorf("AssociateEipAddress fail %s", err) + } + return err +} + +func (region *SRegion) DissociateEip(eipId string) error { + params := make(map[string]string) + params["Region"] = region.Region + params["AddressId"] = eipId + + _, err := region.vpcRequest("DisassociateAddress", params) + if err != nil { + log.Errorf("UnassociateEipAddress fail %s", err) + } + return err +} + +func (region *SRegion) UpdateEipBandwidth(eipId string, bw int) error { + // params := make(map[string]string) + // params["Region"] = region.Region + // params["AddressIds.0"] = eipId + // params["InternetMaxBandwidthOut"] = fmt.Sprintf("%d", bw) + + // _, err := region.vpcRequest("ModifyAddressesBandwidth", params) + // return err + // 腾讯云这个接口目前有问题 + return nil +} diff --git a/pkg/util/qcloud/host.go b/pkg/util/qcloud/host.go new file mode 100644 index 0000000000..67f362deb4 --- /dev/null +++ b/pkg/util/qcloud/host.go @@ -0,0 +1,272 @@ +package qcloud + +import ( + "fmt" + "strings" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" +) + +type SHost struct { + zone *SZone +} + +func (self *SHost) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SHost) GetId() string { + return fmt.Sprintf("%s-%s", self.zone.region.client.providerId, self.zone.GetId()) +} + +func (self *SHost) GetName() string { + return fmt.Sprintf("%s-%s", self.zone.region.client.providerName, self.zone.GetId()) +} + +func (self *SHost) GetGlobalId() string { + return fmt.Sprintf("%s-%s", self.zone.region.client.providerId, self.zone.GetId()) +} + +func (self *SHost) GetInstanceById(instanceId string) (*SInstance, error) { + inst, err := self.zone.region.GetInstance(instanceId) + if err != nil { + return nil, err + } + inst.host = self + return inst, nil +} + +func (self *SHost) CreateVM(name string, imgId string, sysDiskSize int, cpu int, memMB int, + vswitchId string, ipAddr string, desc string, passwd string, + storageType string, diskSizes []int, publicKey string, secgroupId string, userData string) (cloudprovider.ICloudVM, error) { + vmId, err := self._createVM(name, imgId, sysDiskSize, cpu, memMB, vswitchId, ipAddr, desc, passwd, storageType, diskSizes, publicKey, secgroupId, userData) + if err != nil { + return nil, err + } + vm, err := self.GetInstanceById(vmId) + if err != nil { + return nil, err + } + return vm, err +} + +func (self *SHost) _createVM(name string, imgId string, sysDiskSize int, cpu int, memMB int, + networkId string, ipAddr string, desc string, passwd string, + storageType string, diskSizes []int, publicKey string, secgroupId string, userData string) (string, error) { + + net := self.zone.getNetworkById(networkId) + if net == nil { + return "", fmt.Errorf("invalid network ID %s", networkId) + } + if net.wire == nil { + log.Errorf("network's wire is empty") + return "", fmt.Errorf("network's wire is empty") + } + if net.wire.vpc == nil { + log.Errorf("network's wire' vpc is empty") + return "", fmt.Errorf("network's wire's vpc is empty") + } + + var err error + + // if len(secgroupId) == 0 { + // secgroups, err := net.wire.vpc.GetISecurityGroups() + // if err != nil { + // return "", fmt.Errorf("get security group error %s", err) + // } + + // if len(secgroups) == 0 { + // secId, err := self.zone.region.createDefaultSecurityGroup(net.wire.vpc.VpcId) + // if err != nil { + // return "", fmt.Errorf("no secgroup for vpc and failed to create a default One!!") + // } else { + // secgroupId = secId + // } + // } else { + // secgroupId = secgroups[0].GetId() + // } + // } + + keypair := "" + if len(publicKey) > 0 { + keypair, err = self.zone.region.syncKeypair(publicKey) + if err != nil { + return "", err + } + } + + img, err := self.zone.region.GetImage(imgId) + if err != nil { + log.Errorf("getiamge fail %s", err) + return "", err + } + if img.ImageState != ImageStatusAvailable { + log.Errorf("image %s status %s", imgId, img.ImageState) + return "", fmt.Errorf("image not ready") + } + + err = self.zone.validateStorageType(storageType) + if err != nil { + return "", fmt.Errorf("Storage %s not avaiable: %s", storageType, err) + } + + disks := make([]SDisk, len(diskSizes)+1) + disks[0].DiskSize = img.ImageSize + if sysDiskSize > 0 && sysDiskSize > img.ImageSize { + disks[0].DiskSize = sysDiskSize + } + if disks[0].DiskSize < 50 { + disks[0].DiskSize = 50 + } + disks[0].DiskType = strings.ToUpper(storageType) + + for i, sz := range diskSizes { + disks[i+1].DiskSize = sz + disks[i+1].DiskType = strings.ToUpper(storageType) + } + + instanceTypes, err := self.zone.region.GetMatchInstanceTypes(cpu, memMB, 0, self.zone.Zone) + if err != nil { + return "", err + } + if len(instanceTypes) == 0 { + return "", fmt.Errorf("instance type %dC%dMB not avaiable", cpu, memMB) + } + + for _, instType := range instanceTypes { + instanceTypeId := instType.InstanceType + log.Debugf("Try instancetype : %s", instanceTypeId) + vmId, err := self.zone.region.CreateInstance(name, imgId, instanceTypeId, secgroupId, self.zone.Zone, desc, passwd, disks, networkId, ipAddr, keypair, userData) + if err != nil { + log.Errorf("Failed for %s: %s", instanceTypeId, err) + } else { + return vmId, nil + } + } + + return "", fmt.Errorf("Failed to create, specification not supported") +} + +func (self *SHost) Refresh() error { + return nil +} + +func (self *SHost) GetAccessIp() string { + return "" +} + +func (self *SHost) GetAccessMac() string { + return "" +} + +func (self *SHost) GetSN() string { + return "" +} + +func (self *SHost) GetCpuCount() int8 { + return 0 +} + +func (self *SHost) GetNodeCount() int8 { + return 0 +} + +func (self *SHost) GetCpuDesc() string { + return "" +} + +func (self *SHost) GetCpuMhz() int { + return 0 +} + +func (self *SHost) GetMemSizeMB() int { + return 0 +} + +func (self *SHost) GetStorageSizeMB() int { + return 0 +} + +func (self *SHost) GetEnabled() bool { + return true +} + +func (self *SHost) GetStatus() string { + return models.HOST_STATUS_RUNNING +} + +func (self *SHost) GetHostStatus() string { + return models.HOST_ONLINE +} + +func (self *SHost) GetHostType() string { + return models.HOST_TYPE_QCLOUD +} + +func (self *SHost) GetStorageType() string { + return models.DISK_TYPE_HYBRID +} + +func (self *SHost) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) { + return self.zone.GetIStorageById(id) +} + +func (self *SHost) GetIStorages() ([]cloudprovider.ICloudStorage, error) { + return self.zone.GetIStorages() +} + +func (self *SHost) GetIVMById(gid string) (cloudprovider.ICloudVM, error) { + parts, _, err := self.zone.region.GetInstances(self.zone.Zone, []string{gid}, 0, 1) + if err != nil { + return nil, err + } + if len(parts) == 0 { + return nil, cloudprovider.ErrNotFound + } + if len(parts) > 1 { + return nil, cloudprovider.ErrDuplicateId + } + parts[0].host = self + return &parts[0], nil +} + +func (self *SHost) GetIVMs() ([]cloudprovider.ICloudVM, error) { + vms := make([]SInstance, 0) + for { + parts, total, err := self.zone.region.GetInstances(self.zone.Zone, nil, len(vms), 50) + if err != nil { + return nil, err + } + vms = append(vms, parts...) + if len(vms) >= total { + break + } + } + ivms := make([]cloudprovider.ICloudVM, len(vms)) + for i := 0; i < len(vms); i++ { + vms[i].host = self + ivms[i] = &vms[i] + } + return ivms, nil +} + +func (self *SHost) GetIWires() ([]cloudprovider.ICloudWire, error) { + return self.zone.GetIWires() +} + +func (self *SHost) GetManagerId() string { + return self.zone.region.client.providerId +} + +func (self *SHost) GetSysInfo() jsonutils.JSONObject { + info := jsonutils.NewDict() + info.Add(jsonutils.NewString(CLOUD_PROVIDER_QCLOUD), "manufacture") + return info +} + +func (self *SHost) IsEmulated() bool { + return true +} diff --git a/pkg/util/qcloud/image.go b/pkg/util/qcloud/image.go new file mode 100644 index 0000000000..2d481ab146 --- /dev/null +++ b/pkg/util/qcloud/image.go @@ -0,0 +1,196 @@ +package qcloud + +import ( + "fmt" + "strconv" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" +) + +type ImageStatusType string + +const ( + ImageStatusCreating ImageStatusType = "Creating" + ImageStatusAvailable ImageStatusType = "NORMAL" + ImageStatusUnAvailable ImageStatusType = "UnAvailable" + ImageStatusCreateFailed ImageStatusType = "CreateFailed" +) + +type SImage struct { + storageCache *SStoragecache + + ImageId string // 镜像ID + OsName string // 镜像操作系统 + ImageType string // 镜像类型 + CreatedTime time.Time // 镜像创建时间 + ImageName string // 镜像名称 + ImageDescription string // 镜像描述 + ImageSize int // 镜像大小 + Architecture string // 镜像架构 + ImageState ImageStatusType // 镜像状态 + Platform string // 镜像来源平台 + ImageCreator string // 镜像创建者 + ImageSource string // 镜像来源 + SyncPercent int // 同步百分比 + IsSupportCloudinit bool // 镜像是否支持cloud-init +} + +func (self *SRegion) GetImages(status string, owner string, imageIds []string, name string, offset int, limit int) ([]SImage, int, error) { + if limit > 50 || limit <= 0 { + limit = 50 + } + params := make(map[string]string) + params["Limit"] = fmt.Sprintf("%d", limit) + params["Offset"] = fmt.Sprintf("%d", offset) + + filter := 0 + if len(status) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "image-state" + params[fmt.Sprintf("Filters.%d.Values.0", filter)] = status + filter++ + } + if imageIds != nil && len(imageIds) > 0 { + for index, imageId := range imageIds { + params[fmt.Sprintf("ImageIds.%d", index)] = imageId + } + } + if len(owner) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "image-type" + params[fmt.Sprintf("Filters.%d.Values.0", filter)] = owner + filter++ + } + + if len(name) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "image-name" + params[fmt.Sprintf("Filters.%d.Values.0", filter)] = name + filter++ + } + + images := make([]SImage, 0) + body, err := self.cvmRequest("DescribeImages", params) + if err != nil { + return nil, 0, err + } + err = body.Unmarshal(&images, "ImageSet") + if err != nil { + return nil, 0, err + } + for i := 0; i < len(images); i++ { + images[i].storageCache = self.getStoragecache() + } + total, _ := body.Int("TotalCount") + return images, int(total), nil +} +func (self *SImage) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SImage) GetId() string { + return self.ImageId +} + +func (self *SImage) GetName() string { + return self.ImageName +} + +func (self *SImage) IsEmulated() bool { + return false +} + +func (self *SImage) GetGlobalId() string { + return fmt.Sprintf("%s-%s") +} + +func (self *SImage) Delete() error { + return self.storageCache.region.DeleteImage(self.ImageId) +} + +func (self *SImage) GetStatus() string { + return string(self.ImageState) +} + +func (self *SImage) Refresh() error { + new, err := self.storageCache.region.GetImage(self.ImageId) + if err != nil { + return err + } + return jsonutils.Update(self, new) +} + +func (self *SRegion) GetImage(imageId string) (*SImage, error) { + images, _, err := self.GetImages("", "", []string{imageId}, "", 0, 1) + if err != nil { + return nil, err + } + if len(images) == 0 { + return nil, fmt.Errorf("image %s not found", imageId) + } + return &images[0], nil +} + +func (self *SImage) GetIStoragecache() cloudprovider.ICloudStoragecache { + return self.storageCache +} + +func (self *SRegion) DeleteImage(imageId string) error { + params := make(map[string]string) + params["ImageIds.0"] = imageId + + _, err := self.cvmRequest("DeleteImages", params) + return err +} + +func (self *SRegion) GetImageStatus(imageId string) (ImageStatusType, error) { + image, err := self.GetImage(imageId) + if err != nil { + return "", err + } + return image.ImageState, nil +} + +func (self *SRegion) GetImageByName(name string) (*SImage, error) { + images, _, err := self.GetImages("", "", nil, name, 0, 1) + if err != nil { + return nil, err + } + if len(images) == 0 { + return nil, cloudprovider.ErrNotFound + } + return &images[0], nil +} + +func (self *SRegion) ImportImage(name string, osArch string, osType string, osVersion string, imageUrl string) (*SImage, error) { + params := make(map[string]string) + params["ImageName"] = name + if _, err := strconv.Atoi(osVersion); len(osVersion) == 0 || err != nil { + osVersion = "-" + } + params["OsVersion"] = osVersion // "6|7|8|-" + if len(osType) == 0 || osType == "linux" { + osType = "Other Linux" + } + params["OsType"] = osType // "CentOS|Ubuntu|Debian|OpenSUSE|SUSE|CoreOS|FreeBSD|Other Linux|Windows Server 2008|Windows Server 2012|Windows Server 2016" + if len(osArch) == 0 { + osArch = "x86_64" + } + params["Architecture"] = osArch // "x86_64|i386" + params["ImageUrl"] = imageUrl + params["Force"] = "true" + + log.Debugf("Upload image with params %#v", params) + + if _, err := self.cvmRequest("ImportImage", params); err != nil { + return nil, err + } + for i := 0; i < 8; i++ { + image, err := self.GetImageByName(name) + if err == nil { + return image, nil + } + time.Sleep(time.Minute * time.Duration(i)) + } + return nil, cloudprovider.ErrNotFound +} diff --git a/pkg/util/qcloud/instance.go b/pkg/util/qcloud/instance.go new file mode 100644 index 0000000000..aa51d86820 --- /dev/null +++ b/pkg/util/qcloud/instance.go @@ -0,0 +1,736 @@ +package qcloud + +import ( + "fmt" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/pkg/util/secrules" + "yunion.io/x/pkg/utils" +) + +const ( + // PENDING:表示创建中 + // LAUNCH_FAILED:表示创建失败 + // RUNNING:表示运行中 + // STOPPED:表示关机 + // STARTING:表示开机中 + // STOPPING:表示关机中 + // REBOOTING:表示重启中 + // SHUTDOWN:表示停止待销毁 + // TERMINATING:表示销毁中。 + + InstanceStatusStopped = "STOPPED" + InstanceStatusRunning = "RUNNING" + InstanceStatusStopping = "STOPPING" + InstanceStatusStarting = "STARTING" +) + +type SystemDisk struct { + DiskType string //系统盘类型。系统盘类型限制详见CVM实例配置。取值范围:LOCAL_BASIC:本地硬盘 LOCAL_SSD:本地SSD硬盘 CLOUD_BASIC:普通云硬盘 CLOUD_SSD:SSD云硬盘 CLOUD_PREMIUM:高性能云硬盘 默认取值:CLOUD_BASIC。 + DiskId string // 系统盘ID。LOCAL_BASIC 和 LOCAL_SSD 类型没有ID。暂时不支持该参数。 + DiskSize float32 //系统盘大小,单位:GB。默认值为 50 +} + +type DataDisk struct { + DiskSize float32 // 数据盘大小,单位:GB。最小调整步长为10G,不同数据盘类型取值范围不同,具体限制详见:CVM实例配置。默认值为0,表示不购买数据盘。更多限制详见产品文档。 + DiskType string // 数据盘类型。数据盘类型限制详见CVM实例配置。取值范围:LOCAL_BASIC:本地硬盘 LOCAL_SSD:本地SSD硬盘 CLOUD_BASIC:普通云硬盘 CLOUD_PREMIUM:高性能云硬盘 CLOUD_SSD:SSD云硬盘 默认取值:LOCAL_BASIC。 该参数对ResizeInstanceDisk接口无效。 + DiskId string // 数据盘ID。LOCAL_BASIC 和 LOCAL_SSD 类型没有ID。暂时不支持该参数。 + DeleteWithInstance bool // 数据盘是否随子机销毁。取值范围:TRUE:子机销毁时,销毁数据盘 FALSE:子机销毁时,保留数据盘 默认取值:TRUE 该参数目前仅用于 RunInstances 接口。 +} + +type InternetAccessible struct { + InternetChargeType string //网络计费类型。取值范围:BANDWIDTH_PREPAID:预付费按带宽结算 TRAFFIC_POSTPAID_BY_HOUR:流量按小时后付费 BANDWIDTH_POSTPAID_BY_HOUR:带宽按小时后付费 BANDWIDTH_PACKAGE:带宽包用户 默认取值:非带宽包用户默认与子机付费类型保持一致。 + InternetMaxBandwidthOut int // 公网出带宽上限,单位:Mbps。默认值:0Mbps。不同机型带宽上限范围不一致,具体限制详见购买网络带宽。 + PublicIpAssigned bool // 是否分配公网IP。取值范围: TRUE:表示分配公网IP FALSE:表示不分配公网IP 当公网带宽大于0Mbps时,可自由选择开通与否,默认开通公网IP;当公网带宽为0,则不允许分配公网IP。 +} + +type VirtualPrivateCloud struct { + VpcId string // 私有网络ID,形如vpc-xxx。有效的VpcId可通过登录控制台查询;也可以调用接口 DescribeVpcEx ,从接口返回中的unVpcId字段获取。 + SubnetId string // 私有网络子网ID,形如subnet-xxx。有效的私有网络子网ID可通过登录控制台查询;也可以调用接口 DescribeSubnets ,从接口返回中的unSubnetId字段获取。 + AsVpcGateway bool // 是否用作公网网关。公网网关只有在实例拥有公网IP以及处于私有网络下时才能正常使用。取值范围:TRUE:表示用作公网网关 FALSE:表示不用作公网网关 默认取值:FALSE。 + PrivateIpAddresses []string // 私有网络子网 IP 数组,在创建实例、修改实例vpc属性操作中可使用此参数。当前仅批量创建多台实例时支持传入相同子网的多个 IP。 +} + +type LoginSettings struct { + Password string //实例登录密码。不同操作系统类型密码复杂度限制不一样,具体如下:Linux实例密码必须8到16位,至少包括两项[a-z,A-Z]、[0-9] 和 [( ) ~ ! @ # $ % ^ & * - + = | { } [ ] : ; ' , . ? / ]中的特殊符号。
  • Windows实例密码必须12到16位,至少包括三项[a-z],[A-Z],[0-9] 和 [( ) ~ ! @ # $ % ^ & * - + = { } [ ] : ; ' , . ? /]中的特殊符号。 若不指定该参数,则由系统随机生成密码,并通过站内信方式通知到用户。 + KeyIds []string // 密钥ID列表。关联密钥后,就可以通过对应的私钥来访问实例;KeyId可通过接口DescribeKeyPairs获取,密钥与密码不能同时指定,同时Windows操作系统不支持指定密钥。当前仅支持购买的时候指定一个密钥。 + KeepImageLogin string // 保持镜像的原始设置。该参数与Password或KeyIds.N不能同时指定。只有使用自定义镜像、共享镜像或外部导入镜像创建实例时才能指定该参数为TRUE。取值范围: TRUE:表示保持镜像的登录设置 FALSE:表示不保持镜像的登录设置 默认取值:FALSE。 +} + +type Tag struct { + Key string + Value string +} + +type SInstance struct { + host *SHost + + image *SImage + idisks []cloudprovider.ICloudDisk + + Placement Placement + InstanceId string + InstanceType string + CPU int8 + Memory int + RestrictState string //NORMAL EXPIRED PROTECTIVELY_ISOLATED + InstanceName string + InstanceChargeType InstanceChargeType //PREPAID:表示预付费,即包年包月 POSTPAID_BY_HOUR:表示后付费,即按量计费 CDHPAID:CDH付费,即只对CDH计费,不对CDH上的实例计费。 + SystemDisk SystemDisk //实例系统盘信息。 + DataDisks []DataDisk //实例数据盘信息。只包含随实例购买的数据盘。 + PrivateIpAddresses []string //实例主网卡的内网IP列表。 + PublicIpAddresses []string //实例主网卡的公网IP列表。 + InternetAccessible InternetAccessible //实例带宽信息。 + VirtualPrivateCloud VirtualPrivateCloud //实例所属虚拟私有网络信息。 + ImageId string // 生产实例所使用的镜像ID。 + RenewFlag string // 自动续费标识。取值范围:NOTIFY_AND_MANUAL_RENEW:表示通知即将过期,但不自动续费 NOTIFY_AND_AUTO_RENEW:表示通知即将过期,而且自动续费 DISABLE_NOTIFY_AND_MANUAL_RENEW:表示不通知即将过期,也不自动续费。 + CreatedTime time.Time // 创建时间。按照ISO8601标准表示,并且使用UTC时间。格式为:YYYY-MM-DDThh:mm:ssZ。 + ExpiredTime time.Time // 到期时间。按照ISO8601标准表示,并且使用UTC时间。格式为:YYYY-MM-DDThh:mm:ssZ。 + OsName string // 操作系统名称。 + SecurityGroupIds []string // 实例所属安全组。该参数可以通过调用 DescribeSecurityGroups 的返回值中的sgId字段来获取。 + LoginSettings LoginSettings //实例登录设置。目前只返回实例所关联的密钥。 + InstanceState string // 实例状态。取值范围:PENDING:表示创建中 LAUNCH_FAILED:表示创建失败 RUNNING:表示运行中 STOPPED:表示关机 STARTING:表示开机中 STOPPING:表示关机中 REBOOTING:表示重启中 SHUTDOWN:表示停止待销毁 TERMINATING:表示销毁中。 + Tags []Tag +} + +func (self *SRegion) GetInstances(zoneId string, ids []string, offset int, limit int) ([]SInstance, int, error) { + params := make(map[string]string) + if limit < 1 || limit > 50 { + limit = 50 + } + + params["Limit"] = fmt.Sprintf("%d", limit) + params["Offset"] = fmt.Sprintf("%d", offset) + instances := make([]SInstance, 0) + if ids != nil && len(ids) > 0 { + for index, id := range ids { + params[fmt.Sprintf("InstanceIds.%d", index)] = id + } + } else { + if len(zoneId) > 0 { + params["Filters.0.Name"] = "zone" + params["Filters.0.Values.0"] = zoneId + } + } + body, err := self.cvmRequest("DescribeInstances", params) + if err != nil { + return nil, 0, err + } + err = body.Unmarshal(&instances, "InstanceSet") + if err != nil { + return nil, 0, err + } + total, _ := body.Float("TotalCount") + return instances, int(total), nil +} + +func (self *SInstance) GetMetadata() *jsonutils.JSONDict { + data := jsonutils.NewDict() + if self.image == nil { + image, err := self.host.zone.region.GetImage(self.ImageId) + if err == nil { + self.image = image + } + } + + if self.image != nil { + data.Add(jsonutils.NewString(self.image.OsName), "os_distribution") + } + + return data +} + +func (self *SInstance) GetCreateTime() time.Time { + return self.CreatedTime +} + +func (self *SInstance) GetIHost() cloudprovider.ICloudHost { + return self.host +} + +func (self *SInstance) GetId() string { + return self.InstanceId +} + +func (self *SInstance) GetName() string { + return self.InstanceName +} + +func (self *SInstance) GetGlobalId() string { + return self.InstanceId +} + +func (self *SInstance) IsEmulated() bool { + return false +} + +func (self *SInstance) getVpc() (*SVpc, error) { + return self.host.zone.region.getVpc(self.VirtualPrivateCloud.VpcId) +} + +func (self *SInstance) GetIDisks() ([]cloudprovider.ICloudDisk, error) { + disks, total, err := self.host.zone.region.GetDisks(self.InstanceId, "", "", nil, 0, 50) + if err != nil { + log.Errorf("fetchDisks fail %s", err) + return nil, err + } + if total > len(disks) { + disks, _, err = self.host.zone.region.GetDisks(self.InstanceId, "", "", nil, 0, total) + } + idisks := make([]cloudprovider.ICloudDisk, len(disks)) + for i := 0; i < len(disks); i += 1 { + store, err := self.host.zone.getStorageByCategory(disks[i].DiskType) + if err != nil { + return nil, err + } + disks[i].storage = store + idisks[i] = &disks[i] + } + if utils.IsInStringArray(self.SystemDisk.DiskType, []string{"LOCAL_BASIC", "LOCAL_SSD"}) { + storage := SLocalStorage{zone: self.host.zone, storageType: self.SystemDisk.DiskType} + disk := SLocalDisk{ + storage: &storage, + DiskId: self.SystemDisk.DiskId, + DiskSize: self.SystemDisk.DiskSize, + DisktType: self.SystemDisk.DiskType, + DiskUsage: "SYSTEM_DISK", + } + idisks = append(idisks, &disk) + } + + for i := 0; i < len(self.DataDisks); i++ { + if utils.IsInStringArray(self.DataDisks[i].DiskType, []string{"LOCAL_BASIC", "LOCAL_SSD"}) { + storage := SLocalStorage{zone: self.host.zone, storageType: self.DataDisks[i].DiskType} + disk := SLocalDisk{ + storage: &storage, + DiskId: self.DataDisks[i].DiskId, + DiskSize: self.DataDisks[i].DiskSize, + DisktType: self.DataDisks[i].DiskType, + DiskUsage: "DATA_DISK", + } + idisks = append(idisks, &disk) + } + } + + return idisks, nil +} + +func (self *SInstance) GetINics() ([]cloudprovider.ICloudNic, error) { + nics := make([]cloudprovider.ICloudNic, 0) + for _, ip := range self.VirtualPrivateCloud.PrivateIpAddresses { + nic := SInstanceNic{instance: self, ipAddr: ip} + nics = append(nics, &nic) + } + return nics, nil +} + +func (self *SInstance) GetVcpuCount() int8 { + return self.CPU +} + +func (self *SInstance) GetVmemSizeMB() int { + return self.Memory * 1024 +} + +func (self *SInstance) GetBootOrder() string { + return "dcn" +} + +func (self *SInstance) GetVga() string { + return "std" +} + +func (self *SInstance) GetVdi() string { + return "vnc" +} + +func (self *SInstance) GetOSType() string { + if self.image == nil { + image, err := self.host.zone.region.GetImage(self.ImageId) + if err != nil { + return self.OsName + } + self.image = image + } + if self.image != nil { + switch self.image.Platform { + case "Windows": + return "Windows" + case "CentOS", "Debian", "FreeBSD", "SUSE", "openSUSE": + return "Linux" + default: + return "Linux" + } + } + return self.OsName +} + +func (self *SInstance) GetOSName() string { + return self.OsName +} + +func (self *SInstance) GetBios() string { + return "BIOS" +} + +func (self *SInstance) GetMachine() string { + return "pc" +} + +func (self *SInstance) GetStatus() string { + switch self.InstanceState { + case "PENDING": + return models.VM_DEPLOYING + case "LAUNCH_FAILED": + return models.VM_DEPLOY_FAILED + case "RUNNING": + return models.VM_RUNNING + case "STOPPED": + return models.VM_READY + case "STARTING", "REBOOTING": + return models.VM_STARTING + case "STOPPING": + return models.VM_STOPPING + case "SHUTDOWN": + return models.VM_DEALLOCATED + case "TERMINATING": + return models.VM_DELETING + default: + return models.VM_UNKNOWN + } +} + +func (self *SInstance) Refresh() error { + new, err := self.host.zone.region.GetInstance(self.InstanceId) + if err != nil { + return err + } + return jsonutils.Update(self, new) +} + +func (self *SInstance) GetHypervisor() string { + return models.HYPERVISOR_QCLOUD +} + +func (self *SInstance) StartVM() error { + timeout := 300 * time.Second + interval := 15 * time.Second + + startTime := time.Now() + for time.Now().Sub(startTime) < timeout { + err := self.Refresh() + if err != nil { + return err + } + log.Debugf("status %s expect %s", self.GetStatus(), models.VM_RUNNING) + if self.GetStatus() == models.VM_RUNNING { + return nil + } + if self.GetStatus() == models.VM_READY { + err := self.host.zone.region.StartVM(self.InstanceId) + if err != nil { + return err + } + } + time.Sleep(interval) + } + return cloudprovider.ErrTimeout +} + +func (self *SInstance) StopVM(isForce bool) error { + err := self.host.zone.region.StopVM(self.InstanceId, isForce) + if err != nil { + return err + } + return cloudprovider.WaitStatus(self, models.VM_READY, 10*time.Second, 300*time.Second) // 5mintues +} + +func (self *SInstance) GetVNCInfo() (jsonutils.JSONObject, error) { + url, err := self.host.zone.region.GetInstanceVNCUrl(self.InstanceId) + if err != nil { + return nil, err + } + ret := jsonutils.NewDict() + ret.Add(jsonutils.NewString("https://img.qcloud.com/qcloud/app/active_vnc/index.html?InstanceVncUrl="+url), "url") + ret.Add(jsonutils.NewString("qcloud"), "protocol") + ret.Add(jsonutils.NewString(self.InstanceId), "instance_id") + return ret, nil +} + +func (self *SInstance) UpdateVM(name string) error { + return self.host.zone.region.UpdateVM(self.InstanceId, name) +} + +func (self *SInstance) DeployVM(name string, password string, publicKey string, deleteKeypair bool, description string) error { + var keypairName string + if len(publicKey) > 0 { + var err error + keypairName, err = self.host.zone.region.syncKeypair(publicKey) + if err != nil { + return err + } + } + + return self.host.zone.region.DeployVM(self.InstanceId, name, password, keypairName, deleteKeypair, description) +} + +func (self *SInstance) RebuildRoot(imageId string, passwd string, publicKey string, sysSizeGB int) (string, error) { + keypair := "" + if len(publicKey) > 0 { + var err error + keypair, err = self.host.zone.region.syncKeypair(publicKey) + if err != nil { + return "", err + } + } + err := self.host.zone.region.ReplaceSystemDisk(self.InstanceId, imageId, passwd, keypair, sysSizeGB) + if err != nil { + return "", err + } + self.StopVM(true) + instance, err := self.host.zone.region.GetInstance(self.InstanceId) + if err != nil { + return "", err + } + return instance.SystemDisk.DiskId, nil +} + +func (self *SInstance) ChangeConfig(instanceId string, ncpu int, vmem int) error { + return self.host.zone.region.ChangeVMConfig(self.Placement.Zone, self.InstanceId, ncpu, vmem, nil) +} + +func (self *SInstance) AttachDisk(diskId string) error { + return self.host.zone.region.AttachDisk(self.InstanceId, diskId) +} + +func (self *SInstance) DetachDisk(diskId string) error { + return self.host.zone.region.DetachDisk(self.InstanceId, diskId) +} + +func (self *SRegion) GetInstance(instanceId string) (*SInstance, error) { + instances, _, err := self.GetInstances("", []string{instanceId}, 0, 1) + if err != nil { + return nil, err + } + if len(instances) == 0 { + return nil, cloudprovider.ErrNotFound + } + return &instances[0], nil +} + +func (self *SRegion) CreateInstance(name string, imageId string, instanceType string, securityGroupId string, + zoneId string, desc string, passwd string, disks []SDisk, networkId string, ipAddr string, + keypair string, userData string) (string, error) { + params := make(map[string]string) + params["Region"] = self.Region + params["ImageId"] = imageId + params["InstanceType"] = instanceType + //params["SecurityGroupId"] = securityGroupId + params["Placement.Zone"] = zoneId + params["InstanceName"] = name + params["Description"] = desc + params["InstanceChargeType"] = "POSTPAID_BY_HOUR" + params["InternetAccessible.InternetMaxBandwidthOut"] = "100" + params["InternetAccessible.PublicIpAssigned"] = "FALSE" + params["HostName"] = name + if len(passwd) > 0 { + params["LoginSettings.Password"] = passwd + } else { + params["LoginSettings.KeepImageLogin"] = "TRUE" + } + + if len(userData) > 0 { + params["UserData"] = userData + } + + //params["IoOptimized"] = "optimized" + for i, d := range disks { + if i == 0 { + params["SystemDisk.DiskType"] = d.DiskType + params["SystemDisk.DiskSize"] = fmt.Sprintf("%d", d.DiskSize) + } else { + params[fmt.Sprintf("DataDisks.%d.DiskSize", i-1)] = fmt.Sprintf("%d", d.DiskSize) + params[fmt.Sprintf("DataDisks.%d.DiskType", i-1)] = d.DiskType + } + } + network, err := self.GetNetwork(networkId) + if err != nil { + return "", err + } + params["VirtualPrivateCloud.SubnetId"] = networkId + params["VirtualPrivateCloud.VpcId"] = network.VpcId + if len(ipAddr) > 0 { + params["VirtualPrivateCloud.PrivateIpAddresses.0"] = ipAddr + } + // if len(keypair) > 0 { + // params["KeyPairName"] = keypair + // } + params["ClientToken"] = utils.GenRequestId(20) + //log.Errorf("create params: %s", jsonutils.Marshal(params).PrettyString()) + instanceIdSet := []string{} + body, err := self.cvmRequest("RunInstances", params) + if err != nil { + log.Errorf("RunInstances fail %s", err) + return "", err + } + err = body.Unmarshal(&instanceIdSet, "InstanceIdSet") + if err == nil && len(instanceIdSet) > 0 { + return instanceIdSet[0], nil + } + return "", fmt.Errorf("Failed to create instance") +} + +func (self *SRegion) doStartVM(instanceId string) error { + return self.instanceOperation(instanceId, "StartInstances", nil) +} + +func (self *SRegion) doStopVM(instanceId string, isForce bool) error { + params := make(map[string]string) + if isForce { + params["ForceStop"] = "true" + } else { + params["ForceStop"] = "false" + } + return self.instanceOperation(instanceId, "StopInstances", params) +} + +func (self *SRegion) doDeleteVM(instanceId string) error { + params := make(map[string]string) + return self.instanceOperation(instanceId, "TerminateInstances", params) +} + +func (self *SRegion) StartVM(instanceId string) error { + status, err := self.GetInstanceStatus(instanceId) + if err != nil { + log.Errorf("Fail to get instance status on StartVM: %s", err) + return err + } + if status != InstanceStatusStopped { + log.Errorf("StartVM: vm status is %s expect %s", status, InstanceStatusStopped) + return cloudprovider.ErrInvalidStatus + } + return self.doStartVM(instanceId) +} + +func (self *SRegion) StopVM(instanceId string, isForce bool) error { + status, err := self.GetInstanceStatus(instanceId) + if err != nil { + log.Errorf("Fail to get instance status on StopVM: %s", err) + return err + } + if status == InstanceStatusStopped { + return nil + } + return self.doStopVM(instanceId, isForce) +} + +func (self *SRegion) DeleteVM(instanceId string) error { + status, err := self.GetInstanceStatus(instanceId) + if err != nil { + log.Errorf("Fail to get instance status on DeleteVM: %s", err) + return err + } + log.Debugf("Instance status on delete is %s", status) + if status != InstanceStatusStopped { + log.Warningf("DeleteVM: vm status is %s expect %s", status, InstanceStatusStopped) + } + return self.doDeleteVM(instanceId) +} + +func (self *SRegion) DeployVM(instanceId string, name string, password string, keypairName string, deleteKeypair bool, description string) error { + instance, err := self.GetInstance(instanceId) + if err != nil { + return err + } + + // 修改密钥时直接返回 + if deleteKeypair { + for i := 0; i < len(instance.LoginSettings.KeyIds); i++ { + err = self.DetachKeyPair(instanceId, instance.LoginSettings.KeyIds[i]) + if err != nil { + return err + } + } + } + + if len(keypairName) > 0 { + err = self.AttachKeypair(instanceId, keypairName) + if err != nil { + return err + } + } + + params := make(map[string]string) + + if len(name) > 0 && instance.InstanceName != name { + params["InstanceName"] = name + } + + if len(params) > 0 { + err := self.modifyInstanceAttribute(instanceId, params) + if err != nil { + return err + } + } + if len(password) > 0 { + return self.instanceOperation(instanceId, "ResetInstancesPassword", map[string]string{"Password": password}) + } + return nil +} + +func (self *SInstance) DeleteVM() error { + for { + err := self.host.zone.region.DeleteVM(self.InstanceId) + if err != nil { + // if isError(err, "IncorrectInstanceStatus.Initializing") { + // log.Infof("The instance is initializing, try later ...") + // time.Sleep(10 * time.Second) + // } else { + // return err + // } + } else { + break + } + } + return cloudprovider.WaitDeleted(self, 10*time.Second, 300*time.Second) // 5minutes +} + +func (self *SRegion) UpdateVM(instanceId string, hostname string) error { + params := make(map[string]string) + params["HostName"] = hostname + return self.modifyInstanceAttribute(instanceId, params) +} + +func (self *SRegion) modifyInstanceAttribute(instanceId string, params map[string]string) error { + return self.instanceOperation(instanceId, "ModifyInstancesAttribute", params) +} + +func (self *SRegion) ReplaceSystemDisk(instanceId string, imageId string, passwd string, keypairName string, sysDiskSizeGB int) error { + params := make(map[string]string) + params["InstanceId"] = instanceId + params["ImageId"] = imageId + params["EnhancedService.SecurityService.Enabled"] = "TRUE" + params["EnhancedService.MonitorService.Enabled"] = "TRUE" + if len(passwd) > 0 { + params["LoginSettings.Password"] = passwd + } else { + params["LoginSettings.KeepImageLogin"] = "TRUE" + } + if len(keypairName) > 0 { + params["LoginSettings.KeyIds.0"] = keypairName + } + if sysDiskSizeGB > 0 { + params["SystemDisk.DiskSize"] = fmt.Sprintf("%d", sysDiskSizeGB) + } + _, err := self.cvmRequest("ResetInstance", params) + return err +} + +func (self *SRegion) ChangeVMConfig(zoneId string, instanceId string, ncpu int, vmem int, disks []*SDisk) error { + // todo: support change disk config? + params := make(map[string]string) + instanceTypes, e := self.GetMatchInstanceTypes(ncpu, vmem, 0, zoneId) + if e != nil { + return e + } + + for _, instancetype := range instanceTypes { + params["InstanceType"] = instancetype.InstanceType + err := self.instanceOperation(instanceId, "ResetInstancesType", params) + if err != nil { + log.Errorf("Failed for %s: %s", instancetype.InstanceType, err) + } + return nil + } + + return fmt.Errorf("Failed to change vm config, specification not supported") +} + +func (self *SRegion) DetachDisk(instanceId string, diskId string) error { + params := make(map[string]string) + params["InstanceId"] = instanceId + params["DiskId"] = diskId + log.Infof("Detach instance %s disk %s", instanceId, diskId) + _, err := self.cvmRequest("DetachDisk", params) + if err != nil { + log.Errorf("DetachDisk %s to %s fail %s", diskId, instanceId, err) + return err + } + + return nil +} + +func (self *SRegion) AttachDisk(instanceId string, diskId string) error { + params := make(map[string]string) + params["InstanceId"] = instanceId + params["DiskIds.0"] = diskId + _, err := self.cbsRequest("AttachDisks", params) + if err != nil { + log.Errorf("AttachDisks %s to %s fail %s", diskId, instanceId, err) + return err + } + return nil +} + +func (self *SInstance) SyncSecurityGroup(secgroupId string, name string, rules []secrules.SecurityRule) error { + // if vpc, err := self.getVpc(); err != nil { + // return err + // } else if len(secgroupId) == 0 { + // for index, secgrpId := range self.SecurityGroupIds.SecurityGroupId { + // if err := vpc.revokeSecurityGroup(secgrpId, self.InstanceId, index == 0); err != nil { + // return err + // } + // } + // } else if secgrpId, err := vpc.SyncSecurityGroup(secgroupId, name, rules); err != nil { + // return err + // } else if err := vpc.assignSecurityGroup(secgrpId, self.InstanceId); err != nil { + // return err + // } else { + // for _, secgroupId := range self.SecurityGroupIds.SecurityGroupId { + // if secgroupId != secgrpId { + // if err := vpc.revokeSecurityGroup(secgroupId, self.InstanceId, false); err != nil { + // return err + // } + // } + // } + // self.SecurityGroupIds.SecurityGroupId = []string{secgrpId} + // } + return nil +} + +func (self *SInstance) GetIEIP() (cloudprovider.ICloudEIP, error) { + if len(self.PublicIpAddresses) > 0 { + eip := SEipAddress{region: self.host.zone.region} + eip.AddressIp = self.PublicIpAddresses[0] + eip.InstanceId = self.InstanceId + eip.AddressId = self.InstanceId + eip.AddressName = self.PublicIpAddresses[0] + eip.AddressType = "WanIP" + eip.AddressStatus = EIP_STATUS_INUSE + return &eip, nil + } + return nil, nil +} + +func (self *SInstance) GetBillingType() string { + switch self.InstanceChargeType { + case PrePaidInstanceChargeType: + return models.BILLING_TYPE_PREPAID + case PostPaidInstanceChargeType: + return models.BILLING_TYPE_POSTPAID + default: + return models.BILLING_TYPE_PREPAID + } +} + +func (self *SInstance) GetExpiredAt() time.Time { + return self.ExpiredTime +} + +func (self *SInstance) UpdateUserData(userData string) error { + return cloudprovider.ErrNotSupported +} diff --git a/pkg/util/qcloud/instancenic.go b/pkg/util/qcloud/instancenic.go new file mode 100644 index 0000000000..82e36130dd --- /dev/null +++ b/pkg/util/qcloud/instancenic.go @@ -0,0 +1,40 @@ +package qcloud + +import ( + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/pkg/util/netutils" +) + +type SInstanceNic struct { + instance *SInstance + ipAddr string +} + +func (self *SInstanceNic) GetIP() string { + return self.ipAddr +} + +func (self *SInstanceNic) GetMAC() string { + ip, _ := netutils.NewIPV4Addr(self.ipAddr) + return ip.ToMac("00:16:") +} + +func (self *SInstanceNic) GetDriver() string { + return "virtio" +} + +func (self *SInstanceNic) GetINetwork() cloudprovider.ICloudNetwork { + networkId := self.instance.VirtualPrivateCloud.VpcId + wires, err := self.instance.host.GetIWires() + if err != nil { + return nil + } + for i := 0; i < len(wires); i++ { + wire := wires[i].(*SWire) + net := wire.getNetworkById(networkId) + if net != nil { + return net + } + } + return nil +} diff --git a/pkg/util/qcloud/instancetype.go b/pkg/util/qcloud/instancetype.go new file mode 100644 index 0000000000..09b2f2cb72 --- /dev/null +++ b/pkg/util/qcloud/instancetype.go @@ -0,0 +1,42 @@ +package qcloud + +import "yunion.io/x/log" + +// "time" + +// {"CpuCoreCount":1,"EniQuantity":1,"GPUAmount":0,"GPUSpec":"","InstanceTypeFamily":"ecs.t1","InstanceTypeId":"ecs.t1.xsmall","LocalStorageCategory":"","MemorySize":0.500000} +// InstanceBandwidthRx":26214400,"InstanceBandwidthTx":26214400,"InstancePpsRx":4500000,"InstancePpsTx":4500000 + +type SInstanceType struct { + Zone string // 可用区。 + InstanceType string // 实例机型。 + InstanceFamily string // 实例机型系列。 + GPU int // GPU核数,单位:核。 + CPU int // CPU核数,单位:核。 + Memory int // 内存容量,单位:GB。 + CbsSupport string // 是否支持云硬盘。取值范围:TRUE:表示支持云硬盘;FALSE:表示不支持云硬盘。 + InstanceTypeState string // 机型状态。取值范围:AVAILABLE:表示机型可用;UNAVAILABLE:表示机型不可用。 +} + +func (self *SRegion) GetInstanceTypes() ([]SInstanceType, error) { + params := make(map[string]string) + params["Region"] = self.Region + + body, err := self.cvmRequest("DescribeInstanceTypeConfigs", params) + if err != nil { + log.Errorf("DescribeInstanceTypeConfigs fail %s", err) + return nil, err + } + + instanceTypes := make([]SInstanceType, 0) + err = body.Unmarshal(&instanceTypes, "InstanceTypeConfigSet") + if err != nil { + log.Errorf("Unmarshal instance type details fail %s", err) + return nil, err + } + return instanceTypes, nil +} + +func (self *SInstanceType) memoryMB() int { + return int(self.Memory * 1024) +} diff --git a/pkg/util/qcloud/keypair.go b/pkg/util/qcloud/keypair.go new file mode 100644 index 0000000000..012b478788 --- /dev/null +++ b/pkg/util/qcloud/keypair.go @@ -0,0 +1,143 @@ +package qcloud + +import ( + "fmt" + "strconv" + "time" + + "github.com/aokoli/goutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" +) + +type SKeypair struct { + AssociatedInstanceIds []string + CreateTime time.Time + Description string + KeyId string + KeyName string + PublicKey string +} + +func (self *SRegion) GetKeypairs(name string, keyIds []string, offset int, limit int) ([]SKeypair, int, error) { + if limit > 50 || limit <= 0 { + limit = 50 + } + params := map[string]string{} + params["Limit"] = fmt.Sprintf("%d", limit) + params["Offset"] = fmt.Sprintf("%d", offset) + + if len(keyIds) > 0 { + for i := 0; i < len(keyIds); i++ { + params[fmt.Sprintf("KeyIds.%d", i)] = keyIds[i] + } + } else { + if len(name) > 0 { + params["Filters.0.Name"] = "key-name" + params["Filters.0.Values.0"] = name + } + } + + body, err := self.cvmRequest("DescribeKeyPairs", params) + if err != nil { + log.Errorf("GetKeypairs fail %s", err) + return nil, 0, err + } + + keypairs := []SKeypair{} + err = body.Unmarshal(&keypairs, "KeyPairSet") + if err != nil { + log.Errorf("Unmarshal keypair fail %s", err) + return nil, 0, err + } + total, _ := body.Int("TotalCount") + return keypairs, int(total), nil +} + +func (self *SRegion) ImportKeypair(name string, pubKey string) (*SKeypair, error) { + params := map[string]string{} + params["PublicKey"] = pubKey + params["ProjectId"] = "0" + params["KeyName"] = name + + body, err := self.cvmRequest("ImportKeyPair", params) + if err != nil { + log.Errorf("ImportKeypair fail %s", err) + return nil, err + } + + keypairID, err := body.GetString("KeyId") + if err != nil { + return nil, err + } + keypairs, total, err := self.GetKeypairs("", []string{keypairID}, 0, 1) + if err != nil { + return nil, err + } + if total != 1 { + return nil, cloudprovider.ErrNotFound + } + return &keypairs[0], nil +} + +func (self *SRegion) AttachKeypair(instanceId string, keypairId string) error { + params := map[string]string{} + params["InstanceIds.0"] = instanceId + params["KeyIds.0"] = keypairId + _, err := self.cvmRequest("AssociateInstancesKeyPairs", params) + return err +} + +func (self *SRegion) DetachKeyPair(instanceId string, keypairId string) error { + params := make(map[string]string) + params["InstanceIds.0"] = instanceId + params["KeyIds.0"] = keypairId + _, err := self.cvmRequest("DisassociateInstancesKeyPairs", params) + return err +} + +func (self *SRegion) CreateKeyPair(name string) (*SKeypair, error) { + params := make(map[string]string) + params["KeyName"] = name + params["ProjectId"] = "0" + body, err := self.cvmRequest("CreateKeyPair", params) + keypair := SKeypair{} + err = body.Unmarshal(&keypair, "KeyPair") + if err != nil { + return nil, err + } + return &keypair, err +} + +func (self *SRegion) lookUpAliyunKeypair(publicKey string) (string, error) { + keypairs, _, err := self.GetKeypairs("", []string{}, 0, 0) + if err != nil { + return "", err + } + + for i := 0; i < len(keypairs); i++ { + if keypairs[i].PublicKey == publicKey { + return keypairs[i].KeyId, nil + } + } + return "", cloudprovider.ErrNotFound +} + +func (self *SRegion) syncKeypair(publicKey string) (string, error) { + keypairId, e := self.lookUpAliyunKeypair(publicKey) + if e == nil { + return keypairId, nil + } + + prefix, e := goutils.RandomAlphabetic(6) + if e != nil { + return "", fmt.Errorf("publicKey error %s", e) + } + + name := prefix + strconv.FormatInt(time.Now().Unix(), 10) + keypair, err := self.ImportKeypair(name, publicKey) + if err != nil { + return "", err + } + return keypair.KeyId, nil +} diff --git a/pkg/util/qcloud/latitud_and_longitude.go b/pkg/util/qcloud/latitud_and_longitude.go new file mode 100644 index 0000000000..bc5e9999e5 --- /dev/null +++ b/pkg/util/qcloud/latitud_and_longitude.go @@ -0,0 +1,23 @@ +package qcloud + +var LatitudeAndLongitude = map[string]map[string]float32{ + "ap-bangkok": {"latitude": 13.756330, "longitude": 100.501762}, // 腾讯云 亚太地区(曼谷) + "ap-beijing": {"latitude": 39.904202, "longitude": 116.407394}, // 腾讯云 华北地区(北京) + "ap-chengdu": {"latitude": 30.572815, "longitude": 104.066803}, // 腾讯云 西南地区(成都) + "ap-chongqing": {"latitude": 29.431585, "longitude": 106.912254}, // 腾讯云 西南地区(重庆) + "ap-guangzhou": {"latitude": 23.129110, "longitude": 113.264381}, // 腾讯云 华南地区(广州) + "ap-guangzhou-open": {"latitude": 23.126593, "longitude": 113.273415}, // 腾讯云 华南地区(广州Open) + "ap-hongkong": {"latitude": 22.396427, "longitude": 114.109497}, // 腾讯云 东南亚地区(香港) + "ap-mumbai": {"latitude": 19.075983, "longitude": 72.877655}, // 腾讯云 亚太地区(孟买) + "ap-seoul": {"latitude": 37.566536, "longitude": 126.977966}, // 腾讯云 东南亚地区(首尔) + "ap-shanghai": {"latitude": 31.230391, "longitude": 121.473701}, // 腾讯云 华东地区(上海) + "ap-shanghai-fsi": {"latitude": 31.311033, "longitude": 121.536217}, // 腾讯云 华东地区(上海金融) + "ap-shenzhen-fsi": {"latitude": 22.531544, "longitude": 114.025467}, // 腾讯云 华南地区(深圳金融) + "ap-singapore": {"latitude": 1.352083, "longitude": 103.819839}, // 腾讯云 东南亚地区(新加坡) + "ap-tokyo": {"latitude": 35.709026, "longitude": 139.731995}, // 腾讯云 亚太地区(东京) + "eu-frankfurt": {"latitude": 51.165691, "longitude": 10.451526}, // 腾讯云 欧洲地区(德国) + "eu-moscow": {"latitude": 55.755825, "longitude": 37.617298}, // 腾讯云 欧洲地区(莫斯科) + "na-ashburn": {"latitude": 37.431572, "longitude": -78.656891}, // 腾讯云 美国东部(弗吉尼亚) + "na-siliconvalley": {"latitude": 37.387474, "longitude": -122.057541}, // 腾讯云 美国西部(硅谷) + "na-toronto": {"latitude": 43.653225, "longitude": -79.383186}, // 腾讯云 北美地区(多伦多) +} diff --git a/pkg/util/qcloud/localdisk.go b/pkg/util/qcloud/localdisk.go new file mode 100644 index 0000000000..ad91c8a764 --- /dev/null +++ b/pkg/util/qcloud/localdisk.go @@ -0,0 +1,128 @@ +package qcloud + +import ( + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" +) + +type SLocalDisk struct { + storage *SLocalStorage + DiskId string + DiskSize float32 + DisktType string + DiskUsage string +} + +func (self *SLocalDisk) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SLocalDisk) CreateISnapshot(name, desc string) (cloudprovider.ICloudSnapshot, error) { + return nil, cloudprovider.ErrNotSupported +} + +func (self *SLocalDisk) Delete() error { + return nil +} + +func (self *SLocalDisk) GetBillingType() string { + return "" +} + +func (self *SLocalDisk) GetFsFormat() string { + return "" +} + +func (self *SLocalDisk) GetIsNonPersistent() bool { + return false +} + +func (self *SLocalDisk) GetDriver() string { + return "scsi" +} + +func (self *SLocalDisk) GetCacheMode() string { + return "none" +} + +func (self *SLocalDisk) GetMountpoint() string { + return "" +} + +func (self *SLocalDisk) GetDiskFormat() string { + return "vhd" +} + +func (self *SLocalDisk) GetDiskSizeMB() int { + return int(self.DiskSize) * 1024 +} + +func (self *SLocalDisk) GetIsAutoDelete() bool { + return true +} + +func (self *SLocalDisk) GetExpiredAt() time.Time { + return time.Now() +} + +func (self *SLocalDisk) GetDiskType() string { + switch self.DiskUsage { + case "SYSTEM_DISK": + return models.DISK_TYPE_SYS + case "DATA_DISK": + return models.DISK_TYPE_DATA + default: + return models.DISK_TYPE_DATA + } +} + +func (self *SLocalDisk) Refresh() error { + return nil +} + +func (self *SLocalDisk) Reset(snapshotId string) error { + return cloudprovider.ErrNotSupported +} + +func (self *SLocalDisk) GetTemplateId() string { + return "" +} + +func (self *SLocalDisk) GetStatus() string { + return models.DISK_READY +} + +func (self *SLocalDisk) GetName() string { + return self.DiskId +} + +func (self *SLocalDisk) GetId() string { + return self.DiskId +} + +func (self *SLocalDisk) GetGlobalId() string { + return self.DiskId +} + +func (self *SLocalDisk) IsEmulated() bool { + return false +} + +func (self *SLocalDisk) GetISnapshot(snapshotId string) (cloudprovider.ICloudSnapshot, error) { + return nil, nil +} + +func (self *SLocalDisk) GetISnapshots() ([]cloudprovider.ICloudSnapshot, error) { + return nil, nil +} + +func (self *SLocalDisk) GetIStorge() cloudprovider.ICloudStorage { + return self.storage +} + +func (self *SLocalDisk) Resize(size int64) error { + return cloudprovider.ErrNotSupported +} diff --git a/pkg/util/qcloud/localstorage.go b/pkg/util/qcloud/localstorage.go new file mode 100644 index 0000000000..38d57186c1 --- /dev/null +++ b/pkg/util/qcloud/localstorage.go @@ -0,0 +1,98 @@ +package qcloud + +import ( + "fmt" + "strings" + + "yunion.io/x/jsonutils" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" +) + +type SLocalStorage struct { + zone *SZone + storageType string +} + +func (self *SLocalStorage) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SLocalStorage) GetId() string { + return fmt.Sprintf("%s-%s-%s", self.zone.region.client.providerId, self.zone.GetId(), self.storageType) +} + +func (self *SLocalStorage) GetName() string { + return fmt.Sprintf("%s-%s-%s", self.zone.region.client.providerName, self.zone.GetId(), self.storageType) +} + +func (self *SLocalStorage) GetGlobalId() string { + return fmt.Sprintf("%s-%s-%s", self.zone.region.client.providerId, self.zone.GetGlobalId(), self.storageType) +} + +func (self *SLocalStorage) IsEmulated() bool { + return true +} + +func (self *SLocalStorage) GetIZone() cloudprovider.ICloudZone { + return self.zone +} + +func (self *SLocalStorage) GetIDisks() ([]cloudprovider.ICloudDisk, error) { + disks := []SLocalDisk{} + idisks := make([]cloudprovider.ICloudDisk, len(disks)) + for i := 0; i < len(disks); i++ { + disks[i].storage = self + idisks[i] = &disks[i] + } + return idisks, nil +} + +func (self *SLocalStorage) GetStorageType() string { + return strings.ToLower(self.storageType) +} + +func (self *SLocalStorage) GetMediumType() string { + if strings.HasSuffix(self.storageType, "_BASIC") { + return models.DISK_TYPE_ROTATE + } + return models.DISK_TYPE_SSD +} + +func (self *SLocalStorage) GetCapacityMB() int { + return 0 // unlimited +} + +func (self *SLocalStorage) GetStorageConf() jsonutils.JSONObject { + conf := jsonutils.NewDict() + return conf +} + +func (self *SLocalStorage) GetManagerId() string { + return self.zone.region.client.providerId +} + +func (self *SLocalStorage) GetStatus() string { + return models.STORAGE_ONLINE +} + +func (self *SLocalStorage) Refresh() error { + // do nothing + return nil +} + +func (self *SLocalStorage) GetEnabled() bool { + return true +} + +func (self *SLocalStorage) GetIStoragecache() cloudprovider.ICloudStoragecache { + return self.zone.region.getStoragecache() +} + +func (self *SLocalStorage) CreateIDisk(name string, sizeGb int, desc string) (cloudprovider.ICloudDisk, error) { + return nil, cloudprovider.ErrNotSupported +} + +func (self *SLocalStorage) GetIDisk(idStr string) (cloudprovider.ICloudDisk, error) { + return &SLocalDisk{storage: self, DiskId: idStr}, nil +} diff --git a/pkg/util/qcloud/network.go b/pkg/util/qcloud/network.go new file mode 100644 index 0000000000..fa6ce8a6f2 --- /dev/null +++ b/pkg/util/qcloud/network.go @@ -0,0 +1,132 @@ +package qcloud + +import ( + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/pkg/util/netutils" +) + +type SNetwork struct { + wire *SWire + + CidrBlock string + Zone string + SubnetId string + VpcId string + SubnetName string + AvailableIpAddressCount int + CreatedTime time.Time + EnableBroadcast bool + IsDefault bool + RouteTableId string +} + +func (self *SNetwork) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SNetwork) GetId() string { + return self.SubnetId +} + +func (self *SNetwork) GetName() string { + if len(self.SubnetName) > 0 { + return self.SubnetName + } + return self.SubnetId +} + +func (self *SNetwork) GetGlobalId() string { + return self.SubnetId +} + +func (self *SNetwork) IsEmulated() bool { + return false +} + +func (self *SNetwork) GetStatus() string { + return models.NETWORK_STATUS_AVAILABLE +} + +func (self *SNetwork) Delete() error { + return self.wire.zone.region.DeleteNetwork(self.SubnetId) +} + +func (self *SRegion) DeleteNetwork(networkId string) error { + params := make(map[string]string) + params["SubnetId"] = networkId + + _, err := self.vpcRequest("DeleteSubnet", params) + return err +} + +func (self *SNetwork) GetIWire() cloudprovider.ICloudWire { + return self.wire +} + +func (self *SNetwork) GetAllocTimeoutSeconds() int { + return 120 // 2 minutes +} + +func (self *SNetwork) GetGateway() string { + pref, _ := netutils.NewIPV4Prefix(self.CidrBlock) + endIp := pref.Address.BroadcastAddr(pref.MaskLen) // 255 + endIp = endIp.StepDown() // 254 + return endIp.String() +} + +func (self *SNetwork) GetIpStart() string { + pref, _ := netutils.NewIPV4Prefix(self.CidrBlock) + startIp := pref.Address.NetAddr(pref.MaskLen) // 0 + startIp = startIp.StepUp() // 1 + return startIp.String() +} + +func (self *SNetwork) GetIpEnd() string { + pref, _ := netutils.NewIPV4Prefix(self.CidrBlock) + endIp := pref.Address.BroadcastAddr(pref.MaskLen) // 255 + endIp = endIp.StepDown() // 254 + endIp = endIp.StepDown() // 253 + endIp = endIp.StepDown() // 252 + return endIp.String() +} + +func (self *SNetwork) GetIpMask() int8 { + pref, _ := netutils.NewIPV4Prefix(self.CidrBlock) + return pref.MaskLen +} + +func (self *SNetwork) GetIsPublic() bool { + // return self.IsDefault + return true +} + +func (self *SNetwork) GetServerType() string { + return models.SERVER_TYPE_GUEST +} + +func (self *SNetwork) Refresh() error { + log.Debugf("network refresh %s", self.SubnetId) + new, err := self.wire.zone.region.GetNetwork(self.SubnetId) + if err != nil { + return err + } + return jsonutils.Update(self, new) +} + +func (self *SRegion) CreateNetwork(zoneId string, vpcId string, name string, cidr string, desc string) (string, error) { + params := make(map[string]string) + params["Zone"] = zoneId + params["VpcId"] = vpcId + params["CidrBlock"] = cidr + params["SubnetName"] = name + body, err := self.vpcRequest("CreateSubnet", params) + if err != nil { + return "", err + } + return body.GetString("Subnet", "SubnetId") +} diff --git a/pkg/util/qcloud/provider/doc.go b/pkg/util/qcloud/provider/doc.go new file mode 100644 index 0000000000..4f504f6688 --- /dev/null +++ b/pkg/util/qcloud/provider/doc.go @@ -0,0 +1 @@ +package provider diff --git a/pkg/util/qcloud/provider/provider.go b/pkg/util/qcloud/provider/provider.go new file mode 100644 index 0000000000..206fb909bf --- /dev/null +++ b/pkg/util/qcloud/provider/provider.go @@ -0,0 +1,84 @@ +package provider + +import ( + "yunion.io/x/jsonutils" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/util/qcloud" +) + +type SQcloudProviderFactory struct { + // providerTable map[string]*SQcloudProvider +} + +func (self *SQcloudProviderFactory) GetId() string { + return qcloud.CLOUD_PROVIDER_QCLOUD +} + +func (self *SQcloudProviderFactory) GetProvider(providerId, providerName, url, account, secret string) (cloudprovider.ICloudProvider, error) { + client, err := qcloud.NewQcloudClient(providerId, providerName, account, secret) + if err != nil { + return nil, err + } + return &SQcloudProvider{client: client}, nil +} + +func init() { + factory := SQcloudProviderFactory{} + cloudprovider.RegisterFactory(&factory) +} + +type SQcloudProvider struct { + client *qcloud.SQcloudClient +} + +func (self *SQcloudProvider) IsPublicCloud() bool { + return true +} + +func (self *SQcloudProvider) GetId() string { + return qcloud.CLOUD_PROVIDER_QCLOUD +} + +func (self *SQcloudProvider) GetName() string { + return qcloud.CLOUD_PROVIDER_QCLOUD_CN +} + +func (self *SQcloudProvider) GetSysInfo() (jsonutils.JSONObject, error) { + regions := self.client.GetIRegions() + info := jsonutils.NewDict() + info.Add(jsonutils.NewInt(int64(len(regions))), "region_count") + info.Add(jsonutils.NewString(qcloud.QCLOUD_API_VERSION), "api_version") + return info, nil +} + +func (self *SQcloudProvider) GetSubAccounts() ([]cloudprovider.SSubAccount, error) { + return self.client.GetSubAccounts() +} + +func (self *SQcloudProvider) GetIRegions() []cloudprovider.ICloudRegion { + return self.client.GetIRegions() +} + +func (self *SQcloudProvider) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) { + return self.client.GetIRegionById(id) +} + +func (self *SQcloudProvider) GetIHostById(id string) (cloudprovider.ICloudHost, error) { + return self.client.GetIHostById(id) +} + +func (self *SQcloudProvider) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) { + return self.client.GetIVpcById(id) +} + +func (self *SQcloudProvider) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) { + return self.client.GetIStorageById(id) +} + +func (self *SQcloudProvider) GetIStoragecacheById(id string) (cloudprovider.ICloudStoragecache, error) { + return self.client.GetIStoragecacheById(id) +} + +func (self *SQcloudProvider) GetBalance() (float64, error) { + return 0.0, nil +} diff --git a/pkg/util/qcloud/qcloud.go b/pkg/util/qcloud/qcloud.go new file mode 100644 index 0000000000..898087ef40 --- /dev/null +++ b/pkg/util/qcloud/qcloud.go @@ -0,0 +1,259 @@ +package qcloud + +import ( + "fmt" + "strings" + + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" +) + +const ( + CLOUD_PROVIDER_QCLOUD = models.CLOUD_PROVIDER_QCLOUD + CLOUD_PROVIDER_QCLOUD_CN = "腾讯云" + + QCLOUD_DEFAULT_REGION = "ap-beijing" + + QCLOUD_API_VERSION = "2017-03-12" +) + +type SQcloudClient struct { + providerId string + providerName string + AppID string + SecretID string + SecretKey string + iregions []cloudprovider.ICloudRegion +} + +func NewQcloudClient(providerId string, providerName string, secretID string, secretKey string) (*SQcloudClient, error) { + client := SQcloudClient{providerId: providerId, providerName: providerName, SecretID: secretID, SecretKey: secretKey} + if account := strings.Split(secretID, "/"); len(account) == 2 { + client.SecretID = account[0] + client.AppID = account[1] + } + err := client.fetchRegions() + if err != nil { + return nil, err + } + return &client, nil +} + +func jsonRequest(client *common.Client, apiName string, params map[string]string) (jsonutils.JSONObject, error) { + domain := "cvm.tencentcloudapi.com" + if region, ok := params["Region"]; ok && strings.HasSuffix(region, "-fsi") { + domain = "cvm." + region + ".tencentcloudapi.com" + } + return _jsonRequest(client, domain, QCLOUD_API_VERSION, apiName, params) +} + +func vpcRequest(client *common.Client, apiName string, params map[string]string) (jsonutils.JSONObject, error) { + domain := "vpc.tencentcloudapi.com" + // if region, ok := params["Region"]; ok && strings.HasSuffix(region, "-fsi") { + // domain = "vpc." + region + ".tencentcloudapi.com" + // } + return _jsonRequest(client, domain, QCLOUD_API_VERSION, apiName, params) +} + +func cbsRequest(client *common.Client, apiName string, params map[string]string) (jsonutils.JSONObject, error) { + domain := "cbs.tencentcloudapi.com" + if region, ok := params["Region"]; ok && strings.HasSuffix(region, "-fsi") { + domain = "cbs." + region + ".tencentcloudapi.com" + } + return _jsonRequest(client, domain, QCLOUD_API_VERSION, apiName, params) +} + +type QcloudResponse struct { + *tchttp.BaseResponse + Response *interface{} `json:"Response"` +} + +func _jsonRequest(client *common.Client, domain string, version string, apiName string, params map[string]string) (jsonutils.JSONObject, error) { + req := &tchttp.BaseRequest{} + if region, ok := params["Region"]; ok { + client = client.Init(region) + } + client.WithProfile(profile.NewClientProfile()) + service := strings.Split(domain, ".")[0] + req.Init().WithApiInfo(service, version, apiName) + req.SetDomain(domain) + + for k, v := range params { + req.GetParams()[k] = v + } + resp := &QcloudResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + err := client.Send(req, resp) + if err != nil { + log.Errorf("request url: %s\nparams: %s\nerror: %v", req.GetDomain(), jsonutils.Marshal(req.GetParams()).PrettyString(), err) + return nil, err + } + return jsonutils.Marshal(resp.Response), nil +} + +func (client *SQcloudClient) GetRegions() []SRegion { + regions := make([]SRegion, len(client.iregions)) + for i := 0; i < len(regions); i++ { + region := client.iregions[i].(*SRegion) + regions[i] = *region + } + return regions +} + +func (client *SQcloudClient) getDefaultClient() (*common.Client, error) { + return common.NewClientWithSecretId(client.SecretID, client.SecretKey, QCLOUD_DEFAULT_REGION) +} + +func (client *SQcloudClient) vpcRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) { + cli, err := client.getDefaultClient() + if err != nil { + return nil, err + } + return vpcRequest(cli, apiName, params) +} + +func (client *SQcloudClient) cbsRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) { + cli, err := client.getDefaultClient() + if err != nil { + return nil, err + } + return cbsRequest(cli, apiName, params) +} + +func (client *SQcloudClient) jsonRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) { + cli, err := client.getDefaultClient() + if err != nil { + return nil, err + } + return jsonRequest(cli, apiName, params) +} + +func (client *SQcloudClient) fetchRegions() error { + body, err := client.jsonRequest("DescribeRegions", nil) + if err != nil { + log.Errorf("fetchRegions fail %s", err) + return err + } + + regions := make([]SRegion, 0) + err = body.Unmarshal(®ions, "RegionSet") + if err != nil { + log.Errorf("unmarshal json error %s", err) + return err + } + client.iregions = make([]cloudprovider.ICloudRegion, len(regions)) + for i := 0; i < len(regions); i++ { + regions[i].client = client + client.iregions[i] = ®ions[i] + } + return nil +} + +func (client *SQcloudClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) { + err := client.fetchRegions() + if err != nil { + return nil, err + } + subAccount := cloudprovider.SSubAccount{} + subAccount.Name = client.providerName + subAccount.Account = client.SecretID + if len(client.AppID) > 0 { + subAccount.Account = fmt.Sprintf("%s/%s", client.SecretID, client.AppID) + } + return []cloudprovider.SSubAccount{subAccount}, nil +} + +func (client *SQcloudClient) GetIRegions() []cloudprovider.ICloudRegion { + return client.iregions +} + +func (client *SQcloudClient) getDefaultRegion() (cloudprovider.ICloudRegion, error) { + if len(client.iregions) > 0 { + return client.iregions[0], nil + } + return nil, cloudprovider.ErrNotFound +} + +func (client *SQcloudClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) { + for i := 0; i < len(client.iregions); i++ { + if client.iregions[i].GetGlobalId() == id { + return client.iregions[i], nil + } + } + return nil, cloudprovider.ErrNotFound +} + +func (client *SQcloudClient) GetRegion(regionId string) *SRegion { + for i := 0; i < len(client.iregions); i++ { + if client.iregions[i].GetId() == regionId { + return client.iregions[i].(*SRegion) + } + } + return nil +} + +func (client *SQcloudClient) GetIHostById(id string) (cloudprovider.ICloudHost, error) { + for i := 0; i < len(client.iregions); i++ { + ihost, err := client.iregions[i].GetIHostById(id) + if err == nil { + return ihost, nil + } else if err != cloudprovider.ErrNotFound { + return nil, err + } + } + return nil, cloudprovider.ErrNotFound +} + +func (client *SQcloudClient) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) { + for i := 0; i < len(client.iregions); i++ { + ihost, err := client.iregions[i].GetIVpcById(id) + if err == nil { + return ihost, nil + } else if err != cloudprovider.ErrNotFound { + return nil, err + } + } + return nil, cloudprovider.ErrNotFound +} + +func (client *SQcloudClient) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) { + for i := 0; i < len(client.iregions); i++ { + ihost, err := client.iregions[i].GetIStorageById(id) + if err == nil { + return ihost, nil + } else if err != cloudprovider.ErrNotFound { + return nil, err + } + } + return nil, cloudprovider.ErrNotFound +} + +func (client *SQcloudClient) GetIStoragecacheById(id string) (cloudprovider.ICloudStoragecache, error) { + for i := 0; i < len(client.iregions); i++ { + ihost, err := client.iregions[i].GetIStoragecacheById(id) + if err == nil { + return ihost, nil + } else if err != cloudprovider.ErrNotFound { + return nil, err + } + } + return nil, cloudprovider.ErrNotFound +} + +type SAccountBalance struct { + AvailableAmount float64 + AvailableCashAmount float64 + CreditAmount float64 + MybankCreditAmount float64 + Currency string +} + +func (client *SQcloudClient) QueryAccountBalance() (*SAccountBalance, error) { + return nil, nil +} diff --git a/pkg/util/qcloud/region.go b/pkg/util/qcloud/region.go new file mode 100644 index 0000000000..2c74f880b5 --- /dev/null +++ b/pkg/util/qcloud/region.go @@ -0,0 +1,538 @@ +package qcloud + +import ( + "fmt" + + "github.com/nelsonken/cos-go-sdk-v5/cos" + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/pkg/utils" +) + +type SRegion struct { + client *SQcloudClient + cosClient *cos.Client + + izones []cloudprovider.ICloudZone + ivpcs []cloudprovider.ICloudVpc + + storageCache *SStoragecache + + instanceTypes []SInstanceType + + Region string + RegionName string + RegionState string + + Latitude float64 + Longitude float64 + fetchLocation bool +} + +func (self *SRegion) GetId() string { + return self.Region +} + +func (self *SRegion) GetName() string { + return fmt.Sprintf("%s %s", CLOUD_PROVIDER_QCLOUD_CN, self.RegionName) +} + +func (self *SRegion) GetGlobalId() string { + return fmt.Sprintf("%s/%s", CLOUD_PROVIDER_QCLOUD, self.Region) +} + +func (self *SRegion) IsEmulated() bool { + return false +} + +func (self *SRegion) GetProvider() string { + return CLOUD_PROVIDER_QCLOUD +} + +func (self *SRegion) CreateIVpc(name string, desc string, cidr string) (cloudprovider.ICloudVpc, error) { + params := make(map[string]string) + if len(cidr) > 0 { + params["CidrBlock"] = cidr + } + if len(name) > 0 { + params["VpcName"] = name + } + body, err := self.vpcRequest("CreateVpc", params) + if err != nil { + return nil, err + } + vpcId, err := body.GetString("Vpc", "VpcId") + if err != nil { + return nil, err + } + err = self.fetchInfrastructure() + if err != nil { + return nil, err + } + return self.GetIVpcById(vpcId) +} + +func (self *SRegion) GetCosClient() (*cos.Client, error) { + if self.cosClient == nil { + self.cosClient = cos.New(&cos.Option{ + AppID: self.client.AppID, + SecretID: self.client.SecretID, + SecretKey: self.client.SecretKey, + Region: self.Region, + }) + } + return self.cosClient, nil +} + +func (self *SRegion) GetClient() *SQcloudClient { + return self.client +} + +func (self *SRegion) GetIEipById(eipId string) (cloudprovider.ICloudEIP, error) { + eips, total, err := self.GetEips(eipId, 0, 1) + if err != nil { + return nil, err + } + if total == 0 { + return nil, cloudprovider.ErrNotFound + } + if total > 1 { + return nil, cloudprovider.ErrDuplicateId + } + return &eips[0], nil +} + +func (self *SRegion) GetIEips() ([]cloudprovider.ICloudEIP, error) { + eips, total, err := self.GetEips("", 0, 50) + if err != nil { + return nil, err + } + for len(eips) < total { + var parts []SEipAddress + parts, total, err = self.GetEips("", len(eips), 50) + if err != nil { + return nil, err + } + eips = append(eips, parts...) + } + ret := make([]cloudprovider.ICloudEIP, len(eips)) + for i := 0; i < len(eips); i++ { + ret[i] = &eips[i] + } + return ret, nil +} + +func (self *SRegion) GetIHostById(id string) (cloudprovider.ICloudHost, error) { + izones, err := self.GetIZones() + if err != nil { + return nil, err + } + for i := 0; i < len(izones); i += 1 { + ihost, err := izones[i].GetIHostById(id) + if err == nil { + return ihost, nil + } else if err != cloudprovider.ErrNotFound { + return nil, err + } + } + return nil, cloudprovider.ErrNotFound +} + +func (self *SRegion) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) { + izones, err := self.GetIZones() + if err != nil { + return nil, err + } + for i := 0; i < len(izones); i += 1 { + istore, err := izones[i].GetIStorageById(id) + if err == nil { + return istore, nil + } else if err != cloudprovider.ErrNotFound { + return nil, err + } + } + return nil, cloudprovider.ErrNotFound +} + +func (self *SRegion) GetIStoragecacheById(id string) (cloudprovider.ICloudStoragecache, error) { + storageCache := self.getStoragecache() + if storageCache.GetGlobalId() == id { + return self.storageCache, nil + } + return nil, cloudprovider.ErrNotFound +} + +func (self *SRegion) updateInstance(instId string, name, desc, passwd, hostname string) error { + params := make(map[string]string) + params["InstanceId"] = instId + if len(name) > 0 { + params["InstanceName"] = name + } + if len(desc) > 0 { + params["Description"] = desc + } + if len(passwd) > 0 { + params["Password"] = passwd + } + if len(hostname) > 0 { + params["HostName"] = hostname + } + _, err := self.cvmRequest("ModifyInstanceAttribute", params) + return err +} + +func (self *SRegion) UpdateInstancePassword(instId string, passwd string) error { + return self.updateInstance(instId, "", "", passwd, "") +} + +func (self *SRegion) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) { + ivpcs, err := self.GetIVpcs() + if err != nil { + return nil, err + } + for i := 0; i < len(ivpcs); i++ { + if ivpcs[i].GetGlobalId() == id { + return ivpcs[i], nil + } + } + return nil, cloudprovider.ErrNotFound +} + +func (self *SRegion) getZoneById(id string) (*SZone, error) { + izones, err := self.GetIZones() + if err != nil { + return nil, err + } + for i := 0; i < len(izones); i += 1 { + zone := izones[i].(*SZone) + if zone.Zone == id { + return zone, nil + } + } + return nil, fmt.Errorf("no such zone %s", id) +} + +func (self *SRegion) GetIVpcs() ([]cloudprovider.ICloudVpc, error) { + if self.ivpcs == nil { + err := self.fetchInfrastructure() + if err != nil { + return nil, err + } + } + return self.ivpcs, nil +} + +func (self *SRegion) GetIZoneById(id string) (cloudprovider.ICloudZone, error) { + izones, err := self.GetIZones() + if err != nil { + return nil, err + } + for i := 0; i < len(izones); i += 1 { + if izones[i].GetGlobalId() == id { + return izones[i], nil + } + } + return nil, cloudprovider.ErrNotFound +} + +func (self *SRegion) GetIZones() ([]cloudprovider.ICloudZone, error) { + if self.izones == nil { + var err error + err = self.fetchInfrastructure() + if err != nil { + return nil, err + } + } + return self.izones, nil +} + +func (self *SRegion) _fetchZones() error { + params := make(map[string]string) + zones := make([]SZone, 0) + body, err := self.cvmRequest("DescribeZones", params) + if err != nil { + return err + } + err = body.Unmarshal(&zones, "ZoneSet") + if err != nil { + return err + } + self.izones = make([]cloudprovider.ICloudZone, len(zones)) + for i := 0; i < len(zones); i++ { + zones[i].region = self + self.izones[i] = &zones[i] + } + return nil +} + +func (self *SRegion) fetchInfrastructure() error { + err := self._fetchZones() + if err != nil { + return err + } + err = self.fetchIVpcs() + if err != nil { + return err + } + for i := 0; i < len(self.ivpcs); i += 1 { + for j := 0; j < len(self.izones); j += 1 { + zone := self.izones[j].(*SZone) + vpc := self.ivpcs[i].(*SVpc) + wire := SWire{zone: zone, vpc: vpc} + zone.addWire(&wire) + vpc.addWire(&wire) + } + } + return nil +} + +func (self *SRegion) DeleteVpc(vpcId string) error { + params := make(map[string]string) + params["VpcId"] = vpcId + + _, err := self.vpcRequest("DeleteVpc", params) + return err +} + +func (self *SRegion) getVpc(vpcId string) (*SVpc, error) { + vpcs, total, err := self.GetVpcs([]string{vpcId}, 0, 1) + if err != nil { + return nil, err + } + if total > 1 { + return nil, cloudprovider.ErrDuplicateId + } + if total == 0 { + return nil, cloudprovider.ErrNotFound + } + vpcs[0].region = self + return &vpcs[0], nil +} + +func (self *SRegion) fetchIVpcs() error { + vpcs := make([]SVpc, 0) + for { + part, total, err := self.GetVpcs(nil, len(vpcs), 50) + if err != nil { + return err + } + vpcs = append(vpcs, part...) + if len(vpcs) >= total { + break + } + } + self.ivpcs = make([]cloudprovider.ICloudVpc, len(vpcs)) + for i := 0; i < len(vpcs); i += 1 { + vpcs[i].region = self + self.ivpcs[i] = &vpcs[i] + } + return nil +} + +func (self *SRegion) GetVpcs(vpcIds []string, offset int, limit int) ([]SVpc, int, error) { + if limit > 50 || limit <= 0 { + limit = 50 + } + params := make(map[string]string) + params["Limit"] = fmt.Sprintf("%d", limit) + params["Offset"] = fmt.Sprintf("%d", offset) + if vpcIds != nil && len(vpcIds) > 0 { + for index, vpcId := range vpcIds { + params[fmt.Sprintf("VpcIds.%d", index)] = vpcId + } + } + body, err := self.vpcRequest("DescribeVpcs", params) + if err != nil { + return nil, 0, err + } + vpcs := make([]SVpc, 0) + err = body.Unmarshal(&vpcs, "VpcSet") + if err != nil { + log.Errorf("Unmarshal vpc fail %s", err) + return nil, 0, err + } + total, _ := body.Float("TotalCount") + return vpcs, int(total), nil +} + +func (self *SRegion) GetLatitude() float32 { + if info, ok := LatitudeAndLongitude[self.Region]; ok { + return info["latitude"] + } + return 0.0 +} + +func (self *SRegion) GetLongitude() float32 { + if info, ok := LatitudeAndLongitude[self.Region]; ok { + return info["longitude"] + } + return 0.0 +} + +func (self *SRegion) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SRegion) GetStatus() string { + if self.RegionState == "AVAILABLE" { + return models.CLOUD_REGION_STATUS_INSERVER + } + return models.CLOUD_REGION_STATUS_OUTOFSERVICE +} + +func (self *SRegion) Refresh() error { + // do nothing + return nil +} + +func (self *SRegion) vpcRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) { + params["Region"] = self.Region + return self.client.vpcRequest(apiName, params) +} + +func (self *SRegion) cvmRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) { + params["Region"] = self.Region + return self.client.jsonRequest(apiName, params) +} + +func (self *SRegion) cbsRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) { + params["Region"] = self.Region + return self.client.cbsRequest(apiName, params) +} + +func (self *SRegion) GetNetworks(ids []string, vpcId string, offset int, limit int) ([]SNetwork, int, error) { + if limit > 50 || limit <= 0 { + limit = 50 + } + params := make(map[string]string) + params["Limit"] = fmt.Sprintf("%d", limit) + params["Offset"] = fmt.Sprintf("%d", offset) + base := 0 + if ids != nil && len(ids) > 0 { + for index, networkId := range ids { + params[fmt.Sprintf("SubnetIds.%d", index)] = networkId + } + base += len(ids) + } + if len(vpcId) > 0 { + params["Filters.0.Name"] = "vpc-id" + params["Filters.0.Values.0"] = vpcId + } + + body, err := self.vpcRequest("DescribeSubnets", params) + if err != nil { + log.Errorf("DescribeSubnets fail %s", err) + return nil, 0, err + } + + networks := make([]SNetwork, 0) + err = body.Unmarshal(&networks, "SubnetSet") + if err != nil { + log.Errorf("Unmarshal network fail %s", err) + return nil, 0, err + } + total, _ := body.Float("TotalCount") + return networks, int(total), nil +} + +func (self *SRegion) GetNetwork(networkId string) (*SNetwork, error) { + networks, total, err := self.GetNetworks([]string{networkId}, "", 0, 1) + if err != nil { + return nil, err + } + if total > 1 { + return nil, cloudprovider.ErrDuplicateId + } + if total == 0 { + return nil, cloudprovider.ErrNotFound + } + return &networks[0], nil +} + +func (self *SRegion) getStoragecache() *SStoragecache { + if self.storageCache == nil { + self.storageCache = &SStoragecache{region: self} + } + return self.storageCache +} + +func (self *SRegion) GetMatchInstanceTypes(cpu int, memMB int, gpu int, zoneId string) ([]SInstanceType, error) { + if self.instanceTypes == nil { + types, err := self.GetInstanceTypes() + if err != nil { + log.Errorf("GetInstanceTypes %s", err) + return nil, err + } + self.instanceTypes = types + } + + var available []string + if len(zoneId) > 0 { + zone, err := self.getZoneById(zoneId) + if err != nil { + return nil, err + } + available = zone.getAvaliableInstanceTypes() + } + ret := make([]SInstanceType, 0) + for _, t := range self.instanceTypes { + if t.CPU == cpu && memMB == t.memoryMB() && gpu == t.GPU { + if available == nil || utils.IsInStringArray(t.InstanceType, available) { + ret = append(ret, t) + } + } + } + return ret, nil +} + +func (self *SRegion) CreateInstanceSimple(name string, imgId string, cpu int, memGB int, storageType string, dataDiskSizesGB []int, networkId string, passwd string, publicKey string) (*SInstance, error) { + izones, err := self.GetIZones() + if err != nil { + return nil, err + } + for i := 0; i < len(izones); i += 1 { + z := izones[i].(*SZone) + log.Debugf("Search in zone %s", z.Zone) + net := z.getNetworkById(networkId) + if net != nil { + inst, err := z.getHost().CreateVM(name, imgId, 0, cpu, memGB*1024, networkId, "", "", passwd, storageType, dataDiskSizesGB, publicKey, "", "") + if err != nil { + return nil, err + } + return inst.(*SInstance), nil + } + } + return nil, fmt.Errorf("cannot find network %s", networkId) +} + +func (self *SRegion) instanceOperation(instanceId string, opname string, extra map[string]string) error { + params := make(map[string]string) + params["InstanceIds.0"] = instanceId + if extra != nil && len(extra) > 0 { + for k, v := range extra { + params[k] = v + } + } + _, err := self.cvmRequest(opname, params) + return err +} + +func (self *SRegion) GetInstanceVNCUrl(instanceId string) (string, error) { + params := make(map[string]string) + params["InstanceId"] = instanceId + body, err := self.cvmRequest("DescribeInstanceVncUrl", params) + if err != nil { + return "", err + } + return body.GetString("InstanceVncUrl") +} + +func (self *SRegion) GetInstanceStatus(instanceId string) (string, error) { + instance, err := self.GetInstance(instanceId) + if err != nil { + return "", err + } + return instance.InstanceState, nil +} diff --git a/pkg/util/qcloud/securitygroup.go b/pkg/util/qcloud/securitygroup.go new file mode 100644 index 0000000000..ad1d1d35eb --- /dev/null +++ b/pkg/util/qcloud/securitygroup.go @@ -0,0 +1,415 @@ +package qcloud + +import ( + "fmt" + "net" + "sort" + "strconv" + "strings" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/util/secrules" + "yunion.io/x/pkg/utils" +) + +type SecurityGroupPolicy struct { + vpc *SVpc + PolicyIndex int // 安全组规则索引号。 + Protocol string // 协议, 取值: TCP,UDP, ICMP。 + Port string // 端口(all, 离散port, range)。 + ServiceTemplate ServiceTemplateSpecification // 协议端口ID或者协议端口组ID。ServiceTemplate和Protocol+Port互斥。 + CidrBlock string // 网段或IP(互斥)。 + SecurityGroupId string // 已绑定安全组的网段或IP。 + AddressTemplate AddressTemplateSpecification // IP地址ID或者ID地址组ID。 + Action string // ACCEPT 或 DROP。 + PolicyDescription string // 安全组规则描述。 + direction string +} + +type ServiceTemplateSpecification struct { + ServiceId string // 协议端口ID,例如:ppm-f5n1f8da。 + ServiceGroupId string // 协议端口组ID,例如:ppmg-f5n1f8da。 +} + +type AddressTemplateSpecification struct { + AddressId string // IP地址ID,例如:ipm-2uw6ujo6。 + AddressGroupId string // IP地址组ID,例如:ipmg-2uw6ujo6。 +} + +type SecurityGroupPolicySet struct { + Version string + Egress []SecurityGroupPolicy // 出站规则。 + Ingress []SecurityGroupPolicy // 入站规则。 +} + +type SSecurityGroup struct { + vpc *SVpc + SecurityGroupId string // 安全组实例ID,例如:sg-ohuuioma。 + SecurityGroupName string // 安全组名称,可任意命名,但不得超过60个字符。 + SecurityGroupDesc string // 安全组备注,最多100个字符。 + ProjectId string // 项目id,默认0。可在qcloud控制台项目管理页面查询到。 + IsDefault bool // 是否是默认安全组,默认安全组不支持删除。 + CreatedTime time.Time // 安全组创建时间。 + SecurityGroupPolicySet SecurityGroupPolicySet +} + +type SecurityGroupRuleSet []SecurityGroupPolicy + +func (v SecurityGroupRuleSet) Len() int { + return len(v) +} + +func (v SecurityGroupRuleSet) Swap(i, j int) { + v[i], v[j] = v[j], v[i] +} + +func (v SecurityGroupRuleSet) Less(i, j int) bool { + if v[i].PolicyIndex < v[j].PolicyIndex { + return true + } else if v[i].PolicyIndex == v[j].PolicyIndex { + return strings.Compare(v[i].String(), v[j].String()) <= 0 + } + return false +} + +func (self *SRegion) GetSecurityGroups(vpcId string, offset int, limit int) ([]SSecurityGroup, int, error) { + if limit > 50 || limit <= 0 { + limit = 50 + } + params := make(map[string]string) + params["Limit"] = fmt.Sprintf("%d", limit) + params["Offset"] = fmt.Sprintf("%d", offset) + + body, err := self.vpcRequest("DescribeSecurityGroups", params) + if err != nil { + log.Errorf("GetSecurityGroups fail %s", err) + return nil, 0, err + } + + secgrps := make([]SSecurityGroup, 0) + err = body.Unmarshal(&secgrps, "SecurityGroupSet") + if err != nil { + log.Errorf("Unmarshal security groups fail %s", err) + return nil, 0, err + } + total, _ := body.Float("TotalCount") + return secgrps, int(total), nil +} + +func (self *SSecurityGroup) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SSecurityGroup) GetId() string { + return self.SecurityGroupId +} + +func (self *SSecurityGroup) GetGlobalId() string { + return self.SecurityGroupId +} + +func (self *SSecurityGroup) GetDescription() string { + return self.SecurityGroupDesc +} + +func (self *SSecurityGroup) GetName() string { + if len(self.SecurityGroupName) > 0 { + return self.SecurityGroupName + } + return self.SecurityGroupId +} + +func (self *SecurityGroupPolicy) String() string { + rules := self.toRules() + result := []string{} + for _, rule := range rules { + result = append(result, rule.String()) + } + return strings.Join(result, ";") +} + +func parseCIDR(cidr string) (*net.IPNet, error) { + if strings.Index(cidr, "/") > 0 { + _, ipnet, err := net.ParseCIDR(cidr) + return ipnet, err + } + ip := net.ParseIP(cidr) + if ip == nil { + return nil, fmt.Errorf("Parse ip %s error", cidr) + } + return &net.IPNet{IP: ip, Mask: net.CIDRMask(32, 32)}, nil +} + +func (self *SecurityGroupPolicy) toRules() []secrules.SecurityRule { + result := []secrules.SecurityRule{} + rule := secrules.SecurityRule{ + Action: secrules.SecurityRuleAllow, + Protocol: secrules.PROTO_ANY, + Direction: secrules.TSecurityRuleDirection(self.direction), + Ports: []int{}, + PortStart: -1, + PortEnd: -1, + } + if len(self.SecurityGroupId) != 0 { + //安全组关联安全组的规则忽略 + return nil + } + if strings.ToLower(self.Action) == "drop" { + rule.Action = secrules.SecurityRuleDeny + } + if utils.IsInStringArray(strings.ToLower(self.Protocol), []string{"tcp", "udp", "icmp"}) { + rule.Protocol = strings.ToLower(self.Protocol) + } + if strings.Index(self.Port, ",") > 0 { + for _, _port := range strings.Split(self.Port, ",") { + port, err := strconv.Atoi(_port) + if err != nil { + log.Errorf("parse secgroup port %s %s error %v", self.Port, _port, err) + continue + } + rule.Ports = append(rule.Ports, port) + } + } else if strings.Index(self.Port, "-") > 0 { + ports := strings.Split(self.Port, "-") + if len(ports) == 2 { + portStart, err := strconv.Atoi(ports[0]) + if err != nil { + return nil + } + portEnd, err := strconv.Atoi(ports[1]) + if err != nil { + return nil + } + rule.PortStart, rule.PortEnd = portStart, portEnd + } + } else if strings.ToLower(self.Port) != "all" { + port, err := strconv.Atoi(self.Port) + if err != nil { + return nil + } + rule.PortStart, rule.PortEnd = port, port + } + + if len(self.AddressTemplate.AddressGroupId) > 0 { + addressGroup, total, err := self.vpc.region.AddressGroupList(self.AddressTemplate.AddressGroupId, "", 0, 1) + if err != nil { + log.Errorf("Get AddressList %s failed %v", self.AddressTemplate.AddressId, err) + return nil + } + if total != 1 { + return nil + } + for i := 0; i < len(addressGroup[0].AddressTemplateIdSet); i++ { + rules, err := self.getAddressRules(rule, addressGroup[0].AddressTemplateIdSet[i]) + if err != nil { + return nil + } + result = append(result, rules...) + } + } else if len(self.AddressTemplate.AddressId) > 0 { + rules, err := self.getAddressRules(rule, self.AddressTemplate.AddressId) + if err != nil { + return nil + } + result = append(result, rules...) + } else if len(self.CidrBlock) > 0 { + ipnet, err := parseCIDR(self.CidrBlock) + if err != nil { + return nil + } + rule.IPNet = ipnet + result = append(result, rule) + } + return result +} + +func (self *SecurityGroupPolicy) getAddressRules(rule secrules.SecurityRule, addressId string) ([]secrules.SecurityRule, error) { + result := []secrules.SecurityRule{} + address, total, err := self.vpc.region.AddressList(addressId, "", 0, 1) + if err != nil { + log.Errorf("Get AddressList %s failed %v", self.AddressTemplate.AddressId, err) + return nil, err + } + if total != 1 { + return nil, fmt.Errorf("failed to find address %s", addressId) + } + for _, ip := range address[0].AddressSet { + ipnet, err := parseCIDR(ip) + if err != nil { + return nil, nil + } + rule.IPNet = ipnet + result = append(result, rule) + } + return result, nil +} + +func (self *SSecurityGroup) GetRules() ([]secrules.SecurityRule, error) { + secgroup, err := self.vpc.region.GetSecurityGroupDetails(self.SecurityGroupId) + if err != nil { + return nil, err + } + for i := 0; i < len(secgroup.SecurityGroupPolicySet.Egress); i++ { + secgroup.SecurityGroupPolicySet.Egress[i].direction = "out" + } + for i := 0; i < len(secgroup.SecurityGroupPolicySet.Ingress); i++ { + secgroup.SecurityGroupPolicySet.Ingress[i].direction = "in" + } + originRules := []SecurityGroupPolicy{} + originRules = append(secgroup.SecurityGroupPolicySet.Egress) + originRules = append(secgroup.SecurityGroupPolicySet.Ingress) + sort.Sort(SecurityGroupRuleSet(originRules)) + rules := []secrules.SecurityRule{} + priority := 100 + for _, rule := range originRules { + rule.vpc = self.vpc + subRules := rule.toRules() + for i := 0; i < len(subRules); i++ { + subRules[i].Priority = priority + } + if len(subRules) > 0 { + priority-- + } + rules = append(rules, subRules...) + } + return rules, nil +} + +func (self *SSecurityGroup) GetStatus() string { + return "" +} + +func (self *SSecurityGroup) IsEmulated() bool { + return false +} + +func (self *SSecurityGroup) Refresh() error { + if new, err := self.vpc.region.GetSecurityGroupDetails(self.SecurityGroupId); err != nil { + return err + } else { + return jsonutils.Update(self, new) + } +} + +func (self *SRegion) GetSecurityGroupDetails(secGroupId string) (*SSecurityGroup, error) { + params := make(map[string]string) + params["Region"] = self.Region + params["SecurityGroupId"] = secGroupId + + body, err := self.vpcRequest("DescribeSecurityGroupPolicies", params) + if err != nil { + log.Errorf("DescribeSecurityGroupAttribute fail %s", err) + return nil, err + } + + secgrp := SSecurityGroup{SecurityGroupId: secGroupId} + err = body.Unmarshal(&secgrp.SecurityGroupPolicySet, "SecurityGroupPolicySet") + if err != nil { + log.Errorf("Unmarshal security group details fail %s", err) + return nil, err + } + return &secgrp, nil +} + +func (self *SRegion) DeleteSecurityGroup(secGroupId string) error { + params := make(map[string]string) + params["Region"] = self.Region + params["SecurityGroupId"] = secGroupId + _, err := self.vpcRequest("DeleteSecurityGroup", params) + return err +} + +type AddressTemplate struct { + AddressSet []string + AddressTemplateId string + AddressTemplateName string + CreatedTime time.Time +} + +func (self *SRegion) AddressList(addressId, addressName string, offset, limit int) ([]AddressTemplate, int, error) { + params := map[string]string{} + filter := 0 + if len(addressId) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "address-template-id" + params[fmt.Sprintf("Filters.%d.Values.0", filter)] = addressId + filter++ + } + if len(addressName) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "address-template-name" + params[fmt.Sprintf("Filters.%d.Values.0", filter)] = addressName + filter++ + } + params["Offset"] = fmt.Sprintf("%d", offset) + if limit == 0 { + limit = 20 + } + params["Limit"] = fmt.Sprintf("%d", limit) + body, err := self.vpcRequest("DescribeAddressTemplates", params) + if err != nil { + return nil, 0, err + } + addressTemplates := []AddressTemplate{} + err = body.Unmarshal(&addressTemplates, "AddressTemplateSet") + if err != nil { + return nil, 0, err + } + total, _ := body.Float("TotalCount") + return addressTemplates, int(total), nil +} + +type AddressTemplateGroup struct { + AddressTemplateIdSet []string + AddressTemplateGroupName string + AddressTemplateGroupId string + CreatedTime time.Time +} + +func (self *SRegion) AddressGroupList(groupId, groupName string, offset, limit int) ([]AddressTemplateGroup, int, error) { + params := map[string]string{} + filter := 0 + if len(groupId) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "address-template-group-id" + params[fmt.Sprintf("Filters.%d.Values.0", filter)] = groupId + filter++ + } + if len(groupName) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "address-template-group-name" + params[fmt.Sprintf("Filters.%d.Values.0", filter)] = groupName + filter++ + } + params["Offset"] = fmt.Sprintf("%d", offset) + if limit == 0 { + limit = 20 + } + params["Limit"] = fmt.Sprintf("%d", limit) + body, err := self.vpcRequest("DescribeAddressTemplateGroups", params) + if err != nil { + return nil, 0, err + } + addressTemplateGroups := []AddressTemplateGroup{} + err = body.Unmarshal(&addressTemplateGroups, "AddressTemplateGroupSet") + if err != nil { + return nil, 0, err + } + total, _ := body.Float("TotalCount") + return addressTemplateGroups, int(total), nil +} + +func (self *SRegion) CreateSecurityGroup(name, description string) (*SSecurityGroup, error) { + params := make(map[string]string) + params["Region"] = self.Region + params["GroupName"] = name + params["GroupDescription"] = description + if len(description) == 0 { + params["GroupDescription"] = "Customize Create" + } + secgroup := SSecurityGroup{} + if body, err := self.vpcRequest("CreateSecurityGroup", params); err != nil { + return nil, err + } else if err := body.Unmarshal(&secgroup, "SecurityGroup"); err != nil { + return nil, err + } + return &secgroup, nil +} diff --git a/pkg/util/qcloud/shell/cos.go b/pkg/util/qcloud/shell/cos.go new file mode 100644 index 0000000000..374244b2e0 --- /dev/null +++ b/pkg/util/qcloud/shell/cos.go @@ -0,0 +1,106 @@ +package shell + +import ( + "context" + "os" + + coslib "github.com/nelsonken/cos-go-sdk-v5/cos" + "yunion.io/x/onecloud/pkg/util/qcloud" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type CosListOptions struct { + } + shellutils.R(&CosListOptions{}, "cos-list", "List COS buckets", func(cli *qcloud.SRegion, args *CosListOptions) error { + cos, err := cli.GetCosClient() + if err != nil { + return err + } + result, err := cos.GetBucketList(context.Background()) + if err != nil { + return err + } + printList(result.Buckets.Bucket, len(result.Buckets.Bucket), 0, len(result.Buckets.Bucket), nil) + return nil + }) + + type CosListBucketOptions struct { + BUCKET string `help:"bucket name"` + } + + shellutils.R(&CosListBucketOptions{}, "cos-bucket-list", "List content of a OSS bucket", func(cli *qcloud.SRegion, args *CosListBucketOptions) error { + cos, err := cli.GetCosClient() + if err != nil { + return err + } + result, err := cos.ListBucketContents(context.Background(), args.BUCKET, &coslib.QueryCondition{}) + if err != nil { + return err + } + printList(result.Contents, len(result.Contents), 0, len(result.Contents), nil) + return nil + }) + + shellutils.R(&CosListBucketOptions{}, "cos-bucket-create", "Create a OSS bucket", func(cli *qcloud.SRegion, args *CosListBucketOptions) error { + cos, err := cli.GetCosClient() + if err != nil { + return err + } + return cos.CreateBucket(context.Background(), args.BUCKET, &coslib.AccessControl{}) + }) + + type CosUploadOptions struct { + BUCKET string `help:"bucket name"` + KEY string `help:"Object key"` + FILE string `help:"Local file path"` + Acl string `help:"Object ACL" choices:"private|public-read|public-read-write"` + } + shellutils.R(&CosUploadOptions{}, "cos-upload", "Upload a file to a Cos bucket", func(cli *qcloud.SRegion, args *CosUploadOptions) error { + cos, err := cli.GetCosClient() + if err != nil { + return err + } + return cos.Bucket(args.BUCKET).UploadObjectBySlice(context.Background(), args.KEY, args.FILE, 3, nil) + }) + + type CosDownloadOptions struct { + BUCKET string `help:"bucket name"` + NAME string `help:"File name"` + } + shellutils.R(&CosDownloadOptions{}, "cos-download", "Download a file", func(cli *qcloud.SRegion, args *CosDownloadOptions) error { + cos, err := cli.GetCosClient() + if err != nil { + return err + } + //file + return cos.Bucket(args.BUCKET).DownloadObject(context.Background(), args.NAME, os.Stdout) + //return cos.Bucket(args.BUCKET).UploadObjectBySlice(context.Background(), args.KEY, args.FILE, 3, nil) + }) + + type CosObjectAclOptions struct { + BUCKET string `help:"bucket name"` + KEY string `help:"object key"` + ACL string `help:"ACL" choices:"private|public-read|public-read-write"` + } + shellutils.R(&CosObjectAclOptions{}, "cos-set-acl", "Set acl for a object", func(cli *qcloud.SRegion, args *CosObjectAclOptions) error { + cos, err := cli.GetCosClient() + if err != nil { + return err + } + return cos.SetBucketACL(context.Background(), args.KEY, &coslib.AccessControl{ACL: args.ACL}) + }) + + type CosDeleteOptions struct { + BUCKET string `help:"bucket name"` + KEY string `help:"Object key"` + } + + shellutils.R(&CosDeleteOptions{}, "cos-delete", "Delete a file from a Cos bucket", func(cli *qcloud.SRegion, args *CosDeleteOptions) error { + cos, err := cli.GetCosClient() + if err != nil { + return err + } + return cos.Bucket(args.BUCKET).DeleteObject(context.Background(), args.KEY) + }) +} diff --git a/pkg/util/qcloud/shell/disk.go b/pkg/util/qcloud/shell/disk.go new file mode 100644 index 0000000000..70f8e2dacb --- /dev/null +++ b/pkg/util/qcloud/shell/disk.go @@ -0,0 +1,77 @@ +package shell + +import ( + "fmt" + + "yunion.io/x/onecloud/pkg/util/qcloud" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type DiskListOptions struct { + Instance string `help:"Instance ID"` + Zone string `help:"Zone ID"` + Category string `help:"Disk category"` + Offset int `help:"List offset"` + Limit int `help:"List limit"` + } + shellutils.R(&DiskListOptions{}, "disk-list", "List disks", func(cli *qcloud.SRegion, args *DiskListOptions) error { + disks, total, e := cli.GetDisks(args.Instance, args.Zone, args.Category, nil, args.Offset, args.Limit) + if e != nil { + return e + } + printList(disks, total, args.Offset, args.Limit, []string{}) + return nil + }) + + type DiskOptions struct { + ID string `help:"Disk ID"` + } + shellutils.R(&DiskOptions{}, "disk-delete", "Delete disks", func(cli *qcloud.SRegion, args *DiskOptions) error { + e := cli.DeleteDisk(args.ID) + if e != nil { + return e + } + return nil + }) + + shellutils.R(&DiskOptions{}, "disk-show", "Show disk", func(cli *qcloud.SRegion, args *DiskOptions) error { + disk, err := cli.GetDisk(args.ID) + if err != nil { + return err + } + printObject(disk) + return nil + }) + + type DiskCreateOptions struct { + ZONE string `help:"Zone ID"` + CATEGORY string `help:"Disk category" choices:"CLOUD_BASIC|CLOUD_PREMIUM|CLOUD_SSD"` + NAME string `help:"Disk Name"` + SIZE int `help:"Disk Size GB"` + } + shellutils.R(&DiskCreateOptions{}, "disk-create", "Create disk", func(cli *qcloud.SRegion, args *DiskCreateOptions) error { + diskId, err := cli.CreateDisk(args.ZONE, args.CATEGORY, args.NAME, args.SIZE, "") + if err != nil { + return err + } + fmt.Println(diskId) + return nil + }) + + type DiskResizeOptions struct { + ID string `help:"Disk ID"` + SIZE int64 `help:"Disk Size GB"` + } + shellutils.R(&DiskResizeOptions{}, "disk-resize", "Resize disk", func(cli *qcloud.SRegion, args *DiskResizeOptions) error { + return cli.ResizeDisk(args.ID, args.SIZE) + }) + + type DiskResetOptions struct { + ID string `help:"Disk ID"` + SNAPSHOT string `help:"Snapshot ID"` + } + shellutils.R(&DiskResetOptions{}, "disk-reset", "Reset disk", func(cli *qcloud.SRegion, args *DiskResetOptions) error { + return cli.ResetDisk(args.ID, args.SNAPSHOT) + }) +} diff --git a/pkg/util/qcloud/shell/eip.go b/pkg/util/qcloud/shell/eip.go new file mode 100644 index 0000000000..ae6cb1f0db --- /dev/null +++ b/pkg/util/qcloud/shell/eip.go @@ -0,0 +1,71 @@ +package shell + +import ( + "yunion.io/x/onecloud/pkg/util/qcloud" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type EipListOptions struct { + Eip string `help:"EIP ID"` + Offset int `help:"List offset"` + Limit int `help:"List limit"` + } + shellutils.R(&EipListOptions{}, "eip-list", "List eips", func(cli *qcloud.SRegion, args *EipListOptions) error { + eips, total, err := cli.GetEips(args.Eip, args.Offset, args.Limit) + if err != nil { + return err + } + printList(eips, total, args.Offset, args.Limit, []string{}) + return nil + }) + + type EipAllocateOptions struct { + BANDWIDTH int `help:"EIP bandwoidth"` + NAME string `help:"EIP Name"` + ChargeType qcloud.TInternetChargeType `help:"EIP ChargeType"` + } + shellutils.R(&EipAllocateOptions{}, "eip-create", "Allocate an EIP", func(cli *qcloud.SRegion, args *EipAllocateOptions) error { + eip, err := cli.AllocateEIP(args.NAME, args.BANDWIDTH, args.ChargeType) + if err != nil { + return err + } + printObject(eip) + return nil + }) + + type EipReleaseOptions struct { + ID string `help:"EIP allocation ID"` + } + shellutils.R(&EipReleaseOptions{}, "eip-delete", "Release an EIP", func(cli *qcloud.SRegion, args *EipReleaseOptions) error { + return cli.DeallocateEIP(args.ID) + }) + + type EipShowOptions struct { + ID string `help:"EIP ID"` + } + shellutils.R(&EipShowOptions{}, "eip-show", "Show an EIP", func(cli *qcloud.SRegion, args *EipShowOptions) error { + eip, err := cli.GetEip(args.ID) + if err != nil { + return err + } + printObject(eip) + return nil + }) + + type EipAssociateOptions struct { + ID string `help:"EIP allocation ID"` + INSTANCE string `help:"Instance ID"` + } + shellutils.R(&EipAssociateOptions{}, "eip-associate", "Associate an EIP", func(cli *qcloud.SRegion, args *EipAssociateOptions) error { + return cli.AssociateEip(args.ID, args.INSTANCE) + }) + + type EipDissociateOptions struct { + ID string `help:"EIP allocation ID"` + } + + shellutils.R(&EipDissociateOptions{}, "eip-dissociate", "Dissociate an EIP", func(cli *qcloud.SRegion, args *EipDissociateOptions) error { + return cli.DissociateEip(args.ID) + }) +} diff --git a/pkg/util/qcloud/shell/image.go b/pkg/util/qcloud/shell/image.go new file mode 100644 index 0000000000..0f2bbe71a9 --- /dev/null +++ b/pkg/util/qcloud/shell/image.go @@ -0,0 +1,55 @@ +package shell + +import ( + "yunion.io/x/onecloud/pkg/util/qcloud" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type ImageListOptions struct { + Status string `help:"Image status"` + Owner string `help:"Image owner" choices:"PRIVATE_IMAGE|PUBLIC_IMAGE|MARKET_IMAGE|SHARED_IMAGE"` + Image string `help:"Image Id"` + Name string `help:"Image Name"` + Limit int `help:"page size"` + Offset int `help:"page offset"` + } + shellutils.R(&ImageListOptions{}, "image-list", "List images", func(cli *qcloud.SRegion, args *ImageListOptions) error { + imageIds := []string{} + if len(args.Image) > 0 { + imageIds = append(imageIds, args.Image) + } + images, total, err := cli.GetImages(args.Status, args.Owner, imageIds, args.Name, args.Offset, args.Limit) + if err != nil { + return err + } + printList(images, total, args.Offset, args.Limit, []string{}) + return nil + }) + + type ImageCreateOptions struct { + NAME string `helo:"Image name"` + OSTYPE string `helo:"Operation system" choices:"CentOS|Ubuntu|Debian|OpenSUSE|SUSE|CoreOS|FreeBSD|Other Linux|Windows Server 2008|Windows Server 2012|Windows Server 2016"` + OSARCH string `help:"OS Architecture" choices:"x86_64|i386"` + osVersion string `help:"OS Version"` + URL string `helo:"Cos URL"` + } + + shellutils.R(&ImageCreateOptions{}, "image-create", "Create image", func(cli *qcloud.SRegion, args *ImageCreateOptions) error { + image, err := cli.ImportImage(args.NAME, args.OSARCH, args.OSTYPE, args.osVersion, args.URL) + if err != nil { + return err + } + printObject(image) + return nil + }) + + type ImageDeleteOptions struct { + ID string `helo:"Image ID"` + } + + shellutils.R(&ImageDeleteOptions{}, "image-delete", "Delete image", func(cli *qcloud.SRegion, args *ImageDeleteOptions) error { + return cli.DeleteImage(args.ID) + }) + +} diff --git a/pkg/util/qcloud/shell/instance.go b/pkg/util/qcloud/shell/instance.go new file mode 100644 index 0000000000..1343410391 --- /dev/null +++ b/pkg/util/qcloud/shell/instance.go @@ -0,0 +1,176 @@ +package shell + +import ( + "fmt" + + "yunion.io/x/onecloud/pkg/util/qcloud" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type InstanceListOptions struct { + Id []string `help:"IDs of instances to show"` + Zone string `help:"Zone ID"` + Limit int `help:"page size"` + Offset int `help:"page offset"` + } + shellutils.R(&InstanceListOptions{}, "instance-list", "List intances", func(cli *qcloud.SRegion, args *InstanceListOptions) error { + instances, total, e := cli.GetInstances(args.Zone, args.Id, args.Offset, args.Limit) + if e != nil { + return e + } + printList(instances, total, args.Offset, args.Limit, []string{}) + return nil + }) + + type InstanceCrateOptions struct { + NAME string `help:"name of instance"` + IMAGE string `help:"image ID"` + CPU int `help:"CPU count"` + MEMORYGB int `help:"MemoryGB"` + Disk []int `help:"Data disk sizes int GB"` + STORAGE string `help:"Storage type" choices:"LOCAL_BASIC|LOCAL_SSD|CLOUD_BASIC|CLOUD_PREMIUM|CLOUD_SSD"` + NETWORK string `help:"Network ID"` + PASSWD string `help:"password"` + PublicKey string `help:"PublicKey"` + } + + shellutils.R(&InstanceCrateOptions{}, "instance-create", "Create a instance", func(cli *qcloud.SRegion, args *InstanceCrateOptions) error { + instance, e := cli.CreateInstanceSimple(args.NAME, args.IMAGE, args.CPU, args.MEMORYGB, args.STORAGE, args.Disk, args.NETWORK, args.PASSWD, args.PublicKey) + if e != nil { + return e + } + printObject(instance) + return nil + }) + + type InstanceDiskOperationOptions struct { + ID string `help:"instance ID"` + DISK string `help:"disk ID"` + } + + shellutils.R(&InstanceDiskOperationOptions{}, "instance-attach-disk", "Attach a disk to instance", func(cli *qcloud.SRegion, args *InstanceDiskOperationOptions) error { + err := cli.AttachDisk(args.ID, args.DISK) + if err != nil { + return err + } + return nil + }) + + shellutils.R(&InstanceDiskOperationOptions{}, "instance-detach-disk", "Detach a disk to instance", func(cli *qcloud.SRegion, args *InstanceDiskOperationOptions) error { + err := cli.DetachDisk(args.ID, args.DISK) + if err != nil { + return err + } + return nil + }) + + type InstanceOperationOptions struct { + ID string `help:"instance ID"` + } + shellutils.R(&InstanceOperationOptions{}, "instance-start", "Start a instance", func(cli *qcloud.SRegion, args *InstanceOperationOptions) error { + err := cli.StartVM(args.ID) + if err != nil { + return err + } + return nil + }) + + shellutils.R(&InstanceOperationOptions{}, "instance-vnc", "Get a instance VNC url", func(cli *qcloud.SRegion, args *InstanceOperationOptions) error { + url, err := cli.GetInstanceVNCUrl(args.ID) + if err != nil { + return err + } + fmt.Println(url) + return nil + }) + + type InstanceStopOptions struct { + ID string `help:"instance ID"` + Force bool `help:"Force stop instance"` + } + shellutils.R(&InstanceStopOptions{}, "instance-stop", "Stop a instance", func(cli *qcloud.SRegion, args *InstanceStopOptions) error { + err := cli.StopVM(args.ID, args.Force) + if err != nil { + return err + } + return nil + }) + shellutils.R(&InstanceOperationOptions{}, "instance-delete", "Delete a instance", func(cli *qcloud.SRegion, args *InstanceOperationOptions) error { + err := cli.DeleteVM(args.ID) + if err != nil { + return err + } + return nil + }) + + /* + server-change-config 更改系统配置 + server-reset + */ + type InstanceDeployOptions struct { + ID string `help:"instance ID"` + Name string `help:"new instance name"` + Hostname string `help:"new hostname"` + Keypair string `help:"Keypair Name"` + DeleteKeypair bool `help:"Remove SSH keypair"` + Password string `help:"new password"` + // ResetPassword bool `help:"Force reset password"` + Description string `help:"new instances description"` + } + + shellutils.R(&InstanceDeployOptions{}, "instance-deploy", "Deploy keypair/password to a stopped virtual server", func(cli *qcloud.SRegion, args *InstanceDeployOptions) error { + err := cli.DeployVM(args.ID, args.Name, args.Password, args.Keypair, args.DeleteKeypair, args.Description) + if err != nil { + return err + } + return nil + }) + + type InstanceRebuildRootOptions struct { + ID string `help:"instance ID"` + Image string `help:"Image ID"` + Password string `help:"pasword"` + Keypair string `help:"keypair name"` + Size int `help:"system disk size in GB"` + } + + shellutils.R(&InstanceRebuildRootOptions{}, "instance-rebuild-root", "Reinstall virtual server system image", func(cli *qcloud.SRegion, args *InstanceRebuildRootOptions) error { + diskID, err := cli.ReplaceSystemDisk(args.ID, args.Image, args.Password, args.Keypair, args.Size) + if err != nil { + return err + } + fmt.Printf("New diskID is %s", diskID) + return nil + }) + + type InstanceChangeConfigOptions struct { + ID string `help:"instance ID"` + Ncpu int `help:"number of CPU"` + Vmem int `help:"MiB of memory"` + Disk []int `help:"Data disk sizes int GB"` + } + + shellutils.R(&InstanceChangeConfigOptions{}, "instance-change-config", "Deploy keypair/password to a stopped virtual server", func(cli *qcloud.SRegion, args *InstanceChangeConfigOptions) error { + instance, e := cli.GetInstance(args.ID) + if e != nil { + return e + } + + // todo : add create disks + err := cli.ChangeVMConfig(instance.Placement.Zone, args.ID, args.Ncpu, args.Vmem, nil) + if err != nil { + return err + } + return nil + }) + + type InstanceUpdatePasswordOptions struct { + ID string `help:"Instance ID"` + PASSWD string `help:"new password"` + } + shellutils.R(&InstanceUpdatePasswordOptions{}, "instance-update-password", "Update instance password", func(cli *qcloud.SRegion, args *InstanceUpdatePasswordOptions) error { + err := cli.UpdateInstancePassword(args.ID, args.PASSWD) + return err + }) +} diff --git a/pkg/util/qcloud/shell/keypair.go b/pkg/util/qcloud/shell/keypair.go new file mode 100644 index 0000000000..99cd96d389 --- /dev/null +++ b/pkg/util/qcloud/shell/keypair.go @@ -0,0 +1,45 @@ +package shell + +import ( + "yunion.io/x/onecloud/pkg/util/qcloud" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type KeyPairListOptions struct { + Name string `help:"Keypair Name"` + IDs []string `help:"Keypari ids"` + Offset int `help:"List offset"` + Limit int `help:"List limit"` + } + shellutils.R(&KeyPairListOptions{}, "keypair-list", "List keypair", func(cli *qcloud.SRegion, args *KeyPairListOptions) error { + keypairs, total, err := cli.GetKeypairs(args.Name, args.IDs, args.Offset, args.Limit) + if err != nil { + return err + } + printList(keypairs, total, args.Offset, args.Limit, []string{}) + return nil + }) + + type KeyPairCreateOptions struct { + NAME string `help:"Keypair Name"` + } + shellutils.R(&KeyPairCreateOptions{}, "keypair-create", "Create keypair", func(cli *qcloud.SRegion, args *KeyPairCreateOptions) error { + keypair, err := cli.CreateKeyPair(args.NAME) + if err != nil { + return err + } + printObject(keypair) + return nil + }) + + type KeyPairAssociateOptions struct { + KEYPAIRID string `help:"Keypair ID"` + INSTANCEID string `help:"Instance ID"` + } + + shellutils.R(&KeyPairAssociateOptions{}, "keypair-associate-instance", "Attach Keypair to a instance", func(cli *qcloud.SRegion, args *KeyPairAssociateOptions) error { + return cli.AttachKeypair(args.INSTANCEID, args.KEYPAIRID) + }) + +} diff --git a/pkg/util/qcloud/shell/network.go b/pkg/util/qcloud/shell/network.go new file mode 100644 index 0000000000..3e16e834a8 --- /dev/null +++ b/pkg/util/qcloud/shell/network.go @@ -0,0 +1,54 @@ +package shell + +import ( + "fmt" + + "yunion.io/x/onecloud/pkg/util/qcloud" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type NetworkListOptions struct { + Limit int `help:"page size"` + Offset int `help:"page offset"` + } + shellutils.R(&NetworkListOptions{}, "network-list", "List networks", func(cli *qcloud.SRegion, args *NetworkListOptions) error { + networks, total, e := cli.GetNetworks(nil, "", args.Offset, args.Limit) + if e != nil { + return e + } + printList(networks, total, args.Offset, args.Limit, []string{}) + return nil + }) + + type NetworkOptions struct { + ID string `help:"Network ID"` + } + shellutils.R(&NetworkOptions{}, "network-delete", "Delete network", func(cli *qcloud.SRegion, args *NetworkOptions) error { + return cli.DeleteNetwork(args.ID) + }) + + shellutils.R(&NetworkOptions{}, "network-show", "Show network", func(cli *qcloud.SRegion, args *NetworkOptions) error { + network, err := cli.GetNetwork(args.ID) + if err != nil { + return err + } + printObject(network) + return nil + }) + + type NetworkCreateOptions struct { + ZONE string `help:"Zone ID"` + VPC string `help:"VPC ID"` + CIDR string `help:"Network CIDR"` + NAME string `help:"Network Name"` + } + shellutils.R(&NetworkCreateOptions{}, "network-create", "Create network", func(cli *qcloud.SRegion, args *NetworkCreateOptions) error { + networkId, err := cli.CreateNetwork(args.ZONE, args.VPC, args.NAME, args.CIDR, "") + if err != nil { + return err + } + fmt.Println(networkId) + return nil + }) +} diff --git a/pkg/util/qcloud/shell/printutils.go b/pkg/util/qcloud/shell/printutils.go new file mode 100644 index 0000000000..6d573bce88 --- /dev/null +++ b/pkg/util/qcloud/shell/printutils.go @@ -0,0 +1,11 @@ +package shell + +import "yunion.io/x/onecloud/pkg/util/printutils" + +func printList(data interface{}, total, offset, limit int, columns []string) { + printutils.PrintInterfaceList(data, total, offset, limit, columns) +} + +func printObject(obj interface{}) { + printutils.PrintInterfaceObject(obj) +} diff --git a/pkg/util/qcloud/shell/region.go b/pkg/util/qcloud/shell/region.go new file mode 100644 index 0000000000..0c8c7b9425 --- /dev/null +++ b/pkg/util/qcloud/shell/region.go @@ -0,0 +1,16 @@ +package shell + +import ( + "yunion.io/x/onecloud/pkg/util/qcloud" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type RegionListOptions struct { + } + shellutils.R(&RegionListOptions{}, "region-list", "List regions", func(cli *qcloud.SRegion, args *RegionListOptions) error { + regions := cli.GetClient().GetRegions() + printList(regions, 0, 0, 0, nil) + return nil + }) +} diff --git a/pkg/util/qcloud/shell/securitygroup.go b/pkg/util/qcloud/shell/securitygroup.go new file mode 100644 index 0000000000..35d6536d34 --- /dev/null +++ b/pkg/util/qcloud/shell/securitygroup.go @@ -0,0 +1,66 @@ +package shell + +import ( + "yunion.io/x/onecloud/pkg/util/qcloud" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type SecurityGroupListOptions struct { + Limit int `help:"page size"` + Offset int `help:"page offset"` + } + shellutils.R(&SecurityGroupListOptions{}, "security-group-list", "List SecurityGroup", func(cli *qcloud.SRegion, args *SecurityGroupListOptions) error { + secgrps, total, err := cli.GetSecurityGroups("", args.Limit, args.Offset) + if err != nil { + return err + } + printList(secgrps, total, args.Offset, args.Limit, []string{}) + return nil + }) + + type SecurityGroupOptions struct { + ID string `help:"SecurityGroup ID"` + } + shellutils.R(&SecurityGroupOptions{}, "security-group-show", "Show SecurityGroup", func(cli *qcloud.SRegion, args *SecurityGroupOptions) error { + secgroup, err := cli.GetSecurityGroupDetails(args.ID) + if err != nil { + return err + } + printObject(secgroup) + return nil + }) + + shellutils.R(&SecurityGroupOptions{}, "security-group-delete", "Delete SecurityGroup", func(cli *qcloud.SRegion, args *SecurityGroupOptions) error { + return cli.DeleteSecurityGroup(args.ID) + }) + + type SecurityGroupCreateOptions struct { + NAME string `help:"SecurityGroup Name"` + Desc string `help:"SecurityGroup Description"` + } + + shellutils.R(&SecurityGroupCreateOptions{}, "security-group-create", "Create SecurityGroup", func(cli *qcloud.SRegion, args *SecurityGroupCreateOptions) error { + secgrp, err := cli.CreateSecurityGroup(args.NAME, args.Desc) + if err != nil { + return err + } + printObject(secgrp) + return nil + }) + + type AddressShowOptions struct { + Id string `help:"IP address ID"` + Name string `help:"IP address name"` + Limit int `help:"page size"` + Offset int `help:"page offset"` + } + shellutils.R(&AddressShowOptions{}, "address-list", "Show address", func(cli *qcloud.SRegion, args *AddressShowOptions) error { + address, total, err := cli.AddressList(args.Id, args.Name, args.Offset, args.Limit) + if err != nil { + return err + } + printList(address, total, args.Offset, args.Limit, []string{}) + return nil + }) +} diff --git a/pkg/util/qcloud/shell/snapshot.go b/pkg/util/qcloud/shell/snapshot.go new file mode 100644 index 0000000000..997f921f26 --- /dev/null +++ b/pkg/util/qcloud/shell/snapshot.go @@ -0,0 +1,45 @@ +package shell + +import ( + "yunion.io/x/onecloud/pkg/util/qcloud" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type SnapshotListOptions struct { + DiskId string `help:"Disk ID"` + InstanceId string `help:"Instance ID"` + SnapshotIds []string `helo:"Snapshot ids"` + Name string `help:"Snapshot Name"` + Limit int `help:"page size"` + Offset int `help:"page offset"` + } + shellutils.R(&SnapshotListOptions{}, "snapshot-list", "List snapshot", func(cli *qcloud.SRegion, args *SnapshotListOptions) error { + snapshots, total, err := cli.GetSnapshots(args.InstanceId, args.DiskId, args.Name, args.SnapshotIds, args.Offset, args.Limit) + if err != nil { + return err + } + printList(snapshots, total, args.Offset, args.Limit, []string{}) + return nil + }) + + type SnapshotDeleteOptions struct { + ID string `help:"Snapshot ID"` + } + + shellutils.R(&SnapshotDeleteOptions{}, "snapshot-delete", "Delete snapshot", func(cli *qcloud.SRegion, args *SnapshotDeleteOptions) error { + return cli.DeleteSnapshot(args.ID) + }) + + type SnapshotCreateOptions struct { + DISK string `help:"Disk ID"` + Name string `help:"Snapeshot Name"` + Desc string `help:"Snapshot Desc"` + } + + shellutils.R(&SnapshotCreateOptions{}, "snapshot-create", "Create snapshot", func(cli *qcloud.SRegion, args *SnapshotCreateOptions) error { + _, err := cli.CreateSnapshot(args.DISK, args.Name, args.Desc) + return err + }) + +} diff --git a/pkg/util/qcloud/shell/vpc.go b/pkg/util/qcloud/shell/vpc.go new file mode 100644 index 0000000000..67f1920524 --- /dev/null +++ b/pkg/util/qcloud/shell/vpc.go @@ -0,0 +1,41 @@ +package shell + +import ( + "yunion.io/x/onecloud/pkg/util/qcloud" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type VpcListOptions struct { + Limit int `help:"page size"` + Offset int `help:"page offset"` + } + shellutils.R(&VpcListOptions{}, "vpc-list", "List vpcs", func(cli *qcloud.SRegion, args *VpcListOptions) error { + vpcs, total, err := cli.GetVpcs(nil, args.Offset, args.Limit) + if err != nil { + return err + } + printList(vpcs, total, args.Offset, args.Limit, []string{}) + return nil + }) + + type VpcCreateOptions struct { + NAME string `help:"Name for vpc"` + CIDR string `help:"Cidr for vpc" choices:"10.0.0.0/16|172.16.0.0/12|192.168.0.0/16"` + } + shellutils.R(&VpcCreateOptions{}, "vpc-create", "Create vpc", func(cli *qcloud.SRegion, args *VpcCreateOptions) error { + vpc, err := cli.CreateIVpc(args.NAME, "", args.CIDR) + if err != nil { + return err + } + printObject(vpc) + return nil + }) + + type VpcDeleteOptions struct { + ID string `help:"VPC ID or Name"` + } + shellutils.R(&VpcDeleteOptions{}, "vpc-delete", "Delete vpc", func(cli *qcloud.SRegion, args *VpcDeleteOptions) error { + return cli.DeleteVpc(args.ID) + }) +} diff --git a/pkg/util/qcloud/shell/zone.go b/pkg/util/qcloud/shell/zone.go new file mode 100644 index 0000000000..5ae269ecb0 --- /dev/null +++ b/pkg/util/qcloud/shell/zone.go @@ -0,0 +1,21 @@ +package shell + +import ( + "yunion.io/x/onecloud/pkg/util/qcloud" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type ZoneListOptions struct { + Limit int `help:"page size"` + Offset int `help:"page offset"` + } + shellutils.R(&ZoneListOptions{}, "zone-list", "List zones", func(cli *qcloud.SRegion, args *ZoneListOptions) error { + zones, err := cli.GetIZones() + if err != nil { + return err + } + printList(zones, len(zones), args.Offset, args.Limit, []string{}) + return nil + }) +} diff --git a/pkg/util/qcloud/snapshot.go b/pkg/util/qcloud/snapshot.go new file mode 100644 index 0000000000..7f53083b0e --- /dev/null +++ b/pkg/util/qcloud/snapshot.go @@ -0,0 +1,218 @@ +package qcloud + +import ( + "fmt" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" +) + +type SnapshotStatusType string + +const ( + SnapshotStatusAccomplished SnapshotStatusType = "NORMAL" + SnapshotStatusProgress SnapshotStatusType = "CREATING" + SnapshotStatusFailed SnapshotStatusType = "failed" +) + +type SSnapshot struct { + region *SRegion + + SnapshotId string // 快照ID。 + Placement Placement // 快照所在的位置。 + DiskUsage string // 创建此快照的云硬盘类型。取值范围:SYSTEM_DISK:系统盘 DATA_DISK:数据盘。 + DiskId string // 创建此快照的云硬盘ID。 + DiskSize int32 // 创建此快照的云硬盘大小,单位GB。 + SnapshotState SnapshotStatusType // 快照的状态。取值范围: NORMAL:正常 CREATING:创建中 ROLLBACKING:回滚中 COPYING_FROM_REMOTE:跨地域复制快照拷贝中。 + SnapshotName string // 快照名称,用户自定义的快照别名。调用ModifySnapshotAttribute可修改此字段。 + Percent int // 快照创建进度百分比,快照创建成功后此字段恒为100。 + CreateTime time.Time // 快照的创建时间。 + DeadlineTime time.Time // 快照到期时间。如果快照为永久保留,此字段为空。 + Encrypt bool // 是否为加密盘创建的快照。取值范围:true:该快照为加密盘创建的 false:非加密盘创建的快照。 + IsPermanent bool // 是否为永久快照。取值范围: true:永久快照 false:非永久快照。 + CopyingToRegions []string // 快照正在跨地域复制的目的地域,默认取值为[]。 + CopyFromRemote bool // 是否为跨地域复制的快照。取值范围:true:表示为跨地域复制的快照。 false:本地域的快照。 +} + +func (self *SRegion) GetISnapshotById(snapshotId string) (cloudprovider.ICloudSnapshot, error) { + snapshots, total, err := self.GetSnapshots("", "", "", []string{snapshotId}, 0, 1) + if err != nil { + return nil, err + } + if total > 1 { + return nil, cloudprovider.ErrDuplicateId + } + if total == 0 { + return nil, cloudprovider.ErrNotFound + } + return &snapshots[0], nil +} + +func (self *SSnapshot) GetStatus() string { + // NORMAL:正常 + // CREATING:创建中 + // ROLLBACKING:回滚中 + // COPYING_FROM_REMOTE:跨地域复制快照拷贝中。 + switch self.SnapshotState { + case "NORMAL", "COPYING_FROM_REMOTE": + return models.SNAPSHOT_READY + case "CREATING": + return models.SNAPSHOT_CREATING + case "ROLLBACKING": + return models.SNAPSHOT_ROLLBACKING + } + return models.SNAPSHOT_UNKNOWN +} + +func (self *SSnapshot) IsEmulated() bool { + return false +} + +func (self *SSnapshot) Refresh() error { + snapshots, total, err := self.region.GetSnapshots("", "", "", []string{self.SnapshotId}, 0, 1) + if err != nil { + return err + } + if total > 1 { + return cloudprovider.ErrDuplicateId + } + + if total == 0 { + return cloudprovider.ErrNotFound + } + return jsonutils.Update(self, snapshots[0]) +} + +func (self *SRegion) GetSnapshots(instanceId string, diskId string, snapshotName string, snapshotIds []string, offset int, limit int) ([]SSnapshot, int, error) { + if limit > 50 || limit <= 0 { + limit = 50 + } + params := make(map[string]string) + params["Limit"] = fmt.Sprintf("%d", limit) + params["Offset"] = fmt.Sprintf("%d", offset) + + filter := 0 + if len(instanceId) > 0 { + } + if len(diskId) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "disk-id" + params[fmt.Sprintf("Filters.%d.Values", filter)] = diskId + filter++ + } + if len(snapshotName) > 0 { + params[fmt.Sprintf("Filters.%d.Name", filter)] = "snapshot-name" + params[fmt.Sprintf("Filters.%d.Values", filter)] = snapshotName + filter++ + } + if snapshotIds != nil && len(snapshotIds) > 0 { + for index, snapshotId := range snapshotIds { + params[fmt.Sprintf("SnapshotIds.%d", index)] = snapshotId + } + } + snapshots := []SSnapshot{} + body, err := self.cbsRequest("DescribeSnapshots", params) + if err != nil { + log.Errorf("GetSnapshots fail %s", err) + return nil, 0, err + } + body.Unmarshal(&snapshots, "SnapshotSet") + if err != nil { + return nil, 0, err + } + total, _ := body.Float("TotalCount") + for i := 0; i < len(snapshots); i++ { + snapshots[i].region = self + } + return snapshots, int(total), nil +} + +func (self *SRegion) GetISnapshots() ([]cloudprovider.ICloudSnapshot, error) { + snapshots, total, err := self.GetSnapshots("", "", "", []string{}, 0, 50) + if err != nil { + return nil, err + } + for len(snapshots) < total { + var parts []SSnapshot + parts, total, err = self.GetSnapshots("", "", "", []string{}, len(snapshots), 50) + if err != nil { + return nil, err + } + snapshots = append(snapshots, parts...) + } + ret := make([]cloudprovider.ICloudSnapshot, len(snapshots)) + for i := 0; i < len(snapshots); i++ { + ret[i] = &snapshots[i] + } + return ret, nil +} + +func (self *SSnapshot) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SSnapshot) GetRegionId() string { + return self.region.GetId() +} + +func (self *SSnapshot) GetManagerId() string { + return self.region.client.providerId +} + +func (self *SSnapshot) GetSize() int32 { + return self.DiskSize +} + +func (self *SSnapshot) GetDiskId() string { + return self.DiskId +} + +func (self *SSnapshot) GetId() string { + return self.SnapshotId +} + +func (self *SSnapshot) GetGlobalId() string { + return fmt.Sprintf("%s", self.SnapshotId) +} + +func (self *SSnapshot) GetName() string { + return self.SnapshotName +} + +func (self *SSnapshot) Delete() error { + if self.region == nil { + return fmt.Errorf("not init region for snapshot %s", self.SnapshotId) + } + return self.region.DeleteSnapshot(self.SnapshotId) +} + +func (self *SSnapshot) GetDiskType() string { + switch self.DiskUsage { + case "SYSTEM_DISK": + return models.DISK_TYPE_SYS + case "DATA_DISK": + return models.DISK_TYPE_DATA + } + return models.DISK_TYPE_DATA +} + +func (self *SRegion) DeleteSnapshot(snapshotId string) error { + params := map[string]string{"SnapshotIds.0": snapshotId} + _, err := self.cbsRequest("DeleteSnapshots", params) + return err +} + +func (self *SRegion) CreateSnapshot(diskId, name, desc string) (string, error) { + params := make(map[string]string) + params["DiskId"] = diskId + params["SnapshotName"] = name + + body, err := self.cbsRequest("CreateSnapshot", params) + if err != nil { + log.Errorf("CreateSnapshot fail %s", err) + return "", err + } + return body.GetString("SnapshotId") +} diff --git a/pkg/util/qcloud/storage.go b/pkg/util/qcloud/storage.go new file mode 100644 index 0000000000..a9ebca1bc4 --- /dev/null +++ b/pkg/util/qcloud/storage.go @@ -0,0 +1,131 @@ +package qcloud + +import ( + "fmt" + "strings" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" +) + +type SStorage struct { + zone *SZone + storageType string +} + +func (self *SStorage) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SStorage) GetId() string { + return fmt.Sprintf("%s-%s-%s", self.zone.region.client.providerId, self.zone.GetId(), self.storageType) +} + +func (self *SStorage) GetName() string { + return fmt.Sprintf("%s-%s-%s", self.zone.region.client.providerName, self.zone.GetId(), self.storageType) +} + +func (self *SStorage) GetGlobalId() string { + return fmt.Sprintf("%s-%s-%s", self.zone.region.client.providerId, self.zone.GetGlobalId(), self.storageType) +} + +func (self *SStorage) IsEmulated() bool { + return true +} + +func (self *SStorage) GetIZone() cloudprovider.ICloudZone { + return self.zone +} + +func (self *SStorage) GetIDisks() ([]cloudprovider.ICloudDisk, error) { + disks := make([]SDisk, 0) + for { + parts, total, err := self.zone.region.GetDisks("", self.zone.GetId(), self.storageType, nil, len(disks), 50) + if err != nil { + log.Errorf("GetDisks fail %s", err) + return nil, err + } + disks = append(disks, parts...) + if len(disks) >= total { + break + } + } + idisks := make([]cloudprovider.ICloudDisk, len(disks)) + for i := 0; i < len(disks); i++ { + disks[i].storage = self + idisks[i] = &disks[i] + } + return idisks, nil +} + +func (self *SStorage) GetStorageType() string { + return strings.ToLower(self.storageType) +} + +func (self *SStorage) GetMediumType() string { + if strings.HasSuffix(self.storageType, "_BASIC") { + return models.DISK_TYPE_ROTATE + } + return models.DISK_TYPE_SSD +} + +func (self *SStorage) GetCapacityMB() int { + return 0 // unlimited +} + +func (self *SStorage) GetStorageConf() jsonutils.JSONObject { + conf := jsonutils.NewDict() + return conf +} + +func (self *SStorage) GetManagerId() string { + return self.zone.region.client.providerId +} + +func (self *SStorage) GetStatus() string { + return models.STORAGE_ONLINE +} + +func (self *SStorage) Refresh() error { + // do nothing + return nil +} + +func (self *SStorage) GetEnabled() bool { + return true +} + +func (self *SStorage) GetIStoragecache() cloudprovider.ICloudStoragecache { + return self.zone.region.getStoragecache() +} + +func (self *SStorage) CreateIDisk(name string, sizeGb int, desc string) (cloudprovider.ICloudDisk, error) { + diskId, err := self.zone.region.CreateDisk(self.zone.Zone, self.storageType, name, sizeGb, desc) + if err != nil { + log.Errorf("createDisk fail %s", err) + return nil, err + } + //腾讯刚创建完成的磁盘,需要稍微等待才能查询 + for i := 0; i < 3; i++ { + disk, err := self.zone.region.GetDisk(diskId) + if err == nil { + disk.storage = self + return disk, nil + } + time.Sleep(time.Second * 3) + } + log.Errorf("getDisk fail %s id %s", err, diskId) + return nil, cloudprovider.ErrNotFound +} + +func (self *SStorage) GetIDisk(idStr string) (cloudprovider.ICloudDisk, error) { + disk, err := self.zone.region.GetDisk(idStr) + if err != nil { + return nil, err + } + disk.storage = self + return disk, nil +} diff --git a/pkg/util/qcloud/storagecache.go b/pkg/util/qcloud/storagecache.go new file mode 100644 index 0000000000..f51281d605 --- /dev/null +++ b/pkg/util/qcloud/storagecache.go @@ -0,0 +1,208 @@ +package qcloud + +import ( + "context" + "fmt" + "io" + "os" + "strings" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/options" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/auth" + "yunion.io/x/onecloud/pkg/mcclient/modules" + + coslib "github.com/nelsonken/cos-go-sdk-v5/cos" +) + +type SStoragecache struct { + region *SRegion + + iimages []cloudprovider.ICloudImage +} + +func (self *SStoragecache) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SStoragecache) GetId() string { + return fmt.Sprintf("%s-%s", self.region.client.providerId, self.region.GetId()) +} + +func (self *SStoragecache) GetName() string { + return fmt.Sprintf("%s-%s", self.region.client.providerName, self.region.GetId()) +} + +func (self *SStoragecache) GetStatus() string { + return "available" +} + +func (self *SStoragecache) Refresh() error { + return nil +} + +func (self *SStoragecache) GetGlobalId() string { + return fmt.Sprintf("%s-%s", self.region.client.providerId, self.region.GetGlobalId()) +} + +func (self *SStoragecache) IsEmulated() bool { + return false +} + +func (self *SStoragecache) GetManagerId() string { + return self.region.client.providerId +} + +func (self *SStoragecache) CreateIImage(snapshoutId, imageName, osType, imageDesc string) (cloudprovider.ICloudImage, error) { + // if imageId, err := self.region.createIImage(snapshoutId, imageName, imageDesc); err != nil { + // return nil, err + // } else if image, err := self.region.GetImage(imageId); err != nil { + // return nil, err + // } else { + // image.storageCache = self + // iimage := make([]cloudprovider.ICloudImage, 1) + // iimage[0] = image + // if err := cloudprovider.WaitStatus(iimage[0], compute.IMAGE_STATUS_ACTIVE, 15*time.Second, 3600*time.Second); err != nil { + // return nil, err + // } + // return iimage[0], nil + // } + return nil, nil +} + +func (self *SStoragecache) DownloadImage(userCred mcclient.TokenCredential, imageId string, extId string, path string) (jsonutils.JSONObject, error) { + //return self.downloadImage(userCred, imageId, extId, path) + return nil, nil +} + +func (self *SStoragecache) fetchImages() error { + images := make([]SImage, 0) + for { + parts, total, err := self.region.GetImages("", "PRIVATE_IMAGE", nil, "", len(images), 50) + if err != nil { + return err + } + images = append(images, parts...) + if len(images) >= total { + break + } + } + self.iimages = make([]cloudprovider.ICloudImage, len(images)) + for i := 0; i < len(images); i += 1 { + images[i].storageCache = self + self.iimages[i] = &images[i] + } + return nil +} + +func (self *SStoragecache) GetIImages() ([]cloudprovider.ICloudImage, error) { + if self.iimages == nil { + err := self.fetchImages() + if err != nil { + return nil, err + } + } + return self.iimages, nil +} + +func (self *SStoragecache) UploadImage(userCred mcclient.TokenCredential, imageId string, osArch, osType, osDist string, extId string, isForce bool) (string, error) { + if len(extId) > 0 { + log.Debugf("UploadImage: Image external ID exists %s", extId) + + status, err := self.region.GetImageStatus(extId) + if err != nil { + log.Errorf("GetImageStatus error %s", err) + } + if status == ImageStatusAvailable && !isForce { + return extId, nil + } + } else { + log.Debugf("UploadImage: no external ID") + } + return self.uploadImage(userCred, imageId, osArch, osType, osDist, isForce) +} + +func (self *SRegion) getCosUrl(bucket, object string) string { + //signature := cosauth.NewSignature(self.client.AppID, bucket, self.client.SecretID, time.Now().Add(time.Minute*30).String(), time.Now().String(), "yunion", object).SignOnce(self.client.SecretKey) + return fmt.Sprintf("http://%s-%s.cos.%s.myqcloud.com/%s", bucket, self.client.AppID, self.Region, object) +} + +func (self *SStoragecache) uploadImage(userCred mcclient.TokenCredential, imageId string, osArch, osType, osDist string, isForce bool) (string, error) { + // first upload image to oss + s := auth.GetAdminSession(options.Options.Region, "") + + meta, reader, err := modules.Images.Download(s, imageId) + if err != nil { + return "", err + } + + tmpFile := fmt.Sprintf("%s/%s", options.Options.TempPath, imageId) + defer os.Remove(tmpFile) + f, err := os.Create(tmpFile) + if err != nil { + return "", err + } + defer f.Close() + if _, err := io.Copy(f, reader); err != nil { + return "", err + } + + log.Infof("meta data %s", meta) + cos, err := self.region.GetCosClient() + if err != nil { + log.Errorf("GetOssClient err %s", err) + return "", err + } + bucketName := strings.ToLower(fmt.Sprintf("imgcache-%s", self.region.GetId())) + err = cos.BucketExists(context.Background(), bucketName) + if err != nil { + log.Debugf("Bucket %s not exists, to create ...", bucketName) + err := cos.CreateBucket(context.Background(), bucketName, &coslib.AccessControl{ACL: "public-read"}) + if err != nil { + log.Errorf("Create bucket error %s", err) + return "", err + } + } else { + log.Debugf("Bucket %s exists", bucketName) + } + log.Debugf("To upload image to bucket %s ...", bucketName) + err = cos.Bucket(bucketName).UploadObjectBySlice(context.Background(), imageId, tmpFile, 3, map[string]string{}) + if err != nil { + log.Errorf("UploadObject error %s %s", imageId, err) + return "", err + } + // 腾讯云镜像名称需要小于20个字符 + imageBaseName := imageId[:10] + if imageBaseName[0] >= '0' && imageBaseName[0] <= '9' { + imageBaseName = fmt.Sprintf("img%s", imageId[:10]) + } + imageName := imageBaseName + nameIdx := 1 + + // check image name, avoid name conflict + for { + _, err = self.region.GetImageByName(imageName) + if err != nil { + if err == cloudprovider.ErrNotFound { + break + } else { + return "", err + } + } + imageName = fmt.Sprintf("%s-%d", imageBaseName, nameIdx) + nameIdx++ + } + + log.Debugf("Import image %s", imageName) + if image, err := self.region.ImportImage(imageName, osArch, osType, osDist, self.region.getCosUrl(bucketName, imageId)); err != nil { + return "", err + } else if cloudprovider.WaitStatus(image, string(ImageStatusAvailable), 15*time.Second, 3600*time.Second); err != nil { + return "", err + } else { + return image.ImageId, nil + } +} diff --git a/pkg/util/qcloud/vpc.go b/pkg/util/qcloud/vpc.go new file mode 100644 index 0000000000..5aee1b7161 --- /dev/null +++ b/pkg/util/qcloud/vpc.go @@ -0,0 +1,193 @@ +package qcloud + +import ( + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/pkg/util/secrules" +) + +type SVpc struct { + region *SRegion + + iwires []cloudprovider.ICloudWire + + secgroups []cloudprovider.ICloudSecurityGroup + + CidrBlock string + CreatedTime time.Time + DhcpOptionsId string + DnsServerSet []string + DomainName string + EnableMulticast bool + IsDefault bool + VpcId string + VpcName string +} + +func (self *SVpc) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SVpc) GetId() string { + return self.VpcId +} + +func (self *SVpc) GetName() string { + if len(self.VpcName) > 0 { + return self.VpcName + } + return self.VpcId +} + +func (self *SVpc) GetGlobalId() string { + return self.VpcId +} + +func (self *SVpc) IsEmulated() bool { + return false +} + +func (self *SVpc) GetIsDefault() bool { + return self.IsDefault +} + +func (self *SVpc) GetCidrBlock() string { + return self.CidrBlock +} + +func (self *SVpc) GetStatus() string { + return models.VPC_STATUS_AVAILABLE +} + +func (self *SVpc) Delete() error { + return self.region.DeleteVpc(self.VpcId) +} + +func (self *SVpc) GetISecurityGroups() ([]cloudprovider.ICloudSecurityGroup, error) { + secgroups := make([]SSecurityGroup, 0) + for { + parts, total, err := self.region.GetSecurityGroups(self.VpcId, len(secgroups), 50) + if err != nil { + return nil, err + } + secgroups = append(secgroups, parts...) + if len(secgroups) >= total { + break + } + } + isecgroups := make([]cloudprovider.ICloudSecurityGroup, len(secgroups)) + for i := 0; i < len(secgroups); i++ { + secgroups[i].vpc = self + isecgroups[i] = &secgroups[i] + } + return isecgroups, nil +} + +func (self *SVpc) getWireByZoneId(zoneId string) *SWire { + for i := 0; i <= len(self.iwires); i++ { + wire := self.iwires[i].(*SWire) + if wire.zone.Zone == zoneId { + return wire + } + } + return nil +} + +func (self *SVpc) fetchNetworks() error { + networks, total, err := self.region.GetNetworks(nil, self.VpcId, 0, 50) + if err != nil { + return err + } + if total > len(networks) { + networks, _, err = self.region.GetNetworks(nil, self.VpcId, 0, total) + if err != nil { + return err + } + } + for i := 0; i < len(networks); i += 1 { + wire := self.getWireByZoneId(networks[i].Zone) + networks[i].wire = wire + wire.addNetwork(&networks[i]) + } + return nil +} + +func (self *SVpc) GetIWireById(wireId string) (cloudprovider.ICloudWire, error) { + if self.iwires == nil { + err := self.fetchNetworks() + if err != nil { + return nil, err + } + } + for i := 0; i < len(self.iwires); i += 1 { + if self.iwires[i].GetGlobalId() == wireId { + return self.iwires[i], nil + } + } + return nil, cloudprovider.ErrNotFound +} + +func (self *SVpc) GetIWires() ([]cloudprovider.ICloudWire, error) { + if self.iwires == nil { + err := self.fetchNetworks() + if err != nil { + return nil, err + } + } + return self.iwires, nil +} + +func (self *SVpc) GetManagerId() string { + return self.region.client.providerId +} + +func (self *SVpc) GetRegion() cloudprovider.ICloudRegion { + return self.region +} + +func (self *SVpc) Refresh() error { + new, err := self.region.getVpc(self.VpcId) + if err != nil { + return err + } + return jsonutils.Update(self, new) +} + +func (self *SVpc) SyncSecurityGroup(secgroupId string, name string, rules []secrules.SecurityRule) (string, error) { + secgrpId := "" + // if secgroup, err := self.region.getSecurityGroupByTag(self.VpcId, secgroupId); err != nil { + // if secgrpId, err = self.region.createSecurityGroup(self.VpcId, name, ""); err != nil { + // return "", err + // } else if err := self.region.addTagToSecurityGroup(secgrpId, "id", secgroupId, 1); err != nil { + // return "", err + // } + // //addRules + // log.Debugf("Add Rules for %s", secgrpId) + // for _, rule := range rules { + // if err := self.region.addSecurityGroupRule(secgrpId, &rule); err != nil { + // return "", err + // } + // } + // } else { + // //syncRules + // secgrpId = secgroup.SecurityGroupId + // log.Debugf("Sync Rules for %s", secgroup.GetName()) + // if secgroup.GetName() != name { + // if err := self.region.modifySecurityGroup(secgrpId, name, ""); err != nil { + // log.Errorf("Change SecurityGroup name to %s failed: %v", name, err) + // } + // } + // self.region.syncSecgroupRules(secgrpId, rules) + // } + return secgrpId, nil +} + +func (self *SVpc) addWire(wire *SWire) { + if self.iwires == nil { + self.iwires = make([]cloudprovider.ICloudWire, 0) + } + self.iwires = append(self.iwires, wire) +} diff --git a/pkg/util/qcloud/wire.go b/pkg/util/qcloud/wire.go new file mode 100644 index 0000000000..41e331429d --- /dev/null +++ b/pkg/util/qcloud/wire.go @@ -0,0 +1,142 @@ +package qcloud + +import ( + "fmt" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" +) + +type SWire struct { + zone *SZone + vpc *SVpc + + inetworks []cloudprovider.ICloudNetwork +} + +func (self *SWire) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SWire) GetId() string { + return fmt.Sprintf("%s-%s", self.vpc.GetId(), self.zone.GetId()) +} + +func (self *SWire) GetName() string { + return self.GetId() +} + +func (self *SWire) IsEmulated() bool { + return true +} + +func (self *SWire) GetStatus() string { + return "available" +} + +func (self *SWire) Refresh() error { + return nil +} + +func (self *SWire) GetGlobalId() string { + return fmt.Sprintf("%s-%s", self.vpc.GetGlobalId(), self.zone.GetGlobalId()) +} + +func (self *SWire) GetIVpc() cloudprovider.ICloudVpc { + return self.vpc +} + +func (self *SWire) GetIZone() cloudprovider.ICloudZone { + return self.zone +} + +func (self *SWire) GetBandwidth() int { + return 10000 +} + +func (self *SWire) GetNetworkById(networkId string) *SNetwork { + networks, err := self.GetINetworks() + if err != nil { + return nil + } + log.Debugf("search for networks %d", len(networks)) + for i := 0; i < len(networks); i += 1 { + log.Debugf("search %s", networks[i].GetName()) + network := networks[i].(*SNetwork) + if network.SubnetId == networkId { + return network + } + } + return nil +} + +func (self *SWire) CreateINetwork(name string, cidr string, desc string) (cloudprovider.ICloudNetwork, error) { + networkId, err := self.zone.region.CreateNetwork(self.zone.Zone, self.vpc.VpcId, name, cidr, desc) + if err != nil { + log.Errorf("CreateNetwork error %s", err) + return nil, err + } + self.inetworks = nil + network := self.GetNetworkById(networkId) + if network == nil { + log.Errorf("cannot find vswitch after create????") + return nil, cloudprovider.ErrNotFound + } + return network, nil +} + +func (self *SWire) GetINetworkById(netid string) (cloudprovider.ICloudNetwork, error) { + networks, err := self.GetINetworks() + if err != nil { + return nil, err + } + for i := 0; i < len(networks); i += 1 { + if networks[i].GetGlobalId() == netid { + return networks[i], nil + } + } + return nil, cloudprovider.ErrNotFound +} + +func (self *SWire) getNetworkById(networkId string) *SNetwork { + networks, err := self.GetINetworks() + if err != nil { + return nil + } + log.Debugf("search for networks %d", len(networks)) + for i := 0; i < len(networks); i += 1 { + log.Debugf("search %s", networks[i].GetName()) + network := networks[i].(*SNetwork) + if network.SubnetId == networkId { + return network + } + } + return nil +} + +func (self *SWire) GetINetworks() ([]cloudprovider.ICloudNetwork, error) { + if self.inetworks == nil { + err := self.vpc.fetchNetworks() + if err != nil { + return nil, err + } + } + return self.inetworks, nil +} + +func (self *SWire) addNetwork(network *SNetwork) { + if self.inetworks == nil { + self.inetworks = make([]cloudprovider.ICloudNetwork, 0) + } + find := false + for i := 0; i < len(self.inetworks); i += 1 { + if self.inetworks[i].GetId() == network.SubnetId { + find = true + break + } + } + if !find { + self.inetworks = append(self.inetworks, network) + } +} diff --git a/pkg/util/qcloud/zone.go b/pkg/util/qcloud/zone.go new file mode 100644 index 0000000000..95474fbde2 --- /dev/null +++ b/pkg/util/qcloud/zone.go @@ -0,0 +1,214 @@ +package qcloud + +import ( + "fmt" + "strings" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/pkg/utils" +) + +type InstanceChargeType string + +const ( + PrePaidInstanceChargeType InstanceChargeType = "PREPAID" + PostPaidInstanceChargeType InstanceChargeType = "POSTPAID_BY_HOUR" + CdhPaidInstanceChargeType InstanceChargeType = "CDHPAID" + DefaultInstanceChargeType = PostPaidInstanceChargeType +) + +type SZone struct { + region *SRegion + + iwires []cloudprovider.ICloudWire + + host *SHost + + istorages []cloudprovider.ICloudStorage + + instanceTypes []string + refreshTime time.Time + + Zone string + ZoneId string + ZoneName string + ZoneState string +} + +func (self *SZone) GetMetadata() *jsonutils.JSONDict { + return nil +} + +func (self *SZone) GetId() string { + return self.Zone +} + +func (self *SZone) GetName() string { + return fmt.Sprintf("%s %s", CLOUD_PROVIDER_QCLOUD_CN, self.ZoneName) +} + +func (self *SZone) GetGlobalId() string { + return fmt.Sprintf("%s/%s", self.region.GetGlobalId(), self.Zone) +} + +func (self *SZone) IsEmulated() bool { + return false +} + +func (self *SZone) Refresh() error { + // do nothing + return nil +} + +func (self *SZone) GetStatus() string { + if self.ZoneState == "AVAILABLE" { + return models.ZONE_ENABLE + } + return models.ZONE_SOLDOUT +} + +func (self *SZone) GetIHostById(id string) (cloudprovider.ICloudHost, error) { + host := self.getHost() + if host.GetGlobalId() == id { + return host, nil + } + return nil, cloudprovider.ErrNotFound +} + +func (self *SZone) GetIHosts() ([]cloudprovider.ICloudHost, error) { + return []cloudprovider.ICloudHost{self.getHost()}, nil +} + +func (self *SZone) getHost() *SHost { + if self.host == nil { + self.host = &SHost{zone: self} + } + return self.host +} + +func (self *SZone) GetIRegion() cloudprovider.ICloudRegion { + return self.region +} + +func (self *SZone) fetchStorages() error { + self.istorages = []cloudprovider.ICloudStorage{} + for _, storageType := range []string{"CLOUD_BASIC", "CLOUD_PREMIUM", "CLOUD_SSD"} { + storage := SStorage{zone: self, storageType: storageType} + self.istorages = append(self.istorages, &storage) + } + for _, localstorageType := range []string{"LOCAL_BASIC", "LOCAL_SSD"} { + storage := SLocalStorage{zone: self, storageType: localstorageType} + self.istorages = append(self.istorages, &storage) + } + return nil +} + +func (self *SZone) GetIStorages() ([]cloudprovider.ICloudStorage, error) { + if self.istorages == nil { + self.fetchStorages() + } + return self.istorages, nil +} + +func (self *SZone) getLocalStorageByCategory(category string) (*SLocalStorage, error) { + if utils.IsInStringArray(strings.ToLower(category), []string{"local_basic", "local_ssd"}) { + return &SLocalStorage{zone: self, storageType: strings.ToUpper(category)}, nil + } + return nil, fmt.Errorf("No such storage %s", category) +} + +func (self *SZone) validateStorageType(category string) error { + if utils.IsInStringArray(strings.ToLower(category), []string{"local_basic", "local_ssd", "cloud_basic", "cloud_ssd", "cloud_permium"}) { + return nil + } + return fmt.Errorf("No such storage %s", category) +} + +func (self *SZone) getStorageByCategory(category string) (*SStorage, error) { + storages, err := self.GetIStorages() + if err != nil { + return nil, err + } + for i := 0; i < len(storages); i++ { + if utils.IsInStringArray(storages[i].GetStorageType(), []string{"local_basic", "local_ssd"}) { + continue + //return &SStorage{zone: self, storageType: strings.ToUpper(storages[i].GetStorageType())}, nil + } + storage := storages[i].(*SStorage) + if storage.storageType == category { + return storage, nil + } + } + return nil, fmt.Errorf("No such storage %s", category) +} + +func (self *SZone) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) { + if self.istorages == nil { + self.fetchStorages() + } + for i := 0; i < len(self.istorages); i += 1 { + if self.istorages[i].GetGlobalId() == id { + return self.istorages[i], nil + } + } + return nil, cloudprovider.ErrNotFound +} + +func (self *SZone) addWire(wire *SWire) { + if self.iwires == nil { + self.iwires = make([]cloudprovider.ICloudWire, 0) + } + self.iwires = append(self.iwires, wire) +} + +func (self *SZone) GetIWires() ([]cloudprovider.ICloudWire, error) { + return self.iwires, nil +} + +func (self *SZone) getNetworkById(networkId string) *SNetwork { + log.Debugf("Search in wires %d", len(self.iwires)) + for i := 0; i < len(self.iwires); i += 1 { + log.Debugf("Search in wire %s", self.iwires[i].GetName()) + wire := self.iwires[i].(*SWire) + net := wire.getNetworkById(networkId) + if net != nil { + return net + } + } + return nil +} + +func (self *SZone) fetchInstanceTypes() { + self.instanceTypes = []string{} + params := map[string]string{} + params["Region"] = self.region.Region + params["Filters.0.Name"] = "zone" + params["Filters.0.Values.0"] = self.Zone + if body, err := self.region.cvmRequest("DescribeInstanceTypeConfigs", params); err != nil { + log.Errorf("DescribeInstanceTypeConfigs error: %v", err) + } else if configSet, err := body.GetArray("InstanceTypeConfigSet"); err != nil { + log.Errorf("Get InstanceTypeConfigSet error: %v", err) + } else { + for _, config := range configSet { + if instanceType, err := config.GetString("InstanceType"); err == nil && !utils.IsInStringArray(instanceType, self.instanceTypes) { + self.instanceTypes = append(self.instanceTypes, instanceType) + } + } + self.refreshTime = time.Now() + } +} + +func (self *SZone) getAvaliableInstanceTypes() []string { + if self.instanceTypes == nil || len(self.instanceTypes) == 0 || time.Now().Sub(self.refreshTime).Hours() > refreshHours() { + self.fetchInstanceTypes() + } + return self.instanceTypes +} + +func refreshHours() float64 { + return 5 +} diff --git a/vendor/github.com/forhappy/cos-go-sdk/LICENSE b/vendor/github.com/forhappy/cos-go-sdk/LICENSE new file mode 100644 index 0000000000..8dada3edaf --- /dev/null +++ b/vendor/github.com/forhappy/cos-go-sdk/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/vendor/github.com/forhappy/cos-go-sdk/auth/auth.go b/vendor/github.com/forhappy/cos-go-sdk/auth/auth.go new file mode 100644 index 0000000000..397e087015 --- /dev/null +++ b/vendor/github.com/forhappy/cos-go-sdk/auth/auth.go @@ -0,0 +1,99 @@ +package auth + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "fmt" +) + +type Signer interface { + Sign(secretKey string) (string, error) + SignOnce(secretKey string) (string, error) +} + +// 生成签名, 腾讯移动服务通过签名来验证请求的合法性, +// 开发者通过将签名授权给客户端, 使其具备上传下载及管理指定资源的能力, +// 签名分为多次有效签名和单次有效签名. +// 生成签名所需信息包括项目 ID(AppId),空间名称(Bucket,文件资源的组织管理单元),项目的 Secret ID 和 Secret Key, +// 获取这些信息的方法如下: +// 1) 登录 云对象存储, 进入云对象存储空间; +// 2) 如开发者未创建空间,可添加空间,空间名称(Bucket)由用户自行输入 +// 3) 点击“获取secretKey”,获取 Appid,Secret ID 和 Secret Key +type Signature struct { + AppId string + Bucket string + SecretId string + ExpiredTime string + CurrentTime string + Rand string + FileId string +} + +// 构造签名类, 可使用该类的 Sign 和 SignOnce 接口分别进行多次有效签名和单次有效签名 +func NewSignature(appId, bucket, secretId, expiredTime, currentTime, rand, fileId string) *Signature { + return &Signature{ + AppId: appId, + Bucket: bucket, + SecretId: secretId, + ExpiredTime: expiredTime, + CurrentTime: currentTime, + Rand: rand, + FileId: fileId, + } +} + +// 多次有效签名, secretKey 为项目的 Secret Key +func (s *Signature) Sign(secretKey string) string { + stringToSign := fmt.Sprintf("a=%s&k=%s&e=%s&t=%s&r=%s&f=%s&b=%s", + s.AppId, + s.SecretId, + s.ExpiredTime, + s.CurrentTime, + s.Rand, + "", + s.Bucket, + ) + + hmacSha1 := hmac.New(sha1.New, []byte(secretKey)) + hmacSha1.Write([]byte(stringToSign)) + bytesSign := hmacSha1.Sum(nil) + bytesSign = append(bytesSign, []byte(stringToSign)...) + signature := base64.StdEncoding.EncodeToString(bytesSign) + return signature +} + +// 单次有效签名, secretKey 为项目的 Secret Key +func (s *Signature) SignOnce(secretKey string) string { + stringToSign := fmt.Sprintf("a=%s&k=%s&e=%s&t=%s&r=%s&f=%s&b=%s", + s.AppId, + s.SecretId, + "0", + s.CurrentTime, + s.Rand, + s.FileId, + s.Bucket, + ) + + hmacSha1 := hmac.New(sha1.New, []byte(secretKey)) + hmacSha1.Write([]byte(stringToSign)) + bytesSign := hmacSha1.Sum(nil) + bytesSign = append(bytesSign, []byte(stringToSign)...) + signature := base64.StdEncoding.EncodeToString(bytesSign) + return signature +} + +// 字符串签名类 +func (s *Signature) String() string { + str := fmt.Sprintf("a=%s&b=%s&k=%s&e=%s&t=%s&r=%s&f=%s", + s.AppId, + s.SecretId, + s.ExpiredTime, + s.CurrentTime, + s.Rand, + s.FileId, + s.Bucket, + ) + + return str +} diff --git a/vendor/github.com/forhappy/cos-go-sdk/http/LICENSE b/vendor/github.com/forhappy/cos-go-sdk/http/LICENSE new file mode 100644 index 0000000000..b940dd44c9 --- /dev/null +++ b/vendor/github.com/forhappy/cos-go-sdk/http/LICENSE @@ -0,0 +1,24 @@ +The MIT License (MIT) + +Copyright (c) 2013 Jonathan Leibiusky and Marcos Lilljedahl +https://github.com/franela/goreq + +Copyright (c) 2016 Haiping Fu +https://github.com/forhappy/goreq + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/auth.go b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/auth.go new file mode 100644 index 0000000000..c560caa2c6 --- /dev/null +++ b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/auth.go @@ -0,0 +1,128 @@ +package cos + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/hex" + "fmt" + "net/http" + "sort" + "strconv" + "strings" + "time" +) + +func (conn *Conn) signHeader(req *http.Request, params map[string]interface{}, headers map[string]string) { + signTime := getSignTime() + signature := conn.getSignature(req, params, headers, signTime) + authStr := fmt.Sprintf("q-sign-algorithm=sha1&q-ak=%s&q-sign-time=%s&q-key-time=%s&q-header-list=%s&q-url-param-list=%s&q-signature=%s", + conn.conf.SecretID, signTime, signTime, getHeadKeys(headers), getParamKeys(params), signature) + + req.Header.Set("Authorization", authStr) +} + +func getSignTime() string { + now := time.Now() + expired := now.Add(time.Second * 1800) + return fmt.Sprintf("%d;%d", now.Unix(), expired.Unix()) +} + +func getHeadKeys(headers map[string]string) string { + if headers == nil || len(headers) == 0 { + return "" + } + + tmp := []string{} + for k := range headers { + tmp = append(tmp, strings.ToLower(k)) + } + sort.Strings(tmp) + + return strings.Join(tmp, ";") +} + +func getParamKeys(params map[string]interface{}) string { + if params == nil || len(params) == 0 { + return "" + } + + tmp := []string{} + for k := range params { + tmp = append(tmp, strings.ToLower(k)) + } + sort.Strings(tmp) + + return strings.Join(tmp, ";") +} + +func (conn *Conn) getSignature(req *http.Request, params map[string]interface{}, headers map[string]string, signTime string) string { + httpString := fmt.Sprintf("%s\n%s\n%s\n%s\n", strings.ToLower(req.Method), + req.URL.Path, getParamStr(params), getHeadStr(headers)) + + httpString = sha(httpString) + signKey := hmacSha(conn.conf.SecretKey, signTime) + signStr := fmt.Sprintf("sha1\n%s\n%s\n", signTime, httpString) + + return hmacSha(signKey, signStr) +} + +func interfaceToString(i interface{}) string { + switch x := i.(type) { + case string: + return x + case int: + return strconv.Itoa(x) + case int64: + return strconv.FormatInt(x, 10) + case uint64: + return strconv.FormatUint(x, 10) + default: + return "" + } +} + +func getParamStr(params map[string]interface{}) string { + if params == nil || len(params) == 0 { + return "" + } + + tmp := []string{} + for k, v := range params { + str := strings.ToLower(fmt.Sprintf("%s=%s", k, interfaceToString(v))) + tmp = append(tmp, str) + } + sort.Strings(tmp) + + return strings.Join(tmp, "&") +} + +func getHeadStr(headers map[string]string) string { + if headers == nil || len(headers) == 0 { + return "" + } + + tmp := []string{} + for k, v := range headers { + str := fmt.Sprintf("%s=%s", strings.ToLower(k), escape(v)) + tmp = append(tmp, str) + } + sort.Strings(tmp) + + return strings.Join(tmp, "&") +} + +func sha(s string) string { + sha := sha1.New() + sha.Write([]byte(s)) + b := sha.Sum(nil) + + return hex.EncodeToString(b) +} + +func hmacSha(k, s string) string { + enc := hmac.New(sha1.New, []byte(k)) + enc.Write([]byte(s)) + b := enc.Sum(nil) + + return hex.EncodeToString(b) +} diff --git a/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/bucket.go b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/bucket.go new file mode 100644 index 0000000000..3458f2fb30 --- /dev/null +++ b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/bucket.go @@ -0,0 +1,315 @@ +package cos + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/hex" + "encoding/xml" + "fmt" + "io" + "net/http" + "os" + "strings" +) + +// Bucket bucket +type Bucket struct { + Name string + conn *Conn +} + +// ObjectSlice object slice +type ObjectSlice struct { + UploadID string + Size int64 + Offset int64 + Number int + MD5 string + Dst string + Result bool +} + +// 获得云存储上文件信息 +func (b *Bucket) HeadObject(ctx context.Context, object string) error { + resq, err := b.conn.Do(ctx, http.MethodHead, b.Name, object, nil, nil, nil) + if err == nil { + defer resq.Body.Close() + } else { + for k, v := range resq.Header { + value := fmt.Sprintf("%s", v) + fmt.Printf("%-18s: %s\n", k, strings.Replace(strings.Replace(value, "[", "", -1), "]", "", -1)) + } + } + + return err +} + +func (b *Bucket) UploadObject(ctx context.Context, object string, content io.Reader, acl *AccessControl) error { + res, err := b.conn.Do(ctx, http.MethodPut, b.Name, object, nil, acl.GenHead(), content) + if err == nil { + defer res.Body.Close() + } + + return err +} + +func (b *Bucket) CopyObject(ctx context.Context, src, dst string, acl *AccessControl) error { + srcURL := fmt.Sprintf("%s-%s.cos.%s.%s/%s", b.Name, b.conn.conf.AppID, b.conn.conf.Region, b.conn.conf.Domain, dst) + header := map[string]string{ + "x-cos-source-url": srcURL, + } + + res, err := b.conn.Do(ctx, http.MethodPut, b.Name, dst, nil, header, nil) + if err == nil { + defer res.Body.Close() + } + + return err +} + +func (b *Bucket) DeleteObject(ctx context.Context, obj string) error { + res, err := b.conn.Do(ctx, http.MethodDelete, b.Name, obj, nil, nil, nil) + if err == nil { + defer res.Body.Close() + } + + return err +} + +func (b *Bucket) DownloadObject(ctx context.Context, object string, w io.Writer) error { + res, err := b.conn.Do(ctx, http.MethodGet, b.Name, object, nil, nil, nil) + if err != nil { + return err + } + + _, err = io.Copy(w, res.Body) + + return err +} + +// UploadObjectBySlice upload by slice +func (b *Bucket) UploadObjectBySlice(ctx context.Context, dst, src string, taskNum int, headers map[string]string) error { + if taskNum < 1 { + return ParamError{"taskNum 必须大于1"} + } + + uploadID, err := b.InitSliceUpload(ctx, dst, headers) + if err != nil { + return err + } + + fd, err := os.Open(src) + if err != nil { + return err + } + defer fd.Close() + + slices, err := b.PerformSliceUpload(ctx, dst, uploadID, fd, taskNum) + if err != nil { + return err + } + + err = b.CompleteSliceUpload(ctx, dst, uploadID, fd, slices) + + return err +} + +// InitSliceUpload init upload by slice +func (b *Bucket) InitSliceUpload(ctx context.Context, obj string, headers map[string]string) (string, error) { + param := map[string]interface{}{ + "uploads": "", + } + res, err := b.conn.Do(ctx, http.MethodPost, b.Name, obj, param, headers, nil) + if err != nil { + return "", err + } + defer res.Body.Close() + + imur := &InitiateMultipartUploadResult{} + err = XMLDecode(res.Body, imur) + if err != nil { + return "", err + } + + return imur.UploadID, nil +} + +// CompleteSliceUpload finish slice Upload +func (b *Bucket) CompleteSliceUpload(ctx context.Context, dst, uploadID string, fd *os.File, slice []*ObjectSlice) error { + cmu := &CompleteMultipartUpload{} + cmu.Part = []struct { + PartNumber int + ETag string + }{} + + for _, osl := range slice { + cmu.Part = append(cmu.Part, struct { + PartNumber int + ETag string + }{PartNumber: osl.Number, ETag: osl.MD5}) + } + + cmuXML, err := xml.Marshal(cmu) + if err != nil { + return err + } + param := map[string]interface{}{ + "uploadId": uploadID, + } + res, err := b.conn.Do(ctx, http.MethodPost, b.Name, dst, param, nil, bytes.NewReader(cmuXML)) + if err == nil { + defer res.Body.Close() + } + + return err +} + +// PerformSliceUpload perform slice upload +func (b *Bucket) PerformSliceUpload(ctx context.Context, dst, uploadID string, fd *os.File, taskNum int) ([]*ObjectSlice, error) { + oss, err := b.getFileSlices(fd, uploadID, dst) + if err != nil { + return nil, err + } + + jobNum := len(oss) + jobs := make(chan *ObjectSlice, jobNum) + result := make(chan *ObjectSlice, jobNum) + + for i := 0; i < taskNum; i++ { + go b.Worker(ctx, fd, jobs, result) + } + + for _, osl := range oss { + jobs <- osl + } + close(jobs) + + for i := 0; i < jobNum; i++ { + res := <-result + if !res.Result { + return nil, SliceError{fmt.Sprintf("part info : num:%d, md5:%s", res.Number, res.MD5)} + } + } + + return oss, nil +} + +// Worker woker for slice upload +func (b *Bucket) Worker(ctx context.Context, fd *os.File, jobs <-chan *ObjectSlice, result chan<- *ObjectSlice) { + for job := range jobs { + content, err := getFilePartContent(fd, job.Offset, job.Size) + if err != nil { + continue + } + + err = b.UploadSlice(ctx, job.UploadID, job.Dst, job.Number, job.MD5, content) + if err == nil { + job.Result = true + } else { + job.Result = false + } + + result <- job + } +} + +// UploadSlice upload one slice +func (b *Bucket) UploadSlice(ctx context.Context, uploadID, dst string, number int, etag string, content io.Reader) error { + param := map[string]interface{}{ + "PartNumber": number, + "uploadId": uploadID, + } + res, err := b.conn.Do(ctx, http.MethodPut, b.Name, dst, param, nil, content) + + if err != nil { + return FileError{"PUT数据错误:" + err.Error()} + } + defer res.Body.Close() + + if strings.Trim(res.Header.Get("Etag"), "\"") != etag { + return FileError{"cos-etag与文件MD5不匹配"} + } + + return err +} + +func (b *Bucket) getFileSlices(fd *os.File, uploadID, dst string) ([]*ObjectSlice, error) { + sliceSize := b.conn.conf.PartSize + fi, err := fd.Stat() + if err != nil { + return nil, err + } + + fileSize := fi.Size() + oss := []*ObjectSlice{} + var i int + var offset int64 + for fileSize > 0 { + var size int64 + if fileSize > sliceSize { + size = sliceSize + } else { + size = fileSize + } + i++ + md5, err := getFileMD5(fd, offset, size) + if err != nil { + return nil, err + } + + osl := &ObjectSlice{} + osl.Size = size + osl.Number = i + osl.Offset = offset + osl.UploadID = uploadID + osl.MD5 = md5 + osl.Dst = dst + oss = append(oss, osl) + + fileSize -= sliceSize + offset += sliceSize + } + + return oss, nil +} + +func getFileMD5(fd *os.File, offset, size int64) (string, error) { + buf := make([]byte, size) + _, err := fd.ReadAt(buf, offset) + if err != nil { + return "", err + } + + encoder := md5.New() + encoder.Write(buf) + b := encoder.Sum(nil) + + return hex.EncodeToString(b), nil +} + +func getFilePartContent(fd *os.File, offset, size int64) (io.Reader, error) { + buf := make([]byte, size) + _, err := fd.ReadAt(buf, offset) + if err != nil { + return nil, err + } + + return bytes.NewReader(buf), nil +} + +func (b *Bucket) AbortUpload(ctx context.Context, obj, uploadID string) error { + param := map[string]interface{}{ + "uploadId": uploadID, + } + _, err := b.conn.Do(ctx, http.MethodDelete, b.Name, obj, param, nil, nil) + + return err +} + +// ObjectExists object exists +func (b *Bucket) ObjectExists(ctx context.Context, obj string) error { + _, err := b.conn.Do(ctx, http.MethodHead, b.Name, obj, nil, nil, nil) + + return err +} diff --git a/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/client.go b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/client.go new file mode 100644 index 0000000000..18beb021b5 --- /dev/null +++ b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/client.go @@ -0,0 +1,164 @@ +package cos + +import ( + "context" + "net/http" + "time" +) + +// Client 客户端, cos的句柄 +type Client struct { + conn *Conn +} + +// New cos包的入口 +func New(o *Option) *Client { + client := Client{} + conf := getDefaultConf() + conf.AppID = o.AppID + conf.SecretID = o.SecretID + conf.SecretKey = o.SecretKey + conf.Region = o.Region + + if o.Domain != "" { + conf.Domain = o.Domain + } + + conn := Conn{&http.Client{}, conf} + client.conn = &conn + + return &client +} + +// GetTimeoutCtx 获取一个带超时的context +func GetTimeoutCtx(timeout time.Duration) context.Context { + ctx, _ := context.WithTimeout(context.Background(), timeout) + + return ctx +} + +// Bucket get bucket +func (c *Client) Bucket(name string) *Bucket { + return &Bucket{name, c.conn} +} + +// GetBucketList 获取bucketlist +func (c *Client) GetBucketList(ctx context.Context) (*ListAllMyBucketsResult, error) { + req, err := http.NewRequest(http.MethodGet, "http://service.cos.myqcloud.com/", nil) + if err != nil { + return nil, err + } + + req = req.WithContext(ctx) + c.conn.signHeader(req, nil, nil) + res, err := c.conn.c.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + res, err = checkHTTPErr(res) + if err != nil { + return nil, err + } + + labr := &ListAllMyBucketsResult{} + err = XMLDecode(res.Body, labr) + if err != nil { + return nil, err + } + + return labr, err +} + +// CreateBucket 建立bucket +func (c *Client) CreateBucket(ctx context.Context, name string, acl *AccessControl) error { + res, err := c.conn.Do(ctx, http.MethodPut, name, "", nil, acl.GenHead(), nil) + if err == nil { + defer res.Body.Close() + } + + return err +} + +// DeleteBucket delete a bucket +func (c *Client) DeleteBucket(ctx context.Context, name string) error { + _, err := c.conn.Do(ctx, http.MethodDelete, name, "", nil, nil, nil) + + return err +} + +// GetBucketACL get bucket's acl +func (c *Client) GetBucketACL(ctx context.Context, name string) (*AccessControlPolicy, error) { + params := map[string]interface{}{"acl": ""} + res, err := c.conn.Do(ctx, http.MethodGet, name, "", params, nil, nil) + + if err != nil { + return nil, err + } + defer res.Body.Close() + + aclp := &AccessControlPolicy{} + + err = XMLDecode(res.Body, aclp) + if err != nil { + return nil, err + } + + return aclp, nil +} + +// SetBucketACL set bucket's acl +func (c *Client) SetBucketACL(ctx context.Context, name string, acl *AccessControl) error { + params := map[string]interface{}{"acl": ""} + res, err := c.conn.Do(ctx, http.MethodPut, name, "", params, acl.GenHead(), nil) + if err == nil { + defer res.Body.Close() + } + + return err +} + +// BucketExists bucket exists? +func (c *Client) BucketExists(ctx context.Context, name string) error { + res, err := c.conn.Do(ctx, http.MethodHead, name, "", nil, nil, nil) + if err == nil { + defer res.Body.Close() + } + + return err +} + +// ListBucketContents list +func (c *Client) ListBucketContents(ctx context.Context, name string, qc *QueryCondition) (*ListBucketResult, error) { + resp, err := c.conn.Do(ctx, http.MethodGet, name, "", qc.GenParams(), nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + lbr := &ListBucketResult{} + err = XMLDecode(resp.Body, lbr) + if err != nil { + return nil, err + } + + return lbr, nil +} + +// ListUploading list uploading task +func (c *Client) ListUploading(ctx context.Context, bucket string, lu *ListUploadParam) (*ListMultipartUploadsResult, error) { + res, err := c.conn.Do(ctx, http.MethodGet, bucket, "", lu.GenParams(), nil, nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + + lmur := &ListMultipartUploadsResult{} + err = XMLDecode(res.Body, lmur) + if err != nil { + return nil, err + } + + return lmur, nil +} diff --git a/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/conf.go b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/conf.go new file mode 100644 index 0000000000..516c013164 --- /dev/null +++ b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/conf.go @@ -0,0 +1,31 @@ +package cos + +const ( + defaultPartSize = 80 * 1024 * 1024 + defaultRetryTimes = 3 + defaultUA = "cos-go-sdk-v5.2.9" + defaultDomain = "myqcloud.com" +) + +// Conf config struct +type Conf struct { + AppID string + SecretID string + SecretKey string + Region string + PartSize int64 + RetryTimes int + UA string + Domain string + Bucket string +} + +func getDefaultConf() *Conf { + conf := Conf{} + conf.PartSize = defaultPartSize + conf.RetryTimes = defaultRetryTimes + conf.UA = defaultUA + conf.Domain = defaultDomain + + return &conf +} diff --git a/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/conn.go b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/conn.go new file mode 100644 index 0000000000..bf6380ac0e --- /dev/null +++ b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/conn.go @@ -0,0 +1,141 @@ +package cos + +import ( + "bytes" + "context" + "encoding/xml" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +// Conn http 请求类 +type Conn struct { + c *http.Client + conf *Conf +} + +func (conn *Conn) Do(ctx context.Context, method, bucket, object string, params map[string]interface{}, headers map[string]string, body io.Reader) (*http.Response, error) { + queryStr := getQueryStr(params) + url := conn.buildURL(bucket, object, queryStr) + + switch body.(type) { + case *bytes.Buffer, *bytes.Reader, *strings.Reader: + default: + if body != nil { + b, err := ioutil.ReadAll(body) + if err != nil { + return nil, err + } + body = bytes.NewReader(b) + } + } + + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + + conn.signHeader(req, params, headers) + req.Header.Set("User-Agent", conn.conf.UA) + setHeader(req, headers) + + res, err := conn.c.Do(req) + + if err != nil { + return nil, err + } + + if res.StatusCode < 200 || res.StatusCode >= 300 { + defer res.Body.Close() + return checkHTTPErr(res) + } + + return res, nil +} + +func getQueryStr(params map[string]interface{}) string { + if params == nil || len(params) == 0 { + return "" + } + + buf := new(bytes.Buffer) + buf.WriteString("?") + for k, v := range params { + buf.WriteString(k) + vs := interfaceToString(v) + if vs == "" { + buf.WriteString("&") + continue + } + buf.WriteString("=") + buf.WriteString(vs) + buf.WriteString("&") + } + + return strings.Trim(buf.String(), "&") +} + +func (conn *Conn) buildURL(bucket, object, queryStr string) string { + domain := fmt.Sprintf("%s-%s.cos.%s.%s", bucket, conn.conf.AppID, conn.conf.Region, conn.conf.Domain) + url := fmt.Sprintf("http://%s/%s%s", domain, escape(object), queryStr) + + return url +} + +func escape(str string) string { + //go语言中将空格编码为+,需要改为%20 + return strings.Replace(url.QueryEscape(str), "+", "%20", -1) +} + +func setHeader(req *http.Request, headers map[string]string) { + if headers == nil { + return + } + + for k, v := range headers { + req.Header.Set(k, v) + } +} + +func checkHTTPErr(res *http.Response) (*http.Response, error) { + if res.StatusCode >= 200 && res.StatusCode < 300 { + return res, nil + } + + err := HTTPError{} + err.Code = res.StatusCode + if res.StatusCode >= 300 && res.StatusCode < 400 { + err.Message = "资源被重定向" + } + + if res.StatusCode >= 400 && res.StatusCode < 500 { + err.Message = "请求被拒绝" + } + + if res.StatusCode >= 500 { + err.Message = "cos服务器错误" + } + + if res.ContentLength > 0 { + resErr := &Error{} + e := XMLDecode(res.Body, resErr) + if e != nil { + return nil, err + } + err.Message += resErr.Message + } + + return res, err +} + +// XMLDecode xml解析方法 +func XMLDecode(r io.Reader, i interface{}) error { + jd := xml.NewDecoder(r) + err := jd.Decode(i) + + return err +} diff --git a/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/option.go b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/option.go new file mode 100644 index 0000000000..d6bc8af99b --- /dev/null +++ b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/option.go @@ -0,0 +1,10 @@ +package cos + +type Option struct { + AppID string `mapstructure:"app_id" json:"app_id"` + SecretID string `mapstructure:"secret_id" json:"secret_id"` + SecretKey string `mapstructure:"secret_key" json:"secret_key"` + Region string `mapstructure:"region" json:"region"` + Domain string `mapstructure:"domain" json:"domain"` + Bucket string `mapstructure:"bucket" json:"bucket"` +} diff --git a/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/requests.go b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/requests.go new file mode 100644 index 0000000000..e26c6757e9 --- /dev/null +++ b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/requests.go @@ -0,0 +1,102 @@ +package cos + +// AccessControl privilige +type AccessControl struct { + ACL string + GrantRead string + GrantWrite string + FullControl string +} + +// GenHead 生成http head +func (acl *AccessControl) GenHead() map[string]string { + header := map[string]string{ + "x-cos-acl": acl.ACL, + "x-cos-grant-read": acl.GrantRead, + "x-cos-grant-write": acl.GrantWrite, + "x-cos-grant-full-control": acl.FullControl, + } + + for k, v := range header { + if v == "" { + delete(header, k) + } + } + + return header +} + +// QueryCondition query condition +type QueryCondition struct { + Prefix string + Delimiter string + EncodingType string + Marker string + MaxKeys int +} + +// GenParams generate params:map[string]interface{} +func (qc *QueryCondition) GenParams() map[string]interface{} { + params := map[string]interface{}{ + "prefix": qc.Prefix, + "delimiter": qc.Delimiter, + "encoding-type": qc.EncodingType, + "marker": qc.Marker, + "max-keys": qc.MaxKeys, + } + + for k, v := range params { + if v == "" { + delete(params, k) + } + if v == 0 { + delete(params, k) + } + } + + return params +} + +// ListUploadParam list upload param +type ListUploadParam struct { + Prefix string + Delimiter string + EncodingType string + MaxUploads int + KeyMarker string + UploadIDMarker string +} + +// GenParams generate params for request +func (lup *ListUploadParam) GenParams() map[string]interface{} { + params := map[string]interface{}{ + "prefix": lup.Prefix, + "delimiter": lup.Delimiter, + "encoding-type": lup.EncodingType, + "max-uploads": lup.MaxUploads, + "key-marker": lup.KeyMarker, + "upload-id-marker": lup.UploadIDMarker, + } + + for k, v := range params { + if v == "" { + delete(params, k) + } + + if v == 0 { + delete(params, k) + } + } + params["uploads"] = "" + + return params + +} + +// CompleteMultipartUpload compelete slice upload +type CompleteMultipartUpload struct { + Part []struct { + PartNumber int + ETag string + } +} diff --git a/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/response.go b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/response.go new file mode 100644 index 0000000000..8c69ab30cd --- /dev/null +++ b/vendor/github.com/nelsonken/cos-go-sdk-v5/cos/response.go @@ -0,0 +1,156 @@ +package cos + +import ( + "fmt" +) + +// ListAllMyBucketsResult 获取bucket列表的结果 +type ListAllMyBucketsResult struct { + Owner struct { + ID string + DisplayName string + } + + Buckets struct { + Bucket []struct { + Name string + Location string + CreateDate string + } + } +} + +// Error 错误消息 +type Error struct { + Code string + Message string + Resource string + RequestID string `xml:"RequestId"` + TraceID string `xml:"TaceId"` +} + +// HTTPError http error struct +type HTTPError struct { + Code int + Message string +} + +// Error error interface +func (he HTTPError) Error() string { + return fmt.Sprintf("%d:%s", he.Code, he.Message) +} + +// AccessControlPolicy acl return +type AccessControlPolicy struct { + Owner struct { + ID string + DisplayName string + } + AccessControlList struct { + Grant []struct { + Grantee struct { + ID string + DisplayName string + } + Permission string + } + } +} + +// ListBucketResult list bucket contents result +type ListBucketResult struct { + Name string + EncodingType string `xml:"Encoding-Type"` + Prefix string + Marker string + MaxKeys int + IsTruncated bool + NextMarker string + Contents []struct { + Key string + LastModified string + ETag string + Size int64 + Owner struct { + ID string + } + StorageClass string + } + CommonPrefixes []struct { + Prefix string + } +} + +// ListMultipartUploadsResult list uploading task +type ListMultipartUploadsResult struct { + Bucket string + EncodingType string `xml:"Encoding-Type"` + KeyMarker string + UploadIDMarker string `xml:"UploadIdMarker"` + NextKeyMarker string + NextUploadIDMarker string `xml:"NextUploadIdMarker"` + MaxUploads int + IsTruncated bool + Prefix string + Delimiter string + Upload []struct { + Key string + UploadID string + StorageClass string + Initiator struct { + UIN string + } + Owner struct { + UID string + } + Initiated string + } + CommonPrefixes []struct { + Prefix string + } +} + +// InitiateMultipartUploadResult init slice upload +type InitiateMultipartUploadResult struct { + Bucket string + Key string + UploadID string `xml:"UploadId"` +} + +// CompleteMultipartUploadResult compeleted slice upload +type CompleteMultipartUploadResult struct { + Location string + Bucket string + Key string + ETag string +} + +// SliceError slice upload err +type SliceError struct { + Message string +} + +// Error implements error +func (se SliceError) Error() string { + return fmt.Sprintf("上传分片失败:%s", se.Message) +} + +// ParamError slice upload err +type ParamError struct { + Message string +} + +// Error implements error +func (pe ParamError) Error() string { + return fmt.Sprintf("参数错误:%s", pe.Message) +} + +// FileError slice upload err +type FileError struct { + Message string +} + +// Error implements error +func (fe FileError) Error() string { + return fmt.Sprintf("文件错误:%s", fe.Message) +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/LICENSE b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/LICENSE new file mode 100644 index 0000000000..efc75a2253 --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2017-2018 Tencent Ltd. + + 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. diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/client.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/client.go new file mode 100644 index 0000000000..78f3ce7c72 --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/client.go @@ -0,0 +1,88 @@ +package common + +import ( + "log" + "net/http" + "time" + + tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" +) + +type Client struct { + region string + httpClient *http.Client + httpProfile *profile.HttpProfile + credential *Credential + signMethod string + debug bool +} + +func (c *Client) Send(request tchttp.Request, response tchttp.Response) (err error) { + if request.GetDomain() == "" { + domain := c.httpProfile.Endpoint + if domain == "" { + domain = tchttp.GetServiceDomain(request.GetService()) + } + request.SetDomain(domain) + } + err = tchttp.ConstructParams(request) + if err != nil { + return + } + tchttp.CompleteCommonParams(request, c.GetRegion()) + err = signRequest(request, c.credential, c.signMethod) + if err != nil { + return + } + httpRequest, err := http.NewRequest(request.GetHttpMethod(), request.GetUrl(), request.GetBodyReader()) + if err != nil { + return + } + if request.GetHttpMethod() == "POST" { + httpRequest.Header["Content-Type"] = []string{"application/x-www-form-urlencoded"} + } + //log.Printf("[DEBUG] http request=%v", httpRequest) + httpResponse, err := c.httpClient.Do(httpRequest) + if err != nil { + return err + } + err = tchttp.ParseFromHttpResponse(httpResponse, response) + return +} + +func (c *Client) GetRegion() string { + return c.region +} + +func (c *Client) Init(region string) *Client { + c.httpClient = &http.Client{} + c.region = region + c.signMethod = "HmacSHA256" + c.debug = false + log.SetFlags(log.LstdFlags | log.Lshortfile) + return c +} + +func (c *Client) WithSecretId(secretId, secretKey string) *Client { + c.credential = NewCredential(secretId, secretKey) + return c +} + +func (c *Client) WithProfile(clientProfile *profile.ClientProfile) *Client { + c.signMethod = clientProfile.SignMethod + c.httpProfile = clientProfile.HttpProfile + c.httpClient.Timeout = time.Duration(c.httpProfile.ReqTimeout) * time.Second + return c +} + +func (c *Client) WithSignatureMethod(method string) *Client { + c.signMethod = method + return c +} + +func NewClientWithSecretId(secretId, secretKey, region string) (client *Client, err error) { + client = &Client{} + client.Init(region).WithSecretId(secretId, secretKey) + return +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/credentials.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/credentials.go new file mode 100644 index 0000000000..19f0722de8 --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/credentials.go @@ -0,0 +1,40 @@ +package common + +type Credential struct { + SecretId string + SecretKey string +} + +func NewCredential(secretId, secretKey string) *Credential { + return &Credential{ + SecretId: secretId, + SecretKey: secretKey, + } +} + +func (c *Credential) GetCredentialParams() map[string]string { + return map[string]string{ + "SecretId": c.SecretId, + } +} + +type TokenCredential struct { + SecretId string + SecretKey string + Token string +} + +func NewTokenCredential(secretId, secretKey, token string) *TokenCredential { + return &TokenCredential{ + SecretId: secretId, + SecretKey: secretKey, + Token: token, + } +} + +func (c *TokenCredential) GetCredentialParams() map[string]string { + return map[string]string{ + "SecretId": c.SecretId, + "Token": c.Token, + } +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors/errors.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors/errors.go new file mode 100644 index 0000000000..27589e59a1 --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors/errors.go @@ -0,0 +1,35 @@ +package errors + +import ( + "fmt" +) + +type TencentCloudSDKError struct { + Code string + Message string + RequestId string +} + +func (e *TencentCloudSDKError) Error() string { + return fmt.Sprintf("[TencentCloudSDKError] Code=%s, Message=%s, RequestId=%s", e.Code, e.Message, e.RequestId) +} + +func NewTencentCloudSDKError(code, message, requestId string) error { + return &TencentCloudSDKError{ + Code: code, + Message: message, + RequestId: requestId, + } +} + +func (e *TencentCloudSDKError) GetCode() string { + return e.Code +} + +func (e *TencentCloudSDKError) GetMessage() string { + return e.Message +} + +func (e *TencentCloudSDKError) GetRequestId() string { + return e.RequestId +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/request.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/request.go new file mode 100644 index 0000000000..c7a068556a --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/request.go @@ -0,0 +1,231 @@ +package common + +import ( + "io" + //"log" + "math/rand" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +const ( + POST = "POST" + GET = "GET" + + RootDomain = "tencentcloudapi.com" + Path = "/" +) + +type Request interface { + GetAction() string + GetBodyReader() io.Reader + GetDomain() string + GetHttpMethod() string + GetParams() map[string]string + GetPath() string + GetService() string + GetUrl() string + GetVersion() string + SetDomain(string) + SetHttpMethod(string) +} + +type BaseRequest struct { + httpMethod string + domain string + path string + params map[string]string + formParams map[string]string + + service string + version string + action string +} + +func (r *BaseRequest) GetAction() string { + return r.action +} + +func (r *BaseRequest) GetHttpMethod() string { + return r.httpMethod +} + +func (r *BaseRequest) GetParams() map[string]string { + return r.params +} + +func (r *BaseRequest) GetPath() string { + return r.path +} + +func (r *BaseRequest) GetDomain() string { + return r.domain +} + +func (r *BaseRequest) SetDomain(domain string) { + r.domain = domain +} + +func (r *BaseRequest) SetHttpMethod(method string) { + switch strings.ToUpper(method) { + case POST: + { + r.httpMethod = POST + } + case GET: + { + r.httpMethod = GET + } + default: + { + r.httpMethod = GET + } + } +} + +func (r *BaseRequest) GetService() string { + return r.service +} + +func (r *BaseRequest) GetUrl() string { + if r.httpMethod == GET { + return "https://" + r.domain + r.path + "?" + getUrlQueriesEncoded(r.params) + } else if r.httpMethod == POST { + return "https://" + r.domain + r.path + } else { + return "" + } +} + +func (r *BaseRequest) GetVersion() string { + return r.version +} + +func getUrlQueriesEncoded(params map[string]string) string { + values := url.Values{} + for key, value := range params { + if value != "" { + values.Add(key, value) + } + } + return values.Encode() +} + +func (r *BaseRequest) GetBodyReader() io.Reader { + if r.httpMethod == POST { + s := getUrlQueriesEncoded(r.params) + //log.Printf("[DEBUG] body: %s", s) + return strings.NewReader(s) + } else { + return strings.NewReader("") + } +} + +func (r *BaseRequest) Init() *BaseRequest { + r.httpMethod = GET + r.domain = "" + r.path = Path + r.params = make(map[string]string) + r.formParams = make(map[string]string) + return r +} + +func (r *BaseRequest) WithApiInfo(service, version, action string) *BaseRequest { + r.service = service + r.version = version + r.action = action + return r +} + +func GetServiceDomain(service string) (domain string) { + domain = service + "." + RootDomain + return +} + +func CompleteCommonParams(request Request, region string) { + params := request.GetParams() + params["Region"] = region + if request.GetVersion() != "" { + params["Version"] = request.GetVersion() + } + params["Action"] = request.GetAction() + params["Timestamp"] = strconv.FormatInt(time.Now().Unix(), 10) + params["Nonce"] = strconv.Itoa(rand.Int()) + params["RequestClient"] = "SDK_GO_3.0.28" +} + +func ConstructParams(req Request) (err error) { + value := reflect.ValueOf(req).Elem() + err = flatStructure(value, req, "") + //log.Printf("[DEBUG] params=%s", req.GetParams()) + return +} + +func flatStructure(value reflect.Value, request Request, prefix string) (err error) { + //log.Printf("[DEBUG] reflect value: %v", value.Type()) + valueType := value.Type() + for i := 0; i < valueType.NumField(); i++ { + tag := valueType.Field(i).Tag + nameTag, hasNameTag := tag.Lookup("name") + if !hasNameTag { + continue + } + field := value.Field(i) + kind := field.Kind() + if kind == reflect.Ptr && field.IsNil() { + continue + } + if kind == reflect.Ptr { + field = field.Elem() + kind = field.Kind() + } + key := prefix + nameTag + if kind == reflect.String { + s := field.String() + if s != "" { + request.GetParams()[key] = s + } + } else if kind == reflect.Bool { + request.GetParams()[key] = strconv.FormatBool(field.Bool()) + } else if kind == reflect.Int || kind == reflect.Int64 { + request.GetParams()[key] = strconv.FormatInt(field.Int(), 10) + } else if kind == reflect.Uint || kind == reflect.Uint64 { + request.GetParams()[key] = strconv.FormatUint(field.Uint(), 10) + } else if kind == reflect.Float64 { + request.GetParams()[key] = strconv.FormatFloat(field.Float(), 'f', -1, 64) + } else if kind == reflect.Slice { + list := value.Field(i) + for j := 0; j < list.Len(); j++ { + vj := list.Index(j) + key := prefix + nameTag + "." + strconv.Itoa(j) + kind = vj.Kind() + if kind == reflect.Ptr && vj.IsNil() { + continue + } + if kind == reflect.Ptr { + vj = vj.Elem() + kind = vj.Kind() + } + if kind == reflect.String { + request.GetParams()[key] = vj.String() + } else if kind == reflect.Bool { + request.GetParams()[key] = strconv.FormatBool(vj.Bool()) + } else if kind == reflect.Int || kind == reflect.Int64 { + request.GetParams()[key] = strconv.FormatInt(vj.Int(), 10) + } else if kind == reflect.Uint || kind == reflect.Uint64 { + request.GetParams()[key] = strconv.FormatUint(vj.Uint(), 10) + } else if kind == reflect.Float64 { + request.GetParams()[key] = strconv.FormatFloat(vj.Float(), 'f', -1, 64) + } else { + flatStructure(vj, request, key+".") + } + } + } else { + flatStructure(reflect.ValueOf(field.Interface()), request, prefix+nameTag+".") + } + } + return +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/response.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/response.go new file mode 100644 index 0000000000..100fca1102 --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http/response.go @@ -0,0 +1,69 @@ +package common + +import ( + "encoding/json" + "io/ioutil" + // "log" + "net/http" + + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" +) + +type Response interface { + ParseErrorFromHTTPResponse(body []byte) error +} + +type BaseResponse struct { +} + +type ErrorResponse struct { + Response struct { + Error struct { + Code string `json:"Code"` + Message string `json:"Message"` + } `json:"Error" omitempty` + RequestId string `json:"RequestId"` + } `json:"Response"` +} + +type DeprecatedAPIErrorResponse struct { + Code int `json:"code"` + Message string `json:"message"` + CodeDesc string `json:"codeDesc"` +} + +func (r *BaseResponse) ParseErrorFromHTTPResponse(body []byte) (err error) { + resp := &ErrorResponse{} + err = json.Unmarshal(body, resp) + if err != nil { + return + } + if resp.Response.Error.Code != "" { + return errors.NewTencentCloudSDKError(resp.Response.Error.Code, resp.Response.Error.Message, resp.Response.RequestId) + } + + deprecated := &DeprecatedAPIErrorResponse{} + err = json.Unmarshal(body, deprecated) + if err != nil { + return + } + if deprecated.Code != 0 { + return errors.NewTencentCloudSDKError(deprecated.CodeDesc, deprecated.Message, "") + } + return nil +} + +func ParseFromHttpResponse(hr *http.Response, response Response) (err error) { + defer hr.Body.Close() + body, err := ioutil.ReadAll(hr.Body) + if err != nil { + return + } + //log.Printf("[DEBUG] Response Body=%s", body) + err = response.ParseErrorFromHTTPResponse(body) + if err != nil { + return + } + err = json.Unmarshal(body, &response) + return +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/client_profile.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/client_profile.go new file mode 100644 index 0000000000..94c63b2f03 --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/client_profile.go @@ -0,0 +1,13 @@ +package profile + +type ClientProfile struct { + HttpProfile *HttpProfile + SignMethod string +} + +func NewClientProfile() *ClientProfile { + return &ClientProfile{ + HttpProfile: NewHttpProfile(), + SignMethod: "HmacSHA256", + } +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/http_profile.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/http_profile.go new file mode 100644 index 0000000000..8d4bf8f57b --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile/http_profile.go @@ -0,0 +1,17 @@ +package profile + +type HttpProfile struct { + ReqMethod string + ReqTimeout int + Endpoint string + Protocol string +} + +func NewHttpProfile() *HttpProfile { + return &HttpProfile{ + ReqMethod: "POST", + ReqTimeout: 60, + Endpoint: "", + Protocol: "HTTPS", + } +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/sign.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/sign.go new file mode 100644 index 0000000000..450fe2ccde --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/sign.go @@ -0,0 +1,75 @@ +package common + +import ( + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "encoding/base64" + "fmt" + "sort" + "strings" + + tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http" +) + +const ( + SHA256 = "HmacSHA256" + SHA1 = "HmacSHA1" +) + +func Sign(s, secretKey, method string) string { + hashed := hmac.New(sha1.New, []byte(secretKey)) + if method == SHA256 { + hashed = hmac.New(sha256.New, []byte(secretKey)) + } + hashed.Write([]byte(s)) + + return base64.StdEncoding.EncodeToString(hashed.Sum(nil)) +} + +func signRequest(request tchttp.Request, credential *Credential, method string) (err error) { + if method != SHA256 { + method = SHA1 + } + checkAuthParams(request, credential, method) + s := getStringToSign(request) + signature := Sign(s, credential.SecretKey, method) + request.GetParams()["Signature"] = signature + return +} + +func checkAuthParams(request tchttp.Request, credential *Credential, method string) { + params := request.GetParams() + credentialParams := credential.GetCredentialParams() + for key, value := range credentialParams { + params[key] = value + } + params["SignatureMethod"] = method + delete(params, "Signature") +} + +func getStringToSign(request tchttp.Request) string { + method := request.GetHttpMethod() + domain := request.GetDomain() + path := request.GetPath() + + text := method + domain + path + "?" + + params := request.GetParams() + // sort params + keys := make([]string, 0, len(params)) + for k, _ := range params { + keys = append(keys, k) + } + sort.Strings(keys) + + for i := range keys { + k := keys[i] + if params[k] == "" { + continue + } + text += fmt.Sprintf("%v=%v&", strings.Replace(k, "_", ".", -1), params[k]) + } + text = text[:len(text)-1] + return text +} diff --git a/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/types.go b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/types.go new file mode 100644 index 0000000000..ec2c786dbf --- /dev/null +++ b/vendor/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/types.go @@ -0,0 +1,47 @@ +package common + +func IntPtr(v int) *int { + return &v +} + +func Int64Ptr(v int64) *int64 { + return &v +} + +func UintPtr(v uint) *uint { + return &v +} + +func Uint64Ptr(v uint64) *uint64 { + return &v +} + +func Float64Ptr(v float64) *float64 { + return &v +} + +func StringPtr(v string) *string { + return &v +} + +func StringValues(ptrs []*string) []string { + values := make([]string, len(ptrs)) + for i := 0; i < len(ptrs); i++ { + if ptrs[i] != nil { + values[i] = *ptrs[i] + } + } + return values +} + +func StringPtrs(vals []string) []*string { + ptrs := make([]*string, len(vals)) + for i := 0; i < len(vals); i++ { + ptrs[i] = &vals[i] + } + return ptrs +} + +func BoolPtr(v bool) *bool { + return &v +}