// 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" "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/pkg/errors" "yunion.io/x/pkg/util/billing" "yunion.io/x/pkg/util/cloudinit" "yunion.io/x/pkg/util/osprofile" "yunion.io/x/pkg/utils" "yunion.io/x/cloudmux/pkg/apis" billing_api "yunion.io/x/cloudmux/pkg/apis/billing" api "yunion.io/x/cloudmux/pkg/apis/compute" "yunion.io/x/cloudmux/pkg/cloudprovider" "yunion.io/x/cloudmux/pkg/multicloud" ) 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 EbsInstanceBlockDevice struct { AttachTime time.Time `xml:"attachTime"` DeleteOnTermination bool `xml:"deleteOnTermination"` Status string `xml:"status"` VolumeId string `xml:"volumeId"` } type InstanceBlockDeviceMapping struct { DeviceName *string `xml:"deviceName"` Ebs EbsInstanceBlockDevice `xml:"ebs"` } type SInstance struct { multicloud.SInstanceBase AwsTags host *SHost img *SImage AmiLaunchIndex int64 `xml:"amiLaunchIndex"` Architecture string `xml:"architecture"` BlockDeviceMappings []InstanceBlockDeviceMapping `xml:"blockDeviceMapping>item"` BootMode string `xml:"bootMode"` CapacityReservationId string `xml:"capacityReservationId"` ClientToken string `xml:"clientToken"` CpuOptions struct { CoreCount int `xml:"coreCount"` ThreadsPerCore int `xml:"threadsPerCore"` } `xml:"cpuOptions"` EbsOptimized bool `xml:"ebsOptimized"` ElasticGpuAssociations []struct { ElasticGpuAssociationId string `xml:"elasticGpuAssociationId"` ElasticGpuAssociationState string `xml:"elasticGpuAssociationState"` ElasticGpuAssociationTime string `xml:"elasticGpuAssociationTime"` ElasticGpuId string `xml:"elasticGpuId"` } `xml:"elasticGpuAssociationSet>item"` EnaSupport bool `xml:"enaSupport"` EnclaveOptions struct { Enabled bool `xml:"enabled"` } `xml:"enclaveOptions"` HibernationOptions struct { Configured bool `xml:"configured"` } `xml:"hibernationOptions"` Hypervisor string `xml:"hypervisor"` IamInstanceProfile struct { Id string `xml:"id"` Arn string `xml:"arn"` } `xml:"iamInstanceProfile"` ImageId string `xml:"imageId"` InstanceId string `xml:"instanceId"` InstanceLifecycle string `xml:"instanceLifecycle"` InstanceType string `xml:"instanceType"` KernelId string `xml:"kernelId"` KeyName string `xml:"keyName"` LaunchTime time.Time `xml:"launchTime"` Licenses []struct { LicenseConfigurationArn string `xml:"licenseConfigurationArn"` } `xml:"licenseSet>item"` MetadataOptions struct { HttpEndpoint string `xml:"httpEndpoint"` HttpPutResponseHopLimit int64 `xml:"httpPutResponseHopLimit"` HttpTokens string `xml:"httpTokens"` State string `xml:"state"` } `xml:"metadataOptions"` Monitoring struct { State string `xml:"state"` } `xml:"monitoring"` NetworkInterfaces []SNetworkInterface `xml:"networkInterfaceSet>item"` OutpostArn string `xml:"outpostArn"` Placement struct { Affinity string `xml:"affinity"` AvailabilityZone string `xml:"availabilityZone"` GroupName string `xml:"groupName"` HostId string `xml:"hostId"` HostResourceGroupArn string `xml:"hostResourceGroupArn"` PartitionNumber int64 `xml:"partitionNumber"` SpreadDomain string `xml:"spreadDomain"` Tenancy string `xml:"tenancy"` } `xml:"placement"` Platform string `xml:"platform"` PrivateDnsName string `xml:"privateDnsName"` PrivateIpAddress string `xml:"privateIpAddress"` ProductCodes []struct { ProductCodeId string `xml:"productCode"` ProductCodeType string `xml:"type"` } `xml:"productCodes>item"` PublicDnsName string `xml:"dnsName"` PublicIpAddress string `xml:"ipAddress"` RamdiskId string `xml:"ramdiskId"` RootDeviceName string `xml:"rootDeviceName"` RootDeviceType string `xml:"rootDeviceType"` SecurityGroups []struct { GroupId string `xml:"groupId"` GroupName string `xml:"groupName"` } `xml:"groupSet>item"` SourceDestCheck bool `xml:"sourceDestCheck"` SpotInstanceRequestId string `xml:"spotInstanceRequestId"` SriovNetSupport string `xml:"sriovNetSupport"` State struct { Code int64 `xml:"code"` Name string `xml:"name"` } `xml:"instanceState"` StateReason struct { Code string `xml:"code"` Message string `xml:"message"` } `xml:"stateReason"` StateTransitionReason string `xml:"reason"` SubnetId string `xml:"subnetId"` VirtualizationType string `xml:"virtualizationType"` VpcId string `xml:"vpcId"` } func (self *SInstance) UpdateUserData(userData string) error { return self.host.zone.region.ModifyInstanceAttribute(self.InstanceId, &SInstanceAttr{UserData: userData}) } func (self *SInstance) GetUserData() (string, error) { ret, err := self.host.zone.region.DescribeInstanceAttribute(self.InstanceId, &InstanceAttributeInput{UserData: true}) if err != nil { return "", errors.Wrapf(err, "DescribeInstanceAttribute") } udata, err := base64.StdEncoding.DecodeString(ret.UserData.Value) return string(udata), err } type InstanceAttribute struct { UserData struct { Value string `xml:"value"` } `xml:"userData"` } type InstanceAttributeInput struct { UserData bool } func (self *SRegion) DescribeInstanceAttribute(id string, opts *InstanceAttributeInput) (*InstanceAttribute, error) { params := map[string]string{ "InstanceId": id, } if opts.UserData { params["Attribute"] = "userData" } ret := &InstanceAttribute{} err := self.ec2Request("DescribeInstanceAttribute", params, ret) return ret, err } func (self *SInstance) GetId() string { return self.InstanceId } func (self *SInstance) GetName() string { name := self.AwsTags.GetName() if len(name) > 0 { return name } return self.InstanceId } func (self *SInstance) GetHostname() string { return "" } func (self *SInstance) GetGlobalId() string { return self.InstanceId } func (self *SInstance) GetStatus() string { switch self.State.Name { 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 { vm, err := self.host.zone.region.GetInstance(self.InstanceId) if err != nil { return err } self.BlockDeviceMappings = nil self.NetworkInterfaces = nil self.SecurityGroups = nil return jsonutils.Update(self, vm) } func (self *SInstance) GetInstanceType() string { return self.InstanceType } func (self *SInstance) GetSecurityGroupIds() ([]string, error) { ret := []string{} for _, group := range self.SecurityGroups { ret = append(ret, group.GroupId) } return ret, nil } func (self *SInstance) GetBillingType() string { return billing_api.BILLING_TYPE_POSTPAID } func (self *SInstance) GetCreatedAt() time.Time { return self.LaunchTime } func (self *SInstance) GetExpiredAt() time.Time { return time.Time{} } func (self *SInstance) GetIHost() cloudprovider.ICloudHost { return self.host } func (self *SInstance) GetThroughput() int { return 0 } func (self *SInstance) GetInternetMaxBandwidthOut() int { return 0 } func (self *SInstance) GetIDisks() ([]cloudprovider.ICloudDisk, error) { disks, err := self.host.zone.region.GetDisks(self.InstanceId, "", "", nil) if err != nil { return nil, errors.Wrap(err, "GetDisks") } ret := []cloudprovider.ICloudDisk{} for i := 0; i < len(disks); i += 1 { store, err := self.host.zone.getStorageByCategory(disks[i].VolumeType) if err != nil { return nil, errors.Wrap(err, "getStorageByCategory") } disks[i].storage = store if disks[i].getDevice() == self.RootDeviceName { ret = append([]cloudprovider.ICloudDisk{&disks[i]}, ret...) } else { ret = append(ret, &disks[i]) } } return ret, nil } func (self *SInstance) GetINics() ([]cloudprovider.ICloudNic, error) { var ( networkInterfaces = self.NetworkInterfaces nics = make([]cloudprovider.ICloudNic, 0) ) for _, networkInterface := range networkInterfaces { nic := SInstanceNic{ instance: self, id: networkInterface.NetworkInterfaceId, ipAddr: networkInterface.PrivateIpAddress, macAddr: networkInterface.MacAddress, } for _, ip6 := range networkInterface.IPv6AddressesSet { if len(ip6.IPv6Address) > 0 { nic.ip6Addr = ip6.IPv6Address break } } nics = append(nics, &nic) } return nics, nil } func (self *SInstance) GetIEIP() (cloudprovider.ICloudEIP, error) { if len(self.PublicIpAddress) > 0 { eip, err := self.host.zone.region.GetEipByIpAddress(self.PublicIpAddress) if err != nil { if errors.Cause(err) == cloudprovider.ErrNotFound { eip := SEipAddress{region: self.host.zone.region} eip.region = self.host.zone.region eip.PublicIp = self.PublicIpAddress eip.InstanceId = self.InstanceId eip.AllocationId = self.InstanceId // fixed. AllocationId等于InstanceId即表示为 仿真EIP。 return &eip, nil } return nil, err } return eip, nil } for _, nic := range self.NetworkInterfaces { if len(nic.Association.PublicIp) > 0 { eip := SEipAddress{region: self.host.zone.region} eip.region = self.host.zone.region eip.PublicIp = nic.Association.PublicIp eip.InstanceId = self.InstanceId eip.AllocationId = self.InstanceId // fixed. AllocationId等于InstanceId即表示为 仿真EIP。 return &eip, nil } } return nil, errors.Wrapf(cloudprovider.ErrNotFound, "empty eip") } func (self *SInstance) GetVcpuCount() int { return self.CpuOptions.CoreCount * self.CpuOptions.ThreadsPerCore } func (self *SInstance) GetVmemSizeMB() int { instanceType, _ := self.host.zone.region.GetInstanceType(self.InstanceType) if instanceType != nil { return instanceType.MemoryInfo.SizeInMiB } return 0 } 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() cloudprovider.TOsType { if len(self.Platform) > 0 { return cloudprovider.TOsType(osprofile.NormalizeOSType(self.Platform)) } return cloudprovider.OsTypeLinux } func (self *SInstance) GetFullOsName() string { img, err := self.GetImage() if err != nil { return "" } return img.ImageName } func (self *SInstance) GetBios() cloudprovider.TBiosType { img, err := self.GetImage() if err != nil { log.Errorf("GetImage fail %s", err) return cloudprovider.BIOS } return img.GetBios() } func (self *SInstance) GetOsArch() string { if len(self.Architecture) > 0 { switch self.Architecture { case "arm64": return apis.OS_ARCH_AARCH64 case "i386": return apis.OS_ARCH_X86 case "x86_64": return apis.OS_ARCH_X86_64 default: return apis.OS_ARCH_X86_64 } } img, err := self.GetImage() if err != nil { return apis.OS_ARCH_X86_64 } return img.GetOsArch() } func (self *SInstance) GetOsDist() string { img, err := self.GetImage() if err != nil { log.Errorf("GetImage fail %s", err) return "" } return img.GetOsDist() } func (self *SInstance) GetOsVersion() string { img, err := self.GetImage() if err != nil { log.Errorf("GetImage fail %s", err) return "" } return img.GetOsVersion() } func (self *SInstance) GetOsLang() string { img, err := self.GetImage() if err != nil { log.Errorf("GetImage fail %s", err) return "" } return img.GetOsLang() } func (self *SInstance) GetMachine() string { return "pc" } func (self *SInstance) SetSecurityGroups(secgroupIds []string) error { return self.host.zone.region.assignSecurityGroups(secgroupIds, 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, opts *cloudprovider.ServerStopOptions) error { err := cloudprovider.Wait(time.Second*4, time.Minute*10, func() (bool, error) { if utils.IsInStringArray(self.State.Name, []string{"running", "pending", "stopping", "stopped"}) { return true, nil } err := self.Refresh() if err != nil { return false, err } return false, nil }) if err != nil { log.Errorf("wait instance status stoped, current status: %s error: %v", self.State.Name, err) } return self.host.zone.region.StopVM(self.InstanceId, opts.IsForce) } func (self *SInstance) DeleteVM(ctx context.Context) error { err := cloudprovider.Wait(time.Second*4, time.Minute*10, func() (bool, error) { if utils.IsInStringArray(self.State.Name, []string{"running", "pending", "stopping", "stopped"}) { return true, nil } err := self.Refresh() if err != nil { return false, err } return false, nil }) if err != nil { log.Errorf("wait instance status stoped to delete, current status: %s error: %v", self.State.Name, err) } return self.host.zone.region.DeleteVM(self.InstanceId) } func (self *SInstance) UpdateVM(ctx context.Context, input cloudprovider.SInstanceUpdateOptions) error { return self.host.zone.region.UpdateVM(self.InstanceId, input) } func (self *SRegion) UpdateVM(instanceId string, input cloudprovider.SInstanceUpdateOptions) error { return self.setTags("instance", instanceId, map[string]string{"Name": input.NAME, "Description": input.Description}, false) } 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.KeyName 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(string(self.GetOsType())) destOsType := strings.ToLower(string(image.GetOsType())) winOS := strings.ToLower(osprofile.OS_TYPE_WINDOWS) cloudconfig := &cloudinit.SCloudConfig{} if srcOsType != winOS && len(udata) > 0 { _cloudconfig, err := cloudinit.ParseUserData(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, image, desc.SysSizeGB, keypairName, userdata) if err != nil { return "", err } return diskId, nil } func (self *SInstance) DeployVM(ctx context.Context, opts *cloudprovider.SInstanceDeployOptions) error { return cloudprovider.ErrNotSupported } 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.InstanceId, instanceType) } func (self *SInstance) GetVNCInfo(input *cloudprovider.ServerVncInput) (*cloudprovider.ServerVncOutput, error) { return nil, cloudprovider.ErrNotSupported } func (self *SInstance) GetImage() (*SImage, error) { if self.img != nil { return self.img, nil } img, err := self.host.zone.region.GetImage(self.ImageId) if err != nil { return nil, errors.Wrap(err, "GetImage") } self.img = img return self.img, nil } func (self *SInstance) AttachDisk(ctx context.Context, diskId string) error { img, err := self.GetImage() if err != nil { return errors.Wrap(err, "GetImage") } err = self.Refresh() if err != nil { return err } deviceNames := img.GetBlockDeviceNames() for _, dev := range self.BlockDeviceMappings { if dev.DeviceName != nil && len(*dev.DeviceName) > 0 { deviceNames = append(deviceNames, *dev.DeviceName) } } name, err := NextDeviceName(deviceNames) if err != nil { return err } err = self.host.zone.region.AttachDisk(self.InstanceId, diskId, name) if err != nil { return err } 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.VpcId) } func (self *SRegion) GetInstances(zoneId, imageId string, ids []string) ([]SInstance, error) { params := map[string]string{} idx := 1 if len(zoneId) > 0 { params[fmt.Sprintf("Filter.%d.Name", idx)] = "availability-zone" params[fmt.Sprintf("Filter.%d.Value.1", idx)] = zoneId idx++ } if len(imageId) > 0 { params[fmt.Sprintf("Filter.%d.Name", idx)] = "image-id" params[fmt.Sprintf("Filter.%d.Value.1", idx)] = imageId idx++ } // skip terminated instance params[fmt.Sprintf("Filter.%d.Name", idx)] = "instance-state-name" for i, state := range []string{"pending", "running", "shutting-down", "stopping", "stopped"} { params[fmt.Sprintf("Filter.%d.Value.%d", idx, i+1)] = state } idx++ for i, id := range ids { params[fmt.Sprintf("InstanceId.%d", i+1)] = id } ret := []SInstance{} for { part := struct { NextToken string `xml:"nextToken"` ReservationSet []struct { InstancesSet []SInstance `xml:"instancesSet>item"` } `xml:"reservationSet>item"` }{} err := self.ec2Request("DescribeInstances", params, &part) if err != nil { return nil, err } for _, res := range part.ReservationSet { ret = append(ret, res.InstancesSet...) } if len(part.ReservationSet) == 0 || len(part.NextToken) == 0 { break } params["NextToken"] = part.NextToken } return ret, nil } func (self *SRegion) GetInstance(instanceId string) (*SInstance, error) { instances, err := self.GetInstances("", "", []string{instanceId}) if err != nil { return nil, errors.Wrap(err, "GetInstances") } for i := range instances { if instances[i].InstanceId == instanceId { return &instances[i], nil } } return nil, errors.Wrapf(cloudprovider.ErrNotFound, "%s", instanceId) } func (self *SRegion) GetInstanceIdByImageId(imageId string) (string, error) { instances, err := self.GetInstances("", imageId, nil) if err != nil { return "", err } for i := range instances { return instances[i].InstanceId, nil } return "", fmt.Errorf("instance launch with image %s not found", imageId) } func (self *SRegion) CreateInstance(name string, image *SImage, instanceType string, subnetId string, secgroupIds []string, zoneId string, desc string, disks []cloudprovider.SDiskInfo, ipAddr string, keypair string, userData string, tags map[string]string, enableMonitorAgent bool, ) (*SInstance, error) { params, devNames := map[string]string{}, image.GetBlockDeviceNames() for i, disk := range disks { deviceName := image.RootDeviceName if i == 0 && len(deviceName) == 0 { deviceName = "/dev/sda1" devNames = append(devNames, deviceName) } if i > 0 { var err error deviceName, err = NextDeviceName(devNames) if err != nil { return nil, errors.Wrapf(err, "NextDeviceName") } devNames = append(devNames, deviceName) } params[fmt.Sprintf("BlockDeviceMapping.%d.DeviceName", i+1)] = deviceName params[fmt.Sprintf("BlockDeviceMapping.%d.Ebs.DeleteOnTermination", i+1)] = "true" params[fmt.Sprintf("BlockDeviceMapping.%d.Ebs.VolumeSize", i+1)] = fmt.Sprintf("%d", disk.SizeGB) params[fmt.Sprintf("BlockDeviceMapping.%d.Ebs.VolumeType", i+1)] = disk.StorageType iops := disk.Iops if iops == 0 { iops = int(GenDiskIops(disk.StorageType, disk.SizeGB)) } if utils.IsInStringArray(disk.StorageType, []string{ api.STORAGE_IO1_SSD, api.STORAGE_IO2_SSD, api.STORAGE_GP3_SSD, }) { params[fmt.Sprintf("BlockDeviceMapping.%d.Ebs.Iops", i+1)] = fmt.Sprintf("%d", iops) } if disk.Throughput >= 125 && disk.Throughput <= 1000 && disk.StorageType == api.STORAGE_GP3_SSD { params[fmt.Sprintf("BlockDeviceMapping.%d.Ebs.Throughput", i+1)] = fmt.Sprintf("%d", disk.Throughput) } } tagIdx := 1 params[fmt.Sprintf("TagSpecification.1.ResourceType")] = "instance" params[fmt.Sprintf("TagSpecification.1.Tag.%d.Key", tagIdx)] = "Name" params[fmt.Sprintf("TagSpecification.1.Tag.%d.Value", tagIdx)] = name tagIdx++ if len(desc) > 0 { params[fmt.Sprintf("TagSpecification.1.Tag.%d.Key", tagIdx)] = "Description" params[fmt.Sprintf("TagSpecification.1.Tag.%d.Value", tagIdx)] = desc tagIdx++ } for k, v := range tags { params[fmt.Sprintf("TagSpecification.1.Tag.%d.Key", tagIdx)] = k params[fmt.Sprintf("TagSpecification.1.Tag.%d.Value", tagIdx)] = v tagIdx++ } tagIdx = 1 for k, v := range tags { params[fmt.Sprintf("TagSpecification.2.ResourceType")] = "volume" params[fmt.Sprintf("TagSpecification.2.Tag.%d.Key", tagIdx)] = k params[fmt.Sprintf("TagSpecification.2.Tag.%d.Value", tagIdx)] = v tagIdx++ } params["ImageId"] = image.ImageId params["InstanceType"] = instanceType params["MaxCount"] = "1" params["MinCount"] = "1" params["Placement.AvailabilityZone"] = zoneId params["Monitoring.Enabled"] = fmt.Sprintf("%v", enableMonitorAgent) // keypair if len(keypair) > 0 { params["KeyName"] = keypair } // user data if len(userData) > 0 { params["UserData"] = userData } // ip address if len(ipAddr) > 0 { params["PrivateIpAddress"] = ipAddr } // subnet id if len(subnetId) > 0 { params["SubnetId"] = subnetId } // security group for i, id := range secgroupIds { params[fmt.Sprintf("SecurityGroupId.%d", i+1)] = id } ret := struct { InstancesSet []SInstance `xml:"instancesSet>item"` }{} err := self.ec2Request("RunInstances", params, &ret) if err != nil { return nil, errors.Wrapf(err, "RunInstances") } for i := range ret.InstancesSet { return &ret.InstancesSet[i], nil } return nil, errors.Wrapf(cloudprovider.ErrNotFound, "after created") } func (self *SRegion) StartVM(instanceId string) error { params := map[string]string{ "InstanceId.1": instanceId, } ret := struct{}{} return self.ec2Request("StartInstances", params, &ret) } func (self *SRegion) StopVM(instanceId string, isForce bool) error { params := map[string]string{ "InstanceId.1": instanceId, } if isForce { params["Force"] = "true" } ret := struct{}{} return self.ec2Request("StopInstances", params, &ret) } func (self *SRegion) DeleteVM(instanceId string) error { disableApiTermination := false err := self.ModifyInstanceAttribute(instanceId, &SInstanceAttr{ DisableApiTermination: &disableApiTermination, }) if err != nil { return err } params := map[string]string{ "InstanceId.1": instanceId, } ret := struct{}{} return self.ec2Request("TerminateInstances", params, &ret) } func (self *SRegion) ReplaceSystemDisk(ctx context.Context, instanceId string, image *SImage, sysDiskSizeGB int, keypair string, userdata string) (string, error) { instance, err := self.GetInstance(instanceId) if err != nil { return "", err } disks, err := self.GetDisks(instanceId, instance.Placement.AvailabilityZone, "", nil) if err != nil { return "", err } var rootDisk *SDisk for _, disk := range disks { if disk.getDevice() == instance.RootDeviceName { 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.VolumeId) subnetId := instance.SubnetId // create tmp server tempName := fmt.Sprintf("__tmp_%s", instance.GetName()) vm, err := self.CreateInstance(tempName, image, instance.InstanceType, subnetId, []string{}, instance.Placement.AvailabilityZone, instance.GetDescription(), []cloudprovider.SDiskInfo{{SizeGB: sysDiskSizeGB, StorageType: rootDisk.VolumeType}}, "", keypair, userdata, nil, false, ) if err == nil { defer self.DeleteVM(vm.InstanceId) } else { return "", fmt.Errorf("ReplaceSystemDisk create temp server failed.") } err = cloudprovider.Wait(time.Second*2, time.Minute*10, func() (bool, error) { instance, err := self.GetInstance(vm.InstanceId) if err != nil { return false, errors.Wrapf(err, "GetInstance") } log.Debugf("wait temp vm %s running, current status: %s", vm.InstanceId, instance.GetStatus()) if instance.GetStatus() == api.VM_RUNNING { return true, nil } return false, nil }) if err != nil { log.Errorf("wait temp vm %s running error: %v", vm.InstanceId, err) } err = self.StopVM(vm.InstanceId, true) if err != nil { return "", errors.Wrapf(err, "StopVM") } err = cloudprovider.Wait(time.Second*2, time.Minute*10, func() (bool, error) { instance, err := self.GetInstance(vm.InstanceId) if err != nil { return false, errors.Wrapf(err, "GetInstance") } log.Debugf("wait temp vm %s stop, current status: %s", vm.InstanceId, instance.GetStatus()) if instance.GetStatus() == api.VM_READY { return true, nil } return false, nil }) if err != nil { log.Errorf("wait temp vm %s stop error: %v", vm.InstanceId, err) } // detach disks tempInstance, err := self.GetInstance(vm.InstanceId) if err != nil { return "", errors.Wrapf(err, "GetInstance") } tempRootDiskId := tempInstance.BlockDeviceMappings[0].Ebs.VolumeId err = self.DetachDisk(tempInstance.GetId(), tempRootDiskId) if err != nil { return "", errors.Wrapf(err, "DetachDisk temp vm") } err = self.DetachDisk(instance.GetId(), rootDisk.VolumeId) if err != nil { self.DeleteDisk(tempRootDiskId) return "", errors.Wrapf(err, "DetachDisk") } err = self.AttachDisk(instance.GetId(), tempRootDiskId, rootDisk.getDevice()) if err != nil { self.DeleteDisk(tempRootDiskId) self.AttachDisk(instance.GetId(), rootDisk.VolumeId, rootDisk.getDevice()) return "", errors.Wrapf(err, "ttachDisk") } err = self.ModifyInstanceAttribute(instance.InstanceId, &SInstanceAttr{UserData: userdata}) if err != nil { self.DeleteDisk(tempRootDiskId) self.AttachDisk(instance.GetId(), rootDisk.VolumeId, rootDisk.getDevice()) return "", errors.Wrapf(err, "ModifyInstanceAttribute") } err = self.DeleteDisk(rootDisk.VolumeId) if err != nil { log.Errorf("DeleteDisk %s", rootDisk.VolumeId) } return tempRootDiskId, nil } func (self *SRegion) ChangeVMConfig2(instanceId string, instanceType string) error { return self.ModifyInstanceAttribute(instanceId, &SInstanceAttr{InstanceType: instanceType}) } func (self *SRegion) DetachDisk(instanceId string, diskId string) error { params := map[string]string{ "InstanceId": instanceId, "VolumeId": diskId, } ret := struct{}{} err := self.ec2Request("DetachVolume", params, &ret) if err != nil { if strings.Contains(err.Error(), "in the 'available' state") { return nil } if errors.Cause(err) == cloudprovider.ErrNotFound { return nil } return errors.Wrapf(err, "DetachVolume") } return nil } func (self *SRegion) AttachDisk(instanceId string, diskId string, deviceName string) error { params := map[string]string{ "InstanceId": instanceId, "VolumeId": diskId, "Device": deviceName, } ret := struct{}{} return self.ec2Request("AttachVolume", params, &ret) } type SInstanceAttr struct { DisableApiTermination *bool InstanceType string UserData string } func (self *SRegion) ModifyInstanceAttribute(instanceId string, opts *SInstanceAttr) error { params := map[string]string{ "InstanceId": instanceId, } if len(opts.InstanceType) > 0 { params["InstanceType.Value"] = opts.InstanceType } if len(opts.UserData) > 0 { params["UserData.Value"] = opts.UserData } if opts.DisableApiTermination != nil { params["DisableApiTermination.Value"] = fmt.Sprintf("%v", opts.DisableApiTermination) } ret := struct{}{} return self.ec2Request("ModifyInstanceAttribute", params, &ret) } func (self *SRegion) GetPasswordData(instanceId string) (string, error) { params := map[string]string{ "InstanceId": instanceId, } ret := struct { PasswordData string `xml:"passwordData"` }{} err := self.ec2Request("GetPasswordData", params, &ret) if err != nil { return "", errors.Wrapf(err, "GetPasswordData") } return ret.PasswordData, nil } func (self *SInstance) Renew(bc billing.SBillingCycle) error { return cloudprovider.ErrNotSupported } func (self *SInstance) GetProjectId() string { return "" } func (self *SInstance) GetError() error { return nil } func (self *SInstance) SetTags(tags map[string]string, replace bool) error { return self.host.zone.region.setTags("instance", self.InstanceId, tags, replace) } func (self *SInstance) GetAccountId() string { identity, err := self.host.zone.region.client.GetCallerIdentity() if err != nil { log.Errorf("GetCallerIdentity %v", err) return "" } return identity.Account } func (self *SRegion) SaveImage(instanceId string, opts *cloudprovider.SaveImageOptions) (*SImage, error) { params := map[string]string{ "Description": opts.Notes, "InstanceId": instanceId, "Name": opts.Name, } ret := struct { ImageId string `xml:"imageId"` }{} err := self.ec2Request("CreateImage", params, &ret) if err != nil { return nil, errors.Wrapf(err, "CreateImage") } err = cloudprovider.Wait(time.Second*10, time.Minute*5, func() (bool, error) { _, err := self.GetImage(ret.ImageId) if err != nil { if errors.Cause(err) == cloudprovider.ErrNotFound { return false, nil } return false, errors.Wrapf(err, "GetImage(%s)", ret.ImageId) } return true, nil }) if err != nil { return nil, errors.Wrapf(err, "wait for image created") } image, err := self.GetImage(ret.ImageId) if err != nil { return nil, errors.Wrapf(err, "GetImage(%s)", ret.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.InstanceId, opts) if err != nil { return nil, errors.Wrapf(err, "SaveImage") } return image, nil } func (ins *SInstance) GetDescription() string { return ins.AwsTags.GetDescription() }