Files
cloudpods/pkg/multicloud/hcso/instance.go
2022-10-08 11:28:45 +08:00

1462 lines
45 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hcso
import (
"context"
"encoding/base64"
"fmt"
"sort"
"strconv"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/osprofile"
"yunion.io/x/pkg/utils"
"yunion.io/x/onecloud/pkg/apis"
billing_api "yunion.io/x/onecloud/pkg/apis/billing"
api "yunion.io/x/onecloud/pkg/apis/compute"
"yunion.io/x/onecloud/pkg/cloudprovider"
"yunion.io/x/onecloud/pkg/multicloud"
"yunion.io/x/onecloud/pkg/multicloud/hcso/client/modules"
"yunion.io/x/onecloud/pkg/util/billing"
"yunion.io/x/onecloud/pkg/util/cloudinit"
"yunion.io/x/onecloud/pkg/util/imagetools"
)
const (
InstanceStatusRunning = "ACTIVE"
InstanceStatusTerminated = "DELETED"
InstanceStatusStopped = "SHUTOFF"
)
type IpAddress struct {
Version string `json:"version"`
Addr string `json:"addr"`
OSEXTIPSMACMACAddr string `json:"OS-EXT-IPS-MAC:mac_addr"`
OSEXTIPSPortID string `json:"OS-EXT-IPS:port_id"`
OSEXTIPSType string `json:"OS-EXT-IPS:type"`
}
type Flavor struct {
Disk string `json:"disk"`
Vcpus string `json:"vcpus"`
RAM string `json:"ram"`
ID string `json:"id"`
Name string `json:"name"`
}
type Image struct {
ID string `json:"id"`
}
type VMMetadata struct {
MeteringImageID string `json:"metering.image_id"`
MeteringImagetype string `json:"metering.imagetype"`
MeteringResourcespeccode string `json:"metering.resourcespeccode"`
ImageName string `json:"image_name"`
OSBit string `json:"os_bit"`
VpcID string `json:"vpc_id"`
MeteringResourcetype string `json:"metering.resourcetype"`
CascadedInstanceExtrainfo string `json:"cascaded.instance_extrainfo"`
OSType string `json:"os_type"`
ChargingMode string `json:"charging_mode"`
}
type OSExtendedVolumesVolumesAttached struct {
Device string `json:"device"`
BootIndex string `json:"bootIndex"`
ID string `json:"id"`
DeleteOnTermination string `json:"delete_on_termination"`
}
type OSSchedulerHints struct {
}
type SecurityGroup struct {
Name string `json:"name"`
}
type SysTag struct {
Key string `json:"key"`
Value string `json:"value"`
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0094148849.html
// https://support.huaweicloud.com/api-bpconsole/zh-cn_topic_0100166287.html v1.1 支持创建包年/包月的弹性云服务器
type SInstance struct {
multicloud.SInstanceBase
multicloud.HuaweiTags
host *SHost
osInfo *imagetools.ImageInfo
ID string `json:"id"`
Name string `json:"name"`
Addresses map[string][]IpAddress `json:"addresses"`
Flavor Flavor `json:"flavor"`
AccessIPv4 string `json:"accessIPv4"`
AccessIPv6 string `json:"accessIPv6"`
Status string `json:"status"`
Progress string `json:"progress"`
HostID string `json:"hostId"`
Updated string `json:"updated"`
Created time.Time `json:"created"`
Metadata VMMetadata `json:"metadata"`
Description string `json:"description"`
Locked bool `json:"locked"`
ConfigDrive string `json:"config_drive"`
TenantID string `json:"tenant_id"`
UserID string `json:"user_id"`
KeyName string `json:"key_name"`
OSExtendedVolumesVolumesAttached []OSExtendedVolumesVolumesAttached `json:"os-extended-volumes:volumes_attached"`
OSEXTSTSTaskState string `json:"OS-EXT-STS:task_state"`
OSEXTSTSPowerState int64 `json:"OS-EXT-STS:power_state"`
OSEXTSTSVMState string `json:"OS-EXT-STS:vm_state"`
OSEXTSRVATTRHost string `json:"OS-EXT-SRV-ATTR:host"`
OSEXTSRVATTRInstanceName string `json:"OS-EXT-SRV-ATTR:instance_name"`
OSEXTSRVATTRHypervisorHostname string `json:"OS-EXT-SRV-ATTR:hypervisor_hostname"`
OSDCFDiskConfig string `json:"OS-DCF:diskConfig"`
OSEXTAZAvailabilityZone string `json:"OS-EXT-AZ:availability_zone"`
OSSchedulerHints OSSchedulerHints `json:"os:scheduler_hints"`
OSEXTSRVATTRRootDeviceName string `json:"OS-EXT-SRV-ATTR:root_device_name"`
OSEXTSRVATTRRamdiskID string `json:"OS-EXT-SRV-ATTR:ramdisk_id"`
EnterpriseProjectID string `json:"enterprise_project_id"`
OSEXTSRVATTRUserData string `json:"OS-EXT-SRV-ATTR:user_data"`
OSSRVUSGLaunchedAt time.Time `json:"OS-SRV-USG:launched_at"`
OSEXTSRVATTRKernelID string `json:"OS-EXT-SRV-ATTR:kernel_id"`
OSEXTSRVATTRLaunchIndex int64 `json:"OS-EXT-SRV-ATTR:launch_index"`
HostStatus string `json:"host_status"`
OSEXTSRVATTRReservationID string `json:"OS-EXT-SRV-ATTR:reservation_id"`
OSEXTSRVATTRHostname string `json:"OS-EXT-SRV-ATTR:hostname"`
OSSRVUSGTerminatedAt time.Time `json:"OS-SRV-USG:terminated_at"`
SysTags []SysTag `json:"sys_tags"`
SecurityGroups []SecurityGroup `json:"security_groups"`
EnterpriseProjectId string
}
func compareSet(currentSet []string, newSet []string) (add []string, remove []string, keep []string) {
sort.Strings(currentSet)
sort.Strings(newSet)
i, j := 0, 0
for i < len(currentSet) || j < len(newSet) {
if i < len(currentSet) && j < len(newSet) {
if currentSet[i] == newSet[j] {
keep = append(keep, currentSet[i])
i += 1
j += 1
} else if currentSet[i] < newSet[j] {
remove = append(remove, currentSet[i])
i += 1
} else {
add = append(add, newSet[j])
j += 1
}
} else if i >= len(currentSet) {
add = append(add, newSet[j])
j += 1
} else if j >= len(newSet) {
remove = append(remove, currentSet[i])
i += 1
}
}
return add, remove, keep
}
// 启动盘 != 系统盘(必须是启动盘且挂载在root device上)
func isBootDisk(server *SInstance, disk *SDisk) bool {
if disk.GetDiskType() != api.DISK_TYPE_SYS {
return false
}
for _, attachment := range disk.Attachments {
if attachment.ServerID == server.GetId() && attachment.Device == server.OSEXTSRVATTRRootDeviceName {
return true
}
}
return false
}
func (self *SInstance) GetId() string {
return self.ID
}
func (self *SInstance) GetName() string {
return self.Name
}
func (self *SInstance) GetGlobalId() string {
return self.ID
}
func (self *SInstance) GetStatus() string {
switch self.Status {
case "ACTIVE":
return api.VM_RUNNING
case "MIGRATING", "REBUILD", "BUILD", "RESIZE", "VERIFY_RESIZE": // todo: pending ?
return api.VM_STARTING
case "REBOOT", "HARD_REBOOT":
return api.VM_STOPPING
case "SHUTOFF":
return api.VM_READY
default:
return api.VM_UNKNOWN
}
}
func (self *SInstance) Refresh() error {
new, err := self.host.zone.region.GetInstanceByID(self.GetId())
new.host = self.host
if err != nil {
return err
}
if new.Status == InstanceStatusTerminated {
log.Debugf("Instance already terminated.")
return cloudprovider.ErrNotFound
}
err = jsonutils.Update(self, new)
if err != nil {
return err
}
return nil
}
func (self *SInstance) IsEmulated() bool {
return false
}
func (self *SInstance) GetInstanceType() string {
return self.Flavor.ID
}
func (self *SInstance) GetSecurityGroupIds() ([]string, error) {
return self.host.zone.region.GetInstanceSecrityGroupIds(self.GetId())
}
// https://support.huaweicloud.com/api-ecs/ecs_02_1002.html
// key 相同时value不会替换
func (self *SRegion) CreateServerTags(instanceId string, tags map[string]string) error {
params := map[string]interface{}{
"action": "create",
}
tagsObj := []map[string]string{}
for k, v := range tags {
tagsObj = append(tagsObj, map[string]string{"key": k, "value": v})
}
params["tags"] = tagsObj
_, err := self.ecsClient.Servers.PerformAction2("tags/action", instanceId, jsonutils.Marshal(params), "")
return err
}
// https://support.huaweicloud.com/api-ecs/ecs_02_1003.html
func (self *SRegion) DeleteServerTags(instanceId string, tagsKey []string) error {
params := map[string]interface{}{
"action": "delete",
}
tagsObj := []map[string]string{}
for _, k := range tagsKey {
tagsObj = append(tagsObj, map[string]string{"key": k})
}
params["tags"] = tagsObj
_, err := self.ecsClient.Servers.PerformAction2("tags/action", instanceId, jsonutils.Marshal(params), "")
return err
}
func (self *SInstance) SetTags(tags map[string]string, replace bool) error {
existedTags, err := self.GetTags()
if err != nil {
return errors.Wrap(err, "self.GetTags()")
}
deleteTagsKey := []string{}
for k := range existedTags {
if replace {
deleteTagsKey = append(deleteTagsKey, k)
} else {
if _, ok := tags[k]; ok {
deleteTagsKey = append(deleteTagsKey, k)
}
}
}
if len(deleteTagsKey) > 0 {
err := self.host.zone.region.DeleteServerTags(self.GetId(), deleteTagsKey)
if err != nil {
return errors.Wrapf(err, "self.host.zone.region.DeleteServerTags(%s,%s)", self.GetId(), deleteTagsKey)
}
}
if len(tags) > 0 {
err := self.host.zone.region.CreateServerTags(self.GetId(), tags)
if err != nil {
return errors.Wrapf(err, "self.host.zone.region.CreateServerTags(%s,%s)", self.GetId(), jsonutils.Marshal(tags).String())
}
}
return nil
}
func (self *SInstance) GetBillingType() string {
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0094148849.html
// charging_mode “0”按需计费 “1”按包年包月计费
if self.Metadata.ChargingMode == "1" {
return billing_api.BILLING_TYPE_PREPAID
} else {
return billing_api.BILLING_TYPE_POSTPAID
}
}
func (self *SInstance) GetCreatedAt() time.Time {
return self.Created
}
// charging_mode “0”按需计费 “1”按包年包月计费
func (self *SInstance) GetExpiredAt() time.Time {
var expiredTime time.Time
return expiredTime
}
func (self *SInstance) GetIHost() cloudprovider.ICloudHost {
return self.host
}
func (self *SInstance) GetIHostId() string {
return self.host.GetGlobalId()
}
func (self *SInstance) GetIDisks() ([]cloudprovider.ICloudDisk, error) {
err := self.Refresh()
if err != nil {
return nil, err
}
attached := self.OSExtendedVolumesVolumesAttached
disks := make([]SDisk, 0)
for _, vol := range attached {
disk, err := self.host.zone.region.GetDisk(vol.ID)
if err != nil {
return nil, err
}
disks = append(disks, *disk)
}
idisks := make([]cloudprovider.ICloudDisk, len(disks))
for i := 0; i < len(disks); i += 1 {
storage, err := self.host.zone.getStorageByCategory(disks[i].VolumeType)
if err != nil {
return nil, err
}
disks[i].storage = storage
idisks[i] = &disks[i]
// 将系统盘放到第0个位置
if isBootDisk(self, &disks[i]) {
_temp := idisks[0]
idisks[0] = &disks[i]
idisks[i] = _temp
}
}
return idisks, nil
}
func (self *SInstance) GetINics() ([]cloudprovider.ICloudNic, error) {
nics := make([]cloudprovider.ICloudNic, 0)
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0094148849.html
// OS-EXT-IPS.type
// todo: 这里没有区分是IPv4 还是 IPv6。统一当IPv4处理了.可能会引发错误
for _, ipAddresses := range self.Addresses {
for _, ipAddress := range ipAddresses {
if ipAddress.OSEXTIPSType == "fixed" {
nic := SInstanceNic{
instance: self,
ipAddr: ipAddress.Addr,
macAddr: ipAddress.OSEXTIPSMACMACAddr,
}
nics = append(nics, &nic)
}
}
}
return nics, nil
}
func (self *SInstance) GetIEIP() (cloudprovider.ICloudEIP, error) {
ips := make([]string, 0)
for _, addresses := range self.Addresses {
for _, address := range addresses {
if address.OSEXTIPSType != "fixed" && !strings.HasPrefix(address.Addr, "100.") {
ips = append(ips, address.Addr)
}
}
}
if len(ips) == 0 {
return nil, nil
}
eips, err := self.host.zone.region.GetEips()
if err != nil {
return nil, err
}
for _, eip := range eips {
if eip.PublicIPAddress == ips[0] {
return &eip, nil
}
}
return nil, nil
}
func (self *SInstance) GetVcpuCount() int {
cpu, _ := strconv.Atoi(self.Flavor.Vcpus)
return cpu
}
func (self *SInstance) GetVmemSizeMB() int {
mem, _ := strconv.Atoi(self.Flavor.RAM)
return int(mem)
}
func (self *SInstance) GetBootOrder() string {
return "dcn"
}
func (self *SInstance) GetVga() string {
return "std"
}
func (self *SInstance) GetVdi() string {
return "vnc"
}
func (self *SInstance) GetOsArch() string {
if flavor, err := self.host.zone.region.GetICloudSku(self.Flavor.ID); err == nil {
return flavor.GetCpuArch()
} else {
log.Debugf("GetOSArch.GetICloudSku %s: %s", self.Flavor.ID, err)
}
t := self.GetInstanceType()
if len(t) > 0 {
if strings.HasPrefix(t, "k") {
return apis.OS_ARCH_AARCH64
}
}
return apis.OS_ARCH_X86
}
func (self *SInstance) GetOsType() cloudprovider.TOsType {
return cloudprovider.TOsType(osprofile.NormalizeOSType(self.Metadata.OSType))
}
func (self *SInstance) GetFullOsName() string {
return self.Metadata.ImageName
}
func (i *SInstance) getNormalizedOsInfo() *imagetools.ImageInfo {
if i.osInfo == nil {
osInfo := imagetools.NormalizeImageInfo(i.Metadata.ImageName, "", i.Metadata.OSType, "", "")
i.osInfo = &osInfo
}
return i.osInfo
}
func (i *SInstance) GetBios() cloudprovider.TBiosType {
return cloudprovider.ToBiosType(i.getNormalizedOsInfo().OsBios)
}
func (i *SInstance) GetOsDist() string {
return i.getNormalizedOsInfo().OsDistro
}
func (i *SInstance) GetOsVersion() string {
return i.getNormalizedOsInfo().OsVersion
}
func (i *SInstance) GetOsLang() string {
return i.getNormalizedOsInfo().OsLang
}
func (self *SInstance) GetMachine() string {
return "pc"
}
func (self *SInstance) AssignSecurityGroup(secgroupId string) error {
return self.SetSecurityGroups([]string{secgroupId})
}
func (self *SInstance) SetSecurityGroups(secgroupIds []string) error {
currentSecgroups, err := self.host.zone.region.GetInstanceSecrityGroupIds(self.GetId())
if err != nil {
return err
}
add, remove, _ := compareSet(currentSecgroups, secgroupIds)
err = self.host.zone.region.assignSecurityGroups(add, self.GetId())
if err != nil {
return err
}
return self.host.zone.region.unassignSecurityGroups(remove, self.GetId())
}
func (self *SInstance) GetHypervisor() string {
return api.HYPERVISOR_HCSO
}
func (self *SInstance) StartVM(ctx context.Context) error {
if self.Status == InstanceStatusRunning {
return nil
}
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
}
if self.GetStatus() == api.VM_RUNNING {
return nil
} else if self.GetStatus() == api.VM_READY {
err := self.host.zone.region.StartVM(self.GetId())
if err != nil {
return err
}
}
time.Sleep(interval)
}
return cloudprovider.ErrTimeout
}
func (self *SInstance) StopVM(ctx context.Context, opts *cloudprovider.ServerStopOptions) error {
if self.Status == InstanceStatusStopped {
return nil
}
if self.Status == InstanceStatusTerminated {
log.Debugf("Instance already terminated.")
return nil
}
err := self.host.zone.region.StopVM(self.GetId(), opts.IsForce)
if err != nil {
return err
}
return cloudprovider.WaitStatus(self, api.VM_READY, 10*time.Second, 300*time.Second) // 5mintues
}
func (self *SInstance) DeleteVM(ctx context.Context) error {
if self.Status == InstanceStatusTerminated {
return nil
}
for {
err := self.host.zone.region.DeleteVM(self.GetId())
if err != nil && self.Status != InstanceStatusTerminated {
log.Errorf("DeleteVM fail: %s", err)
return err
} else {
break
}
}
return cloudprovider.WaitDeleted(self, 10*time.Second, 300*time.Second) // 5minutes
}
func (self *SInstance) UpdateVM(ctx context.Context, name string) error {
return self.host.zone.region.UpdateVM(self.GetId(), name)
}
// https://support.huaweicloud.com/usermanual-ecs/zh-cn_topic_0032380449.html
// 创建云服务器过程中注入用户数据。支持注入文本、文本文件或gzip文件。
// 注入内容需要进行base64格式编码。注入内容编码之前的内容最大长度32KB。
// 对于Linux弹性云服务器adminPass参数传入时user_data参数不生效。
func (self *SInstance) UpdateUserData(userData string) error {
return cloudprovider.ErrNotSupported
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876349.html 使用原镜像重装
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876971.html 更换系统盘操作系统
// 不支持调整系统盘大小
func (self *SInstance) RebuildRoot(ctx context.Context, desc *cloudprovider.SManagedVMRebuildRootConfig) (string, error) {
var err error
var jobId string
publicKeyName := ""
if len(desc.PublicKey) > 0 {
publicKeyName, err = self.host.zone.region.syncKeypair(desc.PublicKey)
if err != nil {
return "", err
}
}
image, err := self.host.zone.region.GetImage(desc.ImageId)
if err != nil {
return "", errors.Wrap(err, "SInstance.RebuildRoot.GetImage")
}
// Password存在的情况下windows 系统直接使用密码
if strings.ToLower(image.Platform) == strings.ToLower(osprofile.OS_TYPE_WINDOWS) && len(desc.Password) > 0 {
publicKeyName = ""
}
userData, err := updateUserData(self.OSEXTSRVATTRUserData, image.OSVersion, desc.Account, desc.Password, desc.PublicKey)
if err != nil {
return "", errors.Wrap(err, "SInstance.RebuildRoot.updateUserData")
}
if self.Metadata.MeteringImageID == desc.ImageId {
jobId, err = self.host.zone.region.RebuildRoot(ctx, self.UserID, self.GetId(), desc.Password, publicKeyName, userData)
if err != nil {
return "", err
}
} else {
jobId, err = self.host.zone.region.ChangeRoot(ctx, self.UserID, self.GetId(), desc.ImageId, desc.Password, publicKeyName, userData)
if err != nil {
return "", err
}
}
err = self.host.zone.region.waitTaskStatus(self.host.zone.region.ecsClient.Servers.ServiceType(), jobId, TASK_SUCCESS, 15*time.Second, 900*time.Second)
if err != nil {
log.Errorf("RebuildRoot task error %s", err)
return "", err
}
err = self.Refresh()
if err != nil {
return "", err
}
idisks, err := self.GetIDisks()
if err != nil {
return "", err
}
if len(idisks) == 0 {
return "", fmt.Errorf("server %s has no volume attached.", self.GetId())
}
return idisks[0].GetId(), nil
}
func (self *SInstance) DeployVM(ctx context.Context, name string, username string, password string, publicKey string, deleteKeypair bool, description string) error {
return self.host.zone.region.DeployVM(self.GetId(), name, password, publicKey, deleteKeypair, description)
}
func (self *SInstance) ChangeConfig(ctx context.Context, config *cloudprovider.SManagedVMChangeConfig) error {
instanceTypes := []string{}
if len(config.InstanceType) > 0 {
instanceTypes = []string{config.InstanceType}
} else {
flavors, err := self.host.zone.region.GetMatchInstanceTypes(config.Cpu, config.MemoryMB, self.OSEXTAZAvailabilityZone)
if err != nil {
return errors.Wrapf(err, "GetMatchInstanceTypes")
}
for _, flavor := range flavors {
instanceTypes = append(instanceTypes, flavor.ID)
}
}
var err error
for _, instanceType := range instanceTypes {
err = self.host.zone.region.ChangeVMConfig(self.GetId(), instanceType)
if err != nil {
log.Warningf("ChangeVMConfig %s for %s error: %v", self.GetId(), instanceType, err)
} else {
return cloudprovider.WaitStatusWithDelay(self, api.VM_READY, 15*time.Second, 15*time.Second, 180*time.Second)
}
}
if err != nil {
return errors.Wrapf(err, "ChangeVMConfig")
}
return fmt.Errorf("Failed to change vm config, specification not supported")
}
// todo:// 返回jsonobject感觉很诡异。不能直接知道内部细节
func (self *SInstance) GetVNCInfo() (jsonutils.JSONObject, error) {
return self.host.zone.region.GetInstanceVNCUrl(self.GetId())
}
func (self *SInstance) NextDeviceName() (string, error) {
prefix := "s"
if strings.Contains(self.OSEXTSRVATTRRootDeviceName, "/vd") {
prefix = "v"
}
currents := []string{}
for _, item := range self.OSExtendedVolumesVolumesAttached {
currents = append(currents, strings.ToLower(item.Device))
}
for i := 0; i < 25; i++ {
device := fmt.Sprintf("/dev/%sd%s", prefix, string([]byte{byte(98 + i)}))
if ok, _ := utils.InStringArray(device, currents); !ok {
return device, nil
}
}
return "", fmt.Errorf("disk devicename out of index, current deivces: %s", currents)
}
func (self *SInstance) AttachDisk(ctx context.Context, diskId string) error {
device, err := self.NextDeviceName()
if err != nil {
return errors.Wrap(err, "Instance.AttachDisk.NextDeviceName")
}
err = self.host.zone.region.AttachDisk(self.GetId(), diskId, device)
if err != nil {
return errors.Wrap(err, "Instance.AttachDisk.AttachDisk")
}
return cloudprovider.Wait(5*time.Second, 60*time.Second, func() (bool, error) {
disk, err := self.host.zone.region.GetDisk(diskId)
if err != nil {
log.Debugf("Instance.AttachDisk.GetDisk %s", err)
return false, nil
}
if disk.Status == "in-use" {
return true, nil
}
return false, nil
})
}
func (self *SInstance) DetachDisk(ctx context.Context, diskId string) error {
err := self.host.zone.region.DetachDisk(self.GetId(), diskId)
if err != nil {
return errors.Wrap(err, "Instance.DetachDisk")
}
return cloudprovider.Wait(5*time.Second, 60*time.Second, func() (bool, error) {
disk, err := self.host.zone.region.GetDisk(diskId)
if err != nil {
log.Debugf("Instance.DetachDisk.GetDisk %s", err)
return false, nil
}
if disk.Status == "available" {
return true, nil
}
return false, nil
})
}
func (self *SInstance) Renew(bc billing.SBillingCycle) error {
return cloudprovider.ErrNotSupported
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0094148850.html
func (self *SRegion) GetInstances() ([]SInstance, error) {
queries := make(map[string]string)
if len(self.client.projectId) > 0 {
queries["project_id"] = self.client.projectId
}
instances := make([]SInstance, 0)
err := doListAllWithPagerOffset(self.ecsClient.Servers.List, queries, &instances)
return instances, err
}
func (self *SRegion) GetInstanceByID(instanceId string) (SInstance, error) {
instance := SInstance{}
err := DoGet(self.ecsClient.Servers.Get, instanceId, nil, &instance)
if instance.Status == "DELETED" {
return instance, errors.Wrap(cloudprovider.ErrNotFound, "GetInstanceByID")
}
return instance, err
}
func (self *SRegion) GetInstanceByIds(ids []string) ([]SInstance, int, error) {
instances := make([]SInstance, 0)
for _, instanceId := range ids {
instance, err := self.GetInstanceByID(instanceId)
if err != nil {
return nil, 0, err
}
instances = append(instances, instance)
}
return instances, len(instances), nil
}
/*
系统盘大小取值范围1-1024 GB且必须不小于镜像min_disk.
*/
type SServerCreate struct {
AvailabilityZone string `json:"availability_zone"`
Name string `json:"name"`
ImageRef string `json:"imageRef"`
RootVolume RootVolume `json:"root_volume"`
DataVolumes []DataVolume `json:"data_volumes"`
FlavorRef string `json:"flavorRef"`
UserData string `json:"user_data"`
Vpcid string `json:"vpcid"`
SecurityGroups []SecGroup `json:"security_groups"`
Nics []NIC `json:"nics"`
KeyName string `json:"key_name"`
AdminPass string `json:"adminPass"`
Count int64 `json:"count"`
Extendparam ServerExtendparam `json:"extendparam"`
ServerTags []ServerTag `json:"server_tags"`
Description string `json:"description"`
}
type DataVolume struct {
Volumetype string `json:"volumetype"`
SizeGB int `json:"size"`
Extendparam *DataVolumeExtendparam `json:"extendparam,omitempty"`
Multiattach *bool `json:"multiattach,omitempty"`
HwPassthrough *string `json:"hw:passthrough,omitempty"`
}
type DataVolumeExtendparam struct {
SnapshotID string `json:"snapshotId"`
}
type ServerExtendparam struct {
ChargingMode string `json:"chargingMode"` // 计费模式 prePaid|postPaid
PeriodType string `json:"periodType"` // 周期类型month|year
PeriodNum string `json:"periodNum"` // 订购周期数periodType=month周期类型为月取值为[19]。periodType=year周期类型为年取值为1。
IsAutoRenew string `json:"isAutoRenew"` // 是否自动续订 true|false
IsAutoPay string `json:"isAutoPay"` // 是否自动从客户的账户中支付 true|false
RegionID string `json:"regionID"`
EnterpriseProjectId string `json:"enterprise_project_id,omitempty"`
}
type NIC struct {
SubnetID string `json:"subnet_id"` // 网络ID. 与 SNetwork里的ID对应。统一使用这个ID
IpAddress string `json:"ip_address"`
}
type RootVolume struct {
Volumetype string `json:"volumetype"`
SizeGB int `json:"size"`
}
type SecGroup struct {
ID string `json:"id"`
}
type ServerTag struct {
Key string `json:"key"`
Value string `json:"value"`
}
/*
包月机器退订规则: https://support.huaweicloud.com/usermanual-billing/zh-cn_topic_0083138805.html
5天无理由全额退订新购资源不包含续费资源在开通的五天内且退订次数不超过10次每账号每年10次的符合5天无理由全额退订。
非5天无理由退订不符合5天无理由全额退订条件的退订都属于非5天无理由退订。非5天无理由退订不限制退订次数但需要收取退订手续费。
退订资源的方法: https://support.huaweicloud.com/usermanual-billing/zh-cn_topic_0072297197.html
*/
func (self *SRegion) CreateInstance(name string, imageId string, instanceType string, SubnetId string,
securityGroupId string, vpcId string, zoneId string, desc string, disks []SDisk, ipAddr string,
keypair string, publicKey string, passwd string, userData string, bc *billing.SBillingCycle, projectId string, tags map[string]string) (string, error) {
params := SServerCreate{}
params.AvailabilityZone = zoneId
params.Name = name
params.FlavorRef = instanceType
params.ImageRef = imageId
params.Description = desc
params.Count = 1
params.Nics = []NIC{{SubnetID: SubnetId, IpAddress: ipAddr}}
params.SecurityGroups = []SecGroup{{ID: securityGroupId}}
params.Vpcid = vpcId
for i, disk := range disks {
if i == 0 {
params.RootVolume.Volumetype = disk.VolumeType
params.RootVolume.SizeGB = disk.SizeGB
} else {
dataVolume := DataVolume{}
dataVolume.Volumetype = disk.VolumeType
dataVolume.SizeGB = disk.SizeGB
params.DataVolumes = append(params.DataVolumes, dataVolume)
}
}
if len(projectId) > 0 {
params.Extendparam.EnterpriseProjectId = projectId
}
// billing type
if bc != nil {
params.Extendparam.ChargingMode = PRE_PAID
if bc.GetMonths() <= 9 {
params.Extendparam.PeriodNum = strconv.Itoa(bc.GetMonths())
params.Extendparam.PeriodType = "month"
} else {
params.Extendparam.PeriodNum = strconv.Itoa(bc.GetYears())
params.Extendparam.PeriodType = "year"
}
params.Extendparam.RegionID = self.GetId()
if bc.AutoRenew {
params.Extendparam.IsAutoRenew = "true"
} else {
params.Extendparam.IsAutoRenew = "false"
}
params.Extendparam.IsAutoPay = "true"
} else {
params.Extendparam.ChargingMode = POST_PAID
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212668.html#ZH-CN_TOPIC_0020212668__table761103195216
if len(keypair) > 0 {
params.KeyName = keypair
} else {
params.AdminPass = passwd
}
if len(userData) > 0 {
params.UserData = userData
}
if len(tags) > 0 {
serverTags := []ServerTag{}
for k, v := range tags {
serverTags = append(serverTags, ServerTag{Key: k, Value: v})
}
params.ServerTags = serverTags
}
serverObj := jsonutils.Marshal(params)
createParams := jsonutils.NewDict()
createParams.Add(serverObj, "server")
_id, err := self.ecsClient.Servers.AsyncCreate(createParams)
if err != nil {
return "", err
}
var ids []string
if params.Extendparam.ChargingMode == POST_PAID {
// 按需计费
ids, err = self.GetAllSubTaskEntityIDs(self.ecsClient.Servers.ServiceType(), _id, "server_id")
} else {
// 包年包月
return "", errors.Wrap(cloudprovider.ErrNotSupported, "CreateInstance")
}
if err != nil {
return "", err
} else if len(ids) == 0 {
return "", fmt.Errorf("CreateInstance job %s result is emtpy", _id)
} else if len(ids) == 1 {
return ids[0], nil
} else {
return "", fmt.Errorf("CreateInstance job %s mutliple instance id returned. %s", _id, ids)
}
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067161469.html
// 添加多个安全组时建议最多为弹性云服务器添加5个安全组。
// todo: 确认是否需要先删除,再进行添加操作
func (self *SRegion) assignSecurityGroups(secgroupIds []string, instanceId string) error {
_, err := self.GetInstanceByID(instanceId)
if err != nil {
return err
}
for i := range secgroupIds {
secId := secgroupIds[i]
params := jsonutils.NewDict()
secgroupObj := jsonutils.NewDict()
secgroupObj.Add(jsonutils.NewString(secId), "name")
params.Add(secgroupObj, "addSecurityGroup")
_, err := self.ecsClient.NovaServers.PerformAction("action", instanceId, params)
if err != nil {
return err
}
}
return nil
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067161717.html
func (self *SRegion) unassignSecurityGroups(secgroupIds []string, instanceId string) error {
for i := range secgroupIds {
secId := secgroupIds[i]
params := jsonutils.NewDict()
secgroupObj := jsonutils.NewDict()
secgroupObj.Add(jsonutils.NewString(secId), "name")
params.Add(secgroupObj, "removeSecurityGroup")
_, err := self.ecsClient.NovaServers.PerformAction("action", instanceId, params)
if err != nil {
return err
}
}
return nil
}
func (self *SRegion) GetInstanceStatus(instanceId string) (string, error) {
instance, err := self.GetInstanceByID(instanceId)
if err != nil {
return "", err
}
return instance.Status, nil
}
func (self *SRegion) instanceStatusChecking(instanceId, status string) error {
remoteStatus, err := self.GetInstanceStatus(instanceId)
if err != nil {
log.Errorf("Fail to get instance status: %s", err)
return err
}
if status != remoteStatus {
log.Errorf("instanceStatusChecking: vm status is %s expect %s", remoteStatus, status)
return cloudprovider.ErrInvalidStatus
}
return nil
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212207.html
func (self *SRegion) StartVM(instanceId string) error {
rstatus, err := self.GetInstanceStatus(instanceId)
if err != nil {
return err
}
if rstatus == InstanceStatusRunning {
return nil
}
if rstatus != InstanceStatusStopped {
log.Errorf("instanceStatusChecking: vm status is %s expect %s", rstatus, InstanceStatusStopped)
return cloudprovider.ErrInvalidStatus
}
params := jsonutils.NewDict()
startObj := jsonutils.NewDict()
serversObj := jsonutils.NewArray()
serverObj := jsonutils.NewDict()
serverObj.Add(jsonutils.NewString(instanceId), "id")
serversObj.Add(serverObj)
startObj.Add(serversObj, "servers")
params.Add(startObj, "os-start")
_, err = self.ecsClient.Servers.PerformAction2("action", "", params, "")
return err
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212651.html
func (self *SRegion) StopVM(instanceId string, isForce bool) error {
rstatus, err := self.GetInstanceStatus(instanceId)
if err != nil {
return err
}
if rstatus == InstanceStatusStopped {
return nil
}
if rstatus != InstanceStatusRunning {
log.Errorf("instanceStatusChecking: vm status is %s expect %s", rstatus, InstanceStatusRunning)
return cloudprovider.ErrInvalidStatus
}
params := jsonutils.NewDict()
stopObj := jsonutils.NewDict()
serversObj := jsonutils.NewArray()
serverObj := jsonutils.NewDict()
serverObj.Add(jsonutils.NewString(instanceId), "id")
serversObj.Add(serverObj)
stopObj.Add(serversObj, "servers")
if isForce {
stopObj.Add(jsonutils.NewString("HARD"), "type")
} else {
stopObj.Add(jsonutils.NewString("SOFT"), "type")
}
params.Add(stopObj, "os-stop")
_, err = self.ecsClient.Servers.PerformAction2("action", "", params, "")
return err
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212679.html
// 只删除主机。弹性IP和数据盘需要单独删除
func (self *SRegion) DeleteVM(instanceId string) error {
remoteStatus, err := self.GetInstanceStatus(instanceId)
if err != nil {
return err
}
if remoteStatus != InstanceStatusStopped {
log.Errorf("DeleteVM vm status is %s expect %s", remoteStatus, InstanceStatusStopped)
return cloudprovider.ErrInvalidStatus
}
params := jsonutils.NewDict()
serversObj := jsonutils.NewArray()
serverObj := jsonutils.NewDict()
serverObj.Add(jsonutils.NewString(instanceId), "id")
serversObj.Add(serverObj)
params.Add(serversObj, "servers")
params.Add(jsonutils.NewBool(false), "delete_publicip")
params.Add(jsonutils.NewBool(false), "delete_volume")
_, err = self.ecsClient.Servers.PerformAction2("delete", "", params, "")
return err
}
func (self *SRegion) UpdateVM(instanceId, name string) error {
params := jsonutils.NewDict()
serverObj := jsonutils.NewDict()
serverObj.Add(jsonutils.NewString(name), "name")
params.Add(serverObj, "server")
_, err := self.ecsClient.Servers.Update(instanceId, params)
return err
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876349.html
// 返回job id
func (self *SRegion) RebuildRoot(ctx context.Context, userId, instanceId, passwd, publicKeyName, userData string) (string, error) {
params := jsonutils.NewDict()
reinstallObj := jsonutils.NewDict()
if len(publicKeyName) > 0 {
reinstallObj.Add(jsonutils.NewString(publicKeyName), "keyname")
} else if len(passwd) > 0 {
reinstallObj.Add(jsonutils.NewString(passwd), "adminpass")
} else {
return "", fmt.Errorf("both password and publicKey are empty.")
}
if len(userData) > 0 {
meta := jsonutils.NewDict()
meta.Add(jsonutils.NewString(userData), "user_data")
reinstallObj.Add(meta, "metadata")
}
if len(userId) > 0 {
reinstallObj.Add(jsonutils.NewString(userId), "userid")
}
params.Add(reinstallObj, "os-reinstall")
ret, err := self.ecsClient.ServersV2.PerformAction2("reinstallos", instanceId, params, "")
if err != nil {
return "", err
}
return ret.GetString("job_id")
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876971.html
// 返回job id
func (self *SRegion) ChangeRoot(ctx context.Context, userId, instanceId, imageId, passwd, publicKeyName, userData string) (string, error) {
params := jsonutils.NewDict()
changeOsObj := jsonutils.NewDict()
if len(publicKeyName) > 0 {
changeOsObj.Add(jsonutils.NewString(publicKeyName), "keyname")
} else if len(passwd) > 0 {
changeOsObj.Add(jsonutils.NewString(passwd), "adminpass")
} else {
return "", fmt.Errorf("both password and publicKey are empty.")
}
if len(userData) > 0 {
meta := jsonutils.NewDict()
meta.Add(jsonutils.NewString(userData), "user_data")
changeOsObj.Add(meta, "metadata")
}
if len(userId) > 0 {
changeOsObj.Add(jsonutils.NewString(userId), "userid")
}
changeOsObj.Add(jsonutils.NewString(imageId), "imageid")
params.Add(changeOsObj, "os-change")
ret, err := self.ecsClient.ServersV2.PerformAction2("changeos", instanceId, params, "")
if err != nil {
return "", err
}
return ret.GetString("job_id")
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212692.html
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0110109377.html
// 一键式重置密码 需要安装安装一键式重置密码插件 https://support.huaweicloud.com/usermanual-ecs/zh-cn_topic_0068095385.html
// 目前不支持直接重置密钥
func (self *SRegion) DeployVM(instanceId string, name string, password string, keypairName string, deleteKeypair bool, description string) error {
serverObj := jsonutils.NewDict()
if len(name) > 0 {
serverObj.Add(jsonutils.NewString(name), "name")
}
// if len(description) > 0 {
// serverObj.Add(jsonutils.NewString(description), "description")
// }
if serverObj.Size() > 0 {
params := jsonutils.NewDict()
params.Add(serverObj, "server")
// 这里华为返回的image字段是字符串。和SInstance的定义的image是字典结构不一致。
err := DoUpdate(self.ecsClient.NovaServers.Update, instanceId, params, nil)
if err != nil {
return err
}
}
if len(password) > 0 {
params := jsonutils.NewDict()
passwdObj := jsonutils.NewDict()
passwdObj.Add(jsonutils.NewString(password), "new_password")
params.Add(passwdObj, "reset-password")
err := DoUpdateWithSpec(self.ecsClient.NovaServers.UpdateInContextWithSpec, instanceId, "os-reset-password", params)
if err != nil {
return err
}
}
return nil
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212653.html
func (self *SRegion) ChangeVMConfig(instanceId string, instanceType string) error {
self.ecsClient.Servers.SetVersion("v1.1")
defer self.ecsClient.Servers.SetVersion("v1")
params := jsonutils.NewDict()
resizeObj := jsonutils.NewDict()
resizeObj.Add(jsonutils.NewString(instanceType), "flavorRef")
params.Add(resizeObj, "resize")
_, err := self.ecsClient.Servers.PerformAction2("resize", instanceId, params, "")
return errors.Wrapf(err, "PerformAction2(resize)")
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0142763126.html 微版本2.6及以上?
// https://support.huaweicloud.com/api-ecs/ecs_02_0208.html
func (self *SRegion) GetInstanceVNCUrl(instanceId string) (jsonutils.JSONObject, error) {
params := jsonutils.NewDict()
vncObj := jsonutils.NewDict()
vncObj.Add(jsonutils.NewString("novnc"), "type")
vncObj.Add(jsonutils.NewString("vnc"), "protocol")
params.Add(vncObj, "remote_console")
ret, err := self.ecsClient.Servers.PerformAction2("remote_console", instanceId, params, "remote_console")
if err != nil {
return nil, err
}
if retDict, ok := ret.(*jsonutils.JSONDict); ok {
retDict.Set("protocol", jsonutils.NewString("huawei"))
}
return ret, nil
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0022472987.html
// XEN平台虚拟机device为必选参数。
func (self *SRegion) AttachDisk(instanceId string, diskId string, device string) error {
params := jsonutils.NewDict()
volumeObj := jsonutils.NewDict()
volumeObj.Add(jsonutils.NewString(diskId), "volumeId")
if len(device) > 0 {
volumeObj.Add(jsonutils.NewString(device), "device")
}
params.Add(volumeObj, "volumeAttachment")
_, err := self.ecsClient.Servers.PerformAction2("attachvolume", instanceId, params, "")
return err
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0022472988.html
// 默认非强制卸载。delete_flag=0
func (self *SRegion) DetachDisk(instanceId string, diskId string) error {
path := fmt.Sprintf("detachvolume/%s", diskId)
err := DoDeleteWithSpec(self.ecsClient.Servers.DeleteInContextWithSpec, nil, instanceId, path, nil, nil)
//volume a2091934-2669-4fca-8eb4-a950c1836b3c is not in server 49b053d2-f798-432f-af55-76eb6ef2c769 attach volume list => 磁盘已经被卸载了
if err != nil && strings.Contains(err.Error(), fmt.Sprintf("is not in server")) && strings.Contains(err.Error(), fmt.Sprintf("attach volume list")) {
return nil
}
return err
}
// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0065817702.html
func (self *SRegion) GetInstanceSecrityGroupIds(instanceId string) ([]string, error) {
if len(instanceId) == 0 {
return nil, fmt.Errorf("GetInstanceSecrityGroups instanceId is empty")
}
securitygroups := make([]SSecurityGroup, 0)
ctx := &modules.SManagerContext{InstanceManager: self.ecsClient.NovaServers, InstanceId: instanceId}
err := DoListInContext(self.ecsClient.NovaSecurityGroups.ListInContext, ctx, nil, &securitygroups)
if err != nil {
return nil, err
}
securitygroupIds := []string{}
for _, secgroup := range securitygroups {
securitygroupIds = append(securitygroupIds, secgroup.GetId())
}
return securitygroupIds, nil
}
func (self *SInstance) GetProjectId() string {
return self.EnterpriseProjectId
}
func (self *SInstance) GetError() error {
return nil
}
func updateUserData(userData, osVersion, username, password, publicKey string) (string, error) {
winOS := strings.ToLower(osprofile.OS_TYPE_WINDOWS)
osVersion = strings.ToLower(osVersion)
config := &cloudinit.SCloudConfig{}
if strings.Contains(osVersion, winOS) {
if _config, err := cloudinit.ParseUserDataBase64(userData); err == nil {
config = _config
} else {
log.Debugf("updateWindowsUserData invalid userdata %s", userData)
}
} else {
if _config, err := cloudinit.ParseUserDataBase64(userData); err == nil {
config = _config
} else {
return "", fmt.Errorf("updateLinuxUserData invalid userdata %s", userData)
}
}
user := cloudinit.NewUser(username)
config.RemoveUser(user)
config.DisableRoot = 0
if len(password) > 0 {
config.SshPwauth = cloudinit.SSH_PASSWORD_AUTH_ON
user.Password(password)
config.MergeUser(user)
}
if len(publicKey) > 0 {
user.SshKey(publicKey)
config.MergeUser(user)
}
if strings.Contains(osVersion, winOS) {
userData, err := updateWindowsUserData(config.UserDataPowerShell(), osVersion, username, password)
if err != nil {
return "", errors.Wrap(err, "updateUserData.updateWindowsUserData")
}
return userData, nil
} else {
return config.UserDataBase64(), nil
}
}
func updateWindowsUserData(userData string, osVersion string, username, password string) (string, error) {
// Windows Server 2003, Windows Vista, Windows Server 2008, Windows Server 2003 R2, Windows Server 2000, Windows Server 2012, Windows Server 2003 with SP1, Windows 8
oldVersions := []string{"2000", "2003", "2008", "2012", "Vista"}
isOldVersion := false
for i := range oldVersions {
if strings.Contains(osVersion, oldVersions[i]) {
isOldVersion = true
}
}
shells := ""
if isOldVersion {
shells += fmt.Sprintf("rem cmd\n")
if username == "Administrator" {
shells += fmt.Sprintf("net user %s %s\n", username, password)
} else {
shells += fmt.Sprintf("net user %s %s /add\n", username, password)
shells += fmt.Sprintf("net localgroup administrators %s /add\n", username)
}
shells += fmt.Sprintf("net user %s /active:yes", username)
} else {
if !strings.HasPrefix(userData, "#ps1") {
shells = fmt.Sprintf("#ps1\n%s", userData)
}
}
return base64.StdEncoding.EncodeToString([]byte(shells)), nil
}
func (self *SRegion) SaveImage(instanceId string, opts *cloudprovider.SaveImageOptions) (*SImage, error) {
params := map[string]string{
"name": opts.Name,
"instance_id": instanceId,
}
if len(opts.Notes) > 0 {
params["description"] = func() string {
opts.Notes = strings.ReplaceAll(opts.Notes, "<", "")
opts.Notes = strings.ReplaceAll(opts.Notes, ">", "")
opts.Notes = strings.ReplaceAll(opts.Notes, "\n", "")
if len(opts.Notes) > 1024 {
opts.Notes = opts.Notes[:1024]
}
return opts.Notes
}()
}
resp, err := self.ecsClient.Images.CreateInContextWithSpec(nil, "action", jsonutils.Marshal(params), "")
if err != nil {
return nil, errors.Wrapf(err, "Images.Create")
}
jobId, err := resp.GetString("job_id")
if err != nil {
return nil, errors.Wrapf(err, "resp.GetString(job_id)")
}
err = self.waitTaskStatus(self.ecsClient.Images.ServiceType(), jobId, TASK_SUCCESS, 15*time.Second, 10*time.Minute)
if err != nil {
return nil, errors.Wrapf(err, "waitTaskStatus")
}
imageId, err := self.GetTaskEntityID(self.ecsClient.Images.ServiceType(), jobId, "image_id")
if err != nil {
return nil, errors.Wrapf(err, "GetTaskEntityID")
}
image, err := self.GetImage(imageId)
if err != nil {
return nil, errors.Wrapf(err, "GetImage(%s)", imageId)
}
image.storageCache = self.getStoragecache()
return image, nil
}
func (self *SInstance) SaveImage(opts *cloudprovider.SaveImageOptions) (cloudprovider.ICloudImage, error) {
image, err := self.host.zone.region.SaveImage(self.ID, opts)
if err != nil {
return nil, errors.Wrapf(err, "SaveImage")
}
return image, nil
}