// 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 aws
import (
"context"
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go/service/ec2"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/osprofile"
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/util/billing"
"yunion.io/x/onecloud/pkg/util/cloudinit"
)
const (
InstanceStatusPending = "pending"
InstanceStatusRunning = "running"
InstanceStatusShutting = "shutting-down"
InstanceStatusTerminated = "terminated"
InstanceStatusStopping = "stopping"
InstanceStatusStopped = "stopped"
)
type InstanceChargeType string
type SIpAddress struct {
IpAddress []string
}
type SSecurityGroupIds struct {
SecurityGroupId []string
}
type SVpcAttributes struct {
PrivateIpAddress SIpAddress
NetworkId string // subnet id
VpcId string
}
type SInstance struct {
multicloud.SInstanceBase
host *SHost
RegionId string
ZoneId string
InstanceId string
ImageId string
HostName string
InstanceName string
InstanceType string
Cpu int
Memory int // MB
IoOptimized bool
KeyPairName string
CreationTime time.Time // LaunchTime
ExpiredTime time.Time
ProductCodes []string
PublicDNSName string
InnerIpAddress SIpAddress
PublicIpAddress SIpAddress
RootDeviceName string
Status string // state
VlanId string // subnet ID ?
VpcAttributes SVpcAttributes
SecurityGroupIds SSecurityGroupIds
NetworkInterfaces SNetworkInterfaces
EipAddress SEipAddress
Disks []string
DeviceNames []string
OSName string
OSType string
Description string
// 这些貌似都没啥用
// AutoReleaseTime string
// DeviceAvailable bool
// GPUAmount int
// GPUSpec string
// InstanceChargeType InstanceChargeType
// InstanceNetworkType string
// InstanceTypeFamily string
// InternetChargeType string
// InternetMaxBandwidthIn int
// InternetMaxBandwidthOut int
// OperationLocks SOperationLocks
// Recyclable bool
// SerialNumber string
// SpotPriceLimit string
// SpotStrategy string
// StartTime time.Time
// StoppedMode string
}
func (self *SInstance) UpdateUserData(userData string) error {
udata := &ec2.BlobAttributeValue{}
udata.SetValue([]byte(userData))
input := &ec2.ModifyInstanceAttributeInput{}
input.SetUserData(udata)
input.SetInstanceId(self.GetId())
_, err := self.host.zone.region.ec2Client.ModifyInstanceAttribute(input)
if err != nil {
return err
}
return nil
}
func (self *SInstance) GetUserData() (string, error) {
input := &ec2.DescribeInstanceAttributeInput{}
input.SetInstanceId(self.GetId())
input.SetAttribute("userData")
ret, err := self.host.zone.region.ec2Client.DescribeInstanceAttribute(input)
if err != nil {
return "", err
}
d := StrVal(ret.UserData.Value)
udata, err := base64.StdEncoding.DecodeString(d)
if err != nil {
return "", fmt.Errorf("GetUserData decode user data %s", err)
}
return string(udata), nil
}
func (self *SInstance) GetId() string {
return self.InstanceId
}
func (self *SInstance) GetName() string {
if len(self.InstanceName) > 0 {
return self.InstanceName
}
return self.GetId()
}
func (self *SInstance) GetGlobalId() string {
return self.InstanceId
}
func (self *SInstance) GetStatus() string {
switch self.Status {
case InstanceStatusRunning:
return api.VM_RUNNING
case InstanceStatusPending: // todo: pending ?
return api.VM_STARTING
case InstanceStatusStopping:
return api.VM_STOPPING
case InstanceStatusStopped:
return api.VM_READY
default:
return api.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) IsEmulated() bool {
return false
}
func (self *SInstance) GetInstanceType() string {
return self.InstanceType
}
func (self *SInstance) GetSecurityGroupIds() ([]string, error) {
return self.SecurityGroupIds.SecurityGroupId, nil
}
func (self *SInstance) GetMetadata() *jsonutils.JSONDict {
data := jsonutils.NewDict()
// todo: add price_key here
// 格式 :regionId::instanceType::osName::os_license::preInstall::tenancy::usageType
// 举例 : cn-northwest-1::c3.2xlarge::linux::NA::NA::shared::boxusage
// 注意:除了空用大写NA.其他一律用小写格式
priceKey := fmt.Sprintf("%s::%s::%s::NA::NA::shared::boxusage", self.RegionId, self.InstanceType, strings.ToLower(self.OSType))
data.Add(jsonutils.NewString(priceKey), "price_key")
tags, err := FetchTags(self.host.zone.region.ec2Client, self.InstanceId)
if err != nil {
log.Errorln(err)
} else {
data.Update(tags)
}
data.Add(jsonutils.NewString(self.host.zone.GetGlobalId()), "zone_ext_id")
if strings.Contains(strings.ToLower(self.OSType), "window") {
if loginKey, err := self.host.zone.region.getPasswordData(self.GetId()); err == nil {
data.Add(jsonutils.NewString(loginKey), "login_key")
}
}
// no need to sync image metadata
/*
if len(self.ImageId) > 0 {
image, err := self.host.zone.region.GetImage(self.ImageId)
if err != nil {
log.Errorf("Failed to find image %s for instance %s zone %s", self.ImageId, self.GetId(), self.ZoneId)
} else {
meta := image.GetMetadata()
if meta != nil {
data.Update(meta)
}
}
}
*/
return data
}
func (self *SInstance) GetBillingType() string {
// todo: implement me
return billing_api.BILLING_TYPE_POSTPAID
}
func (self *SInstance) GetCreatedAt() time.Time {
return self.CreationTime
}
func (self *SInstance) GetExpiredAt() time.Time {
return self.ExpiredTime
}
func (self *SInstance) GetIHost() cloudprovider.ICloudHost {
return self.host
}
func (self *SInstance) GetIDisks() ([]cloudprovider.ICloudDisk, error) {
disks, _, err := self.host.zone.region.GetDisks(self.InstanceId, "", "", nil, 0, 0)
if err != nil {
log.Errorf("fetchDisks fail %s", err)
return nil, err
}
idisks := make([]cloudprovider.ICloudDisk, len(disks))
for i := 0; i < len(disks); i += 1 {
store, err := self.host.zone.getStorageByCategory(disks[i].Category)
if err != nil {
return nil, err
}
disks[i].storage = store
idisks[i] = &disks[i]
}
return idisks, nil
}
func (self *SInstance) GetINics() ([]cloudprovider.ICloudNic, error) {
nics := make([]cloudprovider.ICloudNic, 0)
for _, ip := range self.VpcAttributes.PrivateIpAddress.IpAddress {
nic := SInstanceNic{instance: self, ipAddr: ip}
nics = append(nics, &nic)
}
return nics, nil
}
func (self *SInstance) GetIEIP() (cloudprovider.ICloudEIP, error) {
if len(self.EipAddress.IpAddress) > 0 {
return self.host.zone.region.GetEipByIpAddress(self.EipAddress.IpAddress)
} else if len(self.PublicIpAddress.IpAddress) > 0 {
eip := SEipAddress{}
eip.region = self.host.zone.region
eip.IpAddress = self.PublicIpAddress.IpAddress[0]
eip.InstanceId = self.InstanceId
eip.AllocationId = self.InstanceId // fixed. AllocationId等于InstanceId即表示为 仿真EIP。
eip.Bandwidth = 10000
eip.Status = EIP_STATUS_INUSE
return &eip, nil
} else {
return nil, nil
}
}
func (self *SInstance) GetVcpuCount() int {
return self.Cpu
}
func (self *SInstance) GetVmemSizeMB() int {
return self.Memory
}
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 {
return osprofile.NormalizeOSType(self.OSType)
}
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) AssignSecurityGroup(secgroupId string) error {
return self.SetSecurityGroups([]string{secgroupId})
}
func (self *SInstance) SetSecurityGroups(secgroupIds []string) error {
ids := []*string{}
for i := 0; i < len(secgroupIds); i++ {
ids = append(ids, &secgroupIds[i])
}
return self.host.zone.region.assignSecurityGroups(ids, self.InstanceId)
}
func (self *SInstance) GetHypervisor() string {
return api.HYPERVISOR_AWS
}
func (self *SInstance) StartVM(ctx context.Context) 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
}
if self.GetStatus() == api.VM_RUNNING {
return nil
} else if self.GetStatus() == api.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(ctx context.Context, isForce bool) error {
err := self.host.zone.region.StopVM(self.InstanceId, 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 {
for {
err := self.host.zone.region.DeleteVM(self.InstanceId)
if err != nil && self.Status != InstanceStatusTerminated {
return err
} else {
break
}
}
params := &ec2.DescribeInstancesInput{InstanceIds: []*string{&self.InstanceId}}
return self.host.zone.region.ec2Client.WaitUntilInstanceTerminated(params)
}
func (self *SInstance) UpdateVM(ctx context.Context, name string) error {
return self.host.zone.region.UpdateVM(self.InstanceId, name)
}
func (self *SInstance) RebuildRoot(ctx context.Context, desc *cloudprovider.SManagedVMRebuildRootConfig) (string, error) {
udata, err := self.GetUserData()
if err != nil {
return "", err
}
// compare sysSizeGB
image, err := self.host.zone.region.GetImage(desc.ImageId)
if err != nil {
return "", err
} else {
minSizeGB := image.GetMinOsDiskSizeGb()
if minSizeGB > desc.SysSizeGB {
desc.SysSizeGB = minSizeGB
}
}
// upload keypair
keypairName := self.KeyPairName
if len(desc.PublicKey) > 0 {
keypairName, err = self.host.zone.region.syncKeypair(desc.PublicKey)
if err != nil {
return "", fmt.Errorf("RebuildRoot.syncKeypair %s", err)
}
}
userdata := ""
srcOsType := strings.ToLower(self.GetOSType())
destOsType := strings.ToLower(image.GetOsType())
winOS := strings.ToLower(osprofile.OS_TYPE_WINDOWS)
cloudconfig := &cloudinit.SCloudConfig{}
if srcOsType != winOS && len(udata) > 0 {
_cloudconfig, err := cloudinit.ParseUserDataBase64(udata)
if err != nil {
// 忽略无效的用户数据
log.Debugf("RebuildRoot invalid instance user data %s", udata)
} else {
cloudconfig = _cloudconfig
}
}
if (srcOsType != winOS && destOsType != winOS) || (srcOsType == winOS && destOsType != winOS) {
// linux/windows to linux
loginUser := cloudinit.NewUser(api.VM_AWS_DEFAULT_LOGIN_USER)
loginUser.SudoPolicy(cloudinit.USER_SUDO_NOPASSWD)
if len(desc.PublicKey) > 0 {
loginUser.SshKey(desc.PublicKey)
cloudconfig.MergeUser(loginUser)
} else if len(desc.Password) > 0 {
cloudconfig.SshPwauth = cloudinit.SSH_PASSWORD_AUTH_ON
loginUser.Password(desc.Password)
cloudconfig.MergeUser(loginUser)
}
userdata = cloudconfig.UserDataBase64()
} else {
// linux/windows to windows
data := ""
if len(desc.Password) > 0 {
cloudconfig.SshPwauth = cloudinit.SSH_PASSWORD_AUTH_ON
loginUser := cloudinit.NewUser(api.VM_AWS_DEFAULT_WINDOWS_LOGIN_USER)
loginUser.SudoPolicy(cloudinit.USER_SUDO_NOPASSWD)
loginUser.Password(desc.Password)
cloudconfig.MergeUser(loginUser)
data = fmt.Sprintf("%s", cloudconfig.UserDataPowerShell())
} else {
if len(udata) > 0 {
data = fmt.Sprintf("%s", udata)
}
}
userdata = base64.StdEncoding.EncodeToString([]byte(data))
}
diskId, err := self.host.zone.region.ReplaceSystemDisk(ctx, self.InstanceId, desc.ImageId, desc.SysSizeGB, keypairName, userdata)
if err != nil {
return "", err
}
return diskId, 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.InstanceId, name, password, publicKey, deleteKeypair, description)
}
func (self *SInstance) ChangeConfig(ctx context.Context, config *cloudprovider.SManagedVMChangeConfig) error {
if len(config.InstanceType) > 0 {
return self.ChangeConfig2(ctx, config.InstanceType)
}
return errors.Wrap(errors.ErrClient, "Instance.ChangeConfig.InstanceTypeIsEmpty")
}
func (self *SInstance) ChangeConfig2(ctx context.Context, instanceType string) error {
return self.host.zone.region.ChangeVMConfig2(self.ZoneId, self.InstanceId, instanceType, nil)
}
func (self *SInstance) GetVNCInfo() (jsonutils.JSONObject, error) {
return nil, cloudprovider.ErrNotSupported
}
func (self *SInstance) AttachDisk(ctx context.Context, diskId string) error {
name, err := NextDeviceName(self.DeviceNames)
if err != nil {
return err
}
err = self.host.zone.region.AttachDisk(self.InstanceId, diskId, name)
if err != nil {
return err
}
self.DeviceNames = append(self.DeviceNames, name)
return nil
}
func (self *SInstance) DetachDisk(ctx context.Context, diskId string) error {
return self.host.zone.region.DetachDisk(self.InstanceId, diskId)
}
func (self *SInstance) getVpc() (*SVpc, error) {
return self.host.zone.region.getVpc(self.VpcAttributes.VpcId)
}
func (self *SRegion) GetInstances(zoneId string, ids []string, offset int, limit int) ([]SInstance, int, error) {
params := &ec2.DescribeInstancesInput{}
filters := make([]*ec2.Filter, 0)
if len(zoneId) > 0 {
filters = AppendSingleValueFilter(filters, "availability-zone", zoneId)
}
if len(ids) > 0 {
params = params.SetInstanceIds(ConvertedList(ids))
}
if len(filters) > 0 {
params = params.SetFilters(filters)
}
res, err := self.ec2Client.DescribeInstances(params)
if err != nil {
if strings.Contains(err.Error(), "InvalidInstanceID.NotFound") {
return nil, 0, ErrorNotFound()
} else {
return nil, 0, err
}
}
instances := []SInstance{}
for _, reservation := range res.Reservations {
for _, instance := range reservation.Instances {
if err := FillZero(instance); err != nil {
return nil, 0, err
}
// 不同步已经terminated的主机
if *instance.State.Name == ec2.InstanceStateNameTerminated {
continue
}
tagspec := TagSpec{}
tagspec.LoadingEc2Tags(instance.Tags)
disks := []string{}
devicenames := []string{}
for _, d := range instance.BlockDeviceMappings {
if d.Ebs != nil && d.Ebs.VolumeId != nil {
disks = append(disks, *d.Ebs.VolumeId)
devicenames = append(devicenames, *d.DeviceName)
}
}
var secgroups SSecurityGroupIds
for _, s := range instance.SecurityGroups {
if s.GroupId != nil {
secgroups.SecurityGroupId = append(secgroups.SecurityGroupId, *s.GroupId)
}
}
var networkInterfaces SNetworkInterfaces
eipAddress := SEipAddress{}
for _, n := range instance.NetworkInterfaces {
i := SNetworkInterface{
MacAddress: *n.MacAddress,
NetworkInterfaceId: *n.NetworkInterfaceId,
PrivateIpAddress: *n.PrivateIpAddress,
}
networkInterfaces.NetworkInterface = append(networkInterfaces.NetworkInterface, i)
// todo: 可能有多个EIP的情况。目前只支持一个EIP
if n.Association != nil && StrVal(n.Association.IpOwnerId) != "amazon" {
if eipAddress.IpAddress == "" && len(StrVal(n.Association.PublicIp)) > 0 {
eipAddress.IpAddress = *n.Association.PublicIp
}
}
}
var vpcattr SVpcAttributes
vpcattr.VpcId = *instance.VpcId
vpcattr.PrivateIpAddress = SIpAddress{[]string{*instance.PrivateIpAddress}}
vpcattr.NetworkId = *instance.SubnetId
var productCodes []string
for _, p := range instance.ProductCodes {
productCodes = append(productCodes, *p.ProductCodeId)
}
publicIpAddress := SIpAddress{}
if len(*instance.PublicIpAddress) > 0 {
publicIpAddress.IpAddress = []string{*instance.PublicIpAddress}
}
innerIpAddress := SIpAddress{}
if len(*instance.PrivateIpAddress) > 0 {
innerIpAddress.IpAddress = []string{*instance.PrivateIpAddress}
}
szone, err := self.getZoneById(*instance.Placement.AvailabilityZone)
if err != nil {
log.Errorf("getZoneById %s fail %s", *instance.Placement.AvailabilityZone, err)
return nil, 0, err
}
osType := "Linux"
if instance.Platform != nil && len(*instance.Platform) > 0 {
osType = *instance.Platform
}
host := szone.getHost()
vcpu := int(*instance.CpuOptions.CoreCount) * int(*instance.CpuOptions.ThreadsPerCore)
sinstance := SInstance{
RegionId: self.RegionId,
host: host,
ZoneId: *instance.Placement.AvailabilityZone,
InstanceId: *instance.InstanceId,
ImageId: *instance.ImageId,
InstanceType: *instance.InstanceType,
Cpu: vcpu,
IoOptimized: *instance.EbsOptimized,
KeyPairName: *instance.KeyName,
CreationTime: *instance.LaunchTime,
PublicDNSName: *instance.PublicDnsName,
RootDeviceName: *instance.RootDeviceName,
Status: *instance.State.Name,
InnerIpAddress: innerIpAddress,
PublicIpAddress: publicIpAddress,
EipAddress: eipAddress,
InstanceName: tagspec.GetNameTag(),
Description: tagspec.GetDescTag(),
Disks: disks,
DeviceNames: devicenames,
SecurityGroupIds: secgroups,
NetworkInterfaces: networkInterfaces,
VpcAttributes: vpcattr,
ProductCodes: productCodes,
OSName: osType, // todo: 这里在model层回写OSName信息
OSType: osType,
// ExpiredTime:
// VlanId:
// OSType:
}
instances = append(instances, sinstance)
}
}
return instances, len(instances), nil
}
func (self *SRegion) GetInstance(instanceId string) (*SInstance, error) {
if len(instanceId) == 0 {
return nil, fmt.Errorf("GetInstance instanceId should not be empty.")
}
instances, _, err := self.GetInstances("", []string{instanceId}, 0, 1)
if err != nil {
return nil, err
}
if len(instances) == 0 {
return nil, ErrorNotFound()
}
return &instances[0], nil
}
func (self *SRegion) GetInstanceIdByImageId(imageId string) (string, error) {
params := &ec2.DescribeInstancesInput{}
filters := []*ec2.Filter{}
filters = AppendSingleValueFilter(filters, "image-id", imageId)
params.SetFilters(filters)
ret, err := self.ec2Client.DescribeInstances(params)
if err != nil {
return "", err
}
for _, item := range ret.Reservations {
for _, instance := range item.Instances {
return *instance.InstanceId, nil
}
}
return "", fmt.Errorf("instance launch with image %s not found", imageId)
}
func (self *SRegion) CreateInstance(name string, imageId string, instanceType string, SubnetId string, securityGroupId string,
zoneId string, desc string, disks []SDisk, ipAddr string,
keypair string, userData string) (string, error) {
var count int64 = 1
// disk
blockDevices := []*ec2.BlockDeviceMapping{}
for i := range disks {
var ebs ec2.EbsBlockDevice
var deviceName string
disk := disks[i]
if i == 0 {
var size int64
deleteOnTermination := true
size = int64(disk.Size)
ebs = ec2.EbsBlockDevice{
DeleteOnTermination: &deleteOnTermination,
// The st1 volume type cannot be used for boot volumes. Please use a supported boot volume type: standard,io1,gp2.
// the encrypted flag cannot be specified since device /dev/sda1 has a snapshot specified.
// Encrypted: &disk.Encrypted,
VolumeSize: &size,
VolumeType: &disk.Category,
}
deviceName = fmt.Sprintf("/dev/sda1")
} else {
var size int64
size = int64(disk.Size)
ebs = ec2.EbsBlockDevice{
DeleteOnTermination: &disk.DeleteWithInstance,
Encrypted: &disk.Encrypted,
VolumeSize: &size,
VolumeType: &disk.Category,
}
// todo: generator device name
// todo: 这里还需要测试预置硬盘的实例。deviceName是否会冲突。
deviceName = fmt.Sprintf("/dev/sd%s", string(98+i))
}
// io1类型的卷需要指定IOPS参数。这里根据aws网站的建议值进行设置
// 卷每增加1G。IOPS增加50。最大不超过32000
if disk.Category == api.STORAGE_IO1_SSD {
iops := int64(disk.Size * 50)
if iops < 32000 {
ebs.SetIops(iops)
} else {
ebs.SetIops(32000)
}
}
blockDevice := &ec2.BlockDeviceMapping{
DeviceName: &deviceName,
Ebs: &ebs,
}
blockDevices = append(blockDevices, blockDevice)
}
// tags
tags := TagSpec{ResourceType: "instance"}
tags.SetNameTag(name)
tags.SetDescTag(desc)
ec2TagSpec, err := tags.GetTagSpecifications()
if err != nil {
return "", err
}
params := ec2.RunInstancesInput{
ImageId: &imageId,
InstanceType: &instanceType,
MaxCount: &count,
MinCount: &count,
BlockDeviceMappings: blockDevices,
Placement: &ec2.Placement{AvailabilityZone: &zoneId},
TagSpecifications: []*ec2.TagSpecification{ec2TagSpec},
}
// keypair
if len(keypair) > 0 {
params.SetKeyName(keypair)
}
// user data
if len(userData) > 0 {
params.SetUserData(userData)
}
// ip address
if len(ipAddr) > 0 {
params.SetPrivateIpAddress(ipAddr)
}
// subnet id
if len(SubnetId) > 0 {
params.SetSubnetId(SubnetId)
}
// security group
if len(securityGroupId) > 0 {
params.SetSecurityGroupIds([]*string{&securityGroupId})
}
res, err := self.ec2Client.RunInstances(¶ms)
if err != nil {
log.Errorf("CreateInstance fail %s", err)
return "", err
}
if len(res.Instances) == 1 {
return *res.Instances[0].InstanceId, nil
} else {
msg := fmt.Sprintf("CreateInstance fail: %d instance created. ", len(res.Instances))
log.Errorf(msg)
return "", fmt.Errorf(msg)
}
}
func (self *SRegion) GetInstanceStatus(instanceId string) (string, error) {
instance, err := self.GetInstance(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
}
func (self *SRegion) StartVM(instanceId string) error {
if err := self.instanceStatusChecking(instanceId, InstanceStatusStopped); err != nil {
return err
}
params := &ec2.StartInstancesInput{}
params.SetInstanceIds([]*string{&instanceId})
_, err := self.ec2Client.StartInstances(params)
return err
}
func (self *SRegion) StopVM(instanceId string, isForce bool) error {
if err := self.instanceStatusChecking(instanceId, InstanceStatusRunning); err != nil {
return err
}
params := &ec2.StopInstancesInput{}
params.SetInstanceIds([]*string{&instanceId})
_, err := self.ec2Client.StopInstances(params)
return err
}
func (self *SRegion) DeleteVM(instanceId string) error {
if err := self.instanceStatusChecking(instanceId, InstanceStatusStopped); err != nil {
return err
}
// 检查删除保护状态.如果已开启则先关闭删除保护再进行删除操作
protect, err := self.deleteProtectStatusVM(instanceId)
if err != nil {
return err
}
if protect {
log.Warningf("DeleteVM instance %s which termination protect is in open status", instanceId)
err = self.deleteProtectVM(instanceId, false)
if err != nil {
return err
}
}
params := &ec2.TerminateInstancesInput{}
params.SetInstanceIds([]*string{&instanceId})
_, err = self.ec2Client.TerminateInstances(params)
return err
}
func (self *SRegion) DeployVM(instanceId string, name string, password string, keypairName string, deleteKeypair bool, description string) error {
params := &ec2.CreateTagsInput{}
params.SetResources([]*string{&instanceId})
tagspec := TagSpec{ResourceType: "instance"}
if len(keypairName) > 0 {
return fmt.Errorf("aws not support reset publickey")
}
if len(password) > 0 {
return fmt.Errorf("aws not support set password, use publickey instead")
}
if deleteKeypair {
return fmt.Errorf("aws not support delete publickey")
}
if len(name) > 0 {
tagspec.SetNameTag(name)
}
if len(description) > 0 {
tagspec.SetDescTag(description)
}
ec2Tag, _ := tagspec.GetTagSpecifications()
if len(ec2Tag.Tags) > 0 {
params.SetTags(ec2Tag.Tags)
_, err := self.ec2Client.CreateTags(params)
if err != nil {
return err
}
} else {
log.Debugf("no changes")
}
return nil
}
func (self *SRegion) UpdateVM(instanceId string, hostname string) error {
// https://docs.aws.amazon.com/zh_cn/AWSEC2/latest/UserGuide/set-hostname.html
return fmt.Errorf("aws not support change hostname.")
}
func (self *SRegion) ReplaceSystemDisk(ctx context.Context, instanceId string, imageId string, sysDiskSizeGB int, keypair string, userdata string) (string, error) {
instance, err := self.GetInstance(instanceId)
if err != nil {
return "", err
}
disks, _, err := self.GetDisks(instanceId, instance.ZoneId, "", nil, 0, 0)
if err != nil {
return "", err
}
var rootDisk *SDisk
for _, disk := range disks {
if disk.Type == api.DISK_TYPE_SYS {
rootDisk = &disk
break
}
}
if rootDisk == nil {
return "", fmt.Errorf("can not find root disk of instance %s", instanceId)
}
log.Debugf("ReplaceSystemDisk replace root disk %s", rootDisk.DiskId)
subnetId := ""
if len(instance.VpcAttributes.NetworkId) > 0 {
subnetId = instance.VpcAttributes.NetworkId
}
// create tmp server
tempName := fmt.Sprintf("__tmp_%s", instance.GetName())
_id, err := self.CreateInstance(tempName,
imageId,
instance.InstanceType,
subnetId,
"",
instance.ZoneId,
instance.Description,
[]SDisk{{Size: sysDiskSizeGB, Category: rootDisk.Category}},
"",
keypair,
userdata)
if err == nil {
defer self.DeleteVM(_id)
} else {
log.Debugf("ReplaceSystemDisk create temp server failed. %s", err)
return "", fmt.Errorf("ReplaceSystemDisk create temp server failed.")
}
self.ec2Client.WaitUntilInstanceRunning(&ec2.DescribeInstancesInput{InstanceIds: []*string{&_id}})
err = self.StopVM(_id, true)
if err != nil {
log.Debugf("ReplaceSystemDisk stop temp server failed %s", err)
return "", fmt.Errorf("ReplaceSystemDisk stop temp server failed")
}
self.ec2Client.WaitUntilInstanceStopped(&ec2.DescribeInstancesInput{InstanceIds: []*string{&_id}})
// detach disks
tempInstance, err := self.GetInstance(_id)
if err != nil {
log.Debugf("ReplaceSystemDisk get temp server failed %s", err)
return "", fmt.Errorf("ReplaceSystemDisk get temp server failed")
}
err = self.DetachDisk(instance.GetId(), rootDisk.DiskId)
if err != nil {
log.Debugf("ReplaceSystemDisk detach disk %s: %s", rootDisk.DiskId, err)
return "", err
}
err = self.DetachDisk(tempInstance.GetId(), tempInstance.Disks[0])
if err != nil {
log.Debugf("ReplaceSystemDisk detach disk %s: %s", tempInstance.Disks[0], err)
return "", err
}
self.ec2Client.WaitUntilVolumeAvailable(&ec2.DescribeVolumesInput{VolumeIds: []*string{&rootDisk.DiskId}})
self.ec2Client.WaitUntilVolumeAvailable(&ec2.DescribeVolumesInput{VolumeIds: []*string{&tempInstance.Disks[0]}})
err = self.AttachDisk(instance.GetId(), tempInstance.Disks[0], rootDisk.Device)
if err != nil {
log.Debugf("ReplaceSystemDisk attach disk %s: %s", tempInstance.Disks[0], err)
return "", err
}
self.ec2Client.WaitUntilInstanceStopped(&ec2.DescribeInstancesInput{InstanceIds: []*string{&instanceId}})
self.ec2Client.WaitUntilVolumeInUse(&ec2.DescribeVolumesInput{VolumeIds: []*string{&tempInstance.Disks[0]}})
userdataText, err := base64.StdEncoding.DecodeString(userdata)
if err != nil {
return "", errors.Wrap(err, "SRegion.ReplaceSystemDisk.DecodeString")
}
err = instance.UpdateUserData(string(userdataText))
if err != nil {
log.Debugf("ReplaceSystemDisk update user data %s", err)
return "", fmt.Errorf("ReplaceSystemDisk update user data failed")
}
err = self.DeleteDisk(rootDisk.DiskId)
if err != nil {
log.Debugf("ReplaceSystemDisk delete old disk %s: %s", rootDisk.DiskId, err)
}
return tempInstance.Disks[0], nil
}
func (self *SRegion) ChangeVMConfig2(zoneId string, instanceId string, instanceType string, disks []*SDisk) error {
params := &ec2.ModifyInstanceAttributeInput{}
params.SetInstanceId(instanceId)
t := &ec2.AttributeValue{Value: &instanceType}
params.SetInstanceType(t)
_, err := self.ec2Client.ModifyInstanceAttribute(params)
if err != nil {
return fmt.Errorf("Failed to change vm config, specification not supported. %s", err.Error())
} else {
return nil
}
}
func (self *SRegion) DetachDisk(instanceId string, diskId string) error {
params := &ec2.DetachVolumeInput{}
params.SetInstanceId(instanceId)
params.SetVolumeId(diskId)
log.Debugf("DetachDisk %s", params.String())
_, err := self.ec2Client.DetachVolume(params)
if err != nil {
if strings.Contains(err.Error(), fmt.Sprintf("'%s'is in the 'available' state", diskId)) {
return nil
}
//InvalidVolume.NotFound: The volume 'vol-0a9eeda0a70a8d7fe' does not exist
if strings.Contains(err.Error(), "InvalidVolume.NotFound") {
return nil
}
return errors.Wrap(err, "ec2Client.DetachVolume")
}
return nil
}
func (self *SRegion) AttachDisk(instanceId string, diskId string, deviceName string) error {
params := &ec2.AttachVolumeInput{}
params.SetInstanceId(instanceId)
params.SetVolumeId(diskId)
params.SetDevice(deviceName)
log.Debugf("AttachDisk %s", params.String())
_, err := self.ec2Client.AttachVolume(params)
return err
}
func (self *SRegion) deleteProtectStatusVM(instanceId string) (bool, error) {
p := &ec2.DescribeInstanceAttributeInput{}
p.SetInstanceId(instanceId)
p.SetAttribute("disableApiTermination")
ret, err := self.ec2Client.DescribeInstanceAttribute(p)
if err != nil {
return false, err
}
return *ret.DisableApiTermination.Value, nil
}
func (self *SRegion) deleteProtectVM(instanceId string, disableDelete bool) error {
p2 := &ec2.ModifyInstanceAttributeInput{
DisableApiTermination: &ec2.AttributeBooleanValue{Value: &disableDelete},
InstanceId: &instanceId,
}
_, err := self.ec2Client.ModifyInstanceAttribute(p2)
return err
}
func (self *SRegion) getPasswordData(instanceId string) (string, error) {
params := &ec2.GetPasswordDataInput{}
params.SetInstanceId(instanceId)
ret, err := self.ec2Client.GetPasswordData(params)
if err != nil {
return "", err
}
return *ret.PasswordData, nil
}
func (self *SInstance) CreateDisk(ctx context.Context, sizeMb int, uuid string, driver string) error {
return cloudprovider.ErrNotSupported
}
func (self *SInstance) Renew(bc billing.SBillingCycle) error {
return cloudprovider.ErrNotSupported
}
func (self *SInstance) GetProjectId() string {
return ""
}
func (self *SInstance) GetError() error {
return nil
}