| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281 |
- // 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 aliyun
- import (
- "context"
- "fmt"
- "sort"
- "strings"
- "time"
- alierr "github.com/aliyun/alibaba-cloud-sdk-go/sdk/errors"
- "yunion.io/x/jsonutils"
- "yunion.io/x/log"
- "yunion.io/x/pkg/errors"
- "yunion.io/x/pkg/util/billing"
- "yunion.io/x/pkg/util/imagetools"
- "yunion.io/x/pkg/util/osprofile"
- "yunion.io/x/pkg/utils"
- 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 (
- // Running:运行中
- //Starting:启动中
- //Stopping:停止中
- //Stopped:已停止
- InstanceStatusStopped = "Stopped"
- InstanceStatusRunning = "Running"
- InstanceStatusStopping = "Stopping"
- InstanceStatusStarting = "Starting"
- )
- type SDedicatedHostAttribute struct {
- DedicatedHostId string
- DedicatedHostName string
- }
- type SIpAddress struct {
- IpAddress []string
- }
- type SNetworkInterfaces struct {
- NetworkInterface []SNetworkInterface
- }
- type SOperationLocks struct {
- LockReason []string
- }
- type SSecurityGroupIds struct {
- SecurityGroupId []string
- }
- // {"NatIpAddress":"","PrivateIpAddress":{"IpAddress":["192.168.220.214"]},"VSwitchId":"vsw-2ze9cqwza4upoyujq1thd","VpcId":"vpc-2zer4jy8ix3i8f0coc5uw"}
- type SVpcAttributes struct {
- NatIpAddress string
- PrivateIpAddress SIpAddress
- VSwitchId string
- VpcId string
- }
- type SInstance struct {
- multicloud.SInstanceBase
- AliyunTags
- host *SHost
- osInfo *imagetools.ImageInfo
- // idisks []cloudprovider.ICloudDisk
- AutoReleaseTime string
- ClusterId string
- Cpu int
- CreationTime time.Time
- DedicatedHostAttribute SDedicatedHostAttribute
- Description string
- DeviceAvailable bool
- EipAddress SEipAddress
- ExpiredTime time.Time
- GPUAmount int
- GPUSpec string
- HostName string
- ImageId string
- InnerIpAddress SIpAddress
- InstanceChargeType TChargeType
- InstanceId string
- InstanceName string
- InstanceNetworkType string
- InstanceType string
- InstanceTypeFamily string
- InternetChargeType TInternetChargeType
- InternetMaxBandwidthIn int
- InternetMaxBandwidthOut int
- IoOptimized bool
- KeyPairName string
- Memory int
- NetworkInterfaces SNetworkInterfaces
- OSName string
- OSType string
- OperationLocks SOperationLocks
- PublicIpAddress SIpAddress
- Recyclable bool
- RegionId string
- ResourceGroupId string
- SaleCycle string
- SecurityGroupIds SSecurityGroupIds
- SerialNumber string
- SpotPriceLimit string
- SpotStrategy string
- StartTime time.Time
- Status string
- StoppedMode string
- VlanId string
- VpcAttributes SVpcAttributes
- ZoneId string
- Throughput int
- }
- // {"AutoReleaseTime":"","ClusterId":"","Cpu":1,"CreationTime":"2018-05-23T07:58Z","DedicatedHostAttribute":{"DedicatedHostId":"","DedicatedHostName":""},"Description":"","DeviceAvailable":true,"EipAddress":{"AllocationId":"","InternetChargeType":"","IpAddress":""},"ExpiredTime":"2018-05-30T16:00Z","GPUAmount":0,"GPUSpec":"","HostName":"iZ2ze57isp1ali72tzkjowZ","ImageId":"centos_7_04_64_20G_alibase_201701015.vhd","InnerIpAddress":{"IpAddress":[]},"InstanceChargeType":"PrePaid","InstanceId":"i-2ze57isp1ali72tzkjow","InstanceName":"gaoxianqi-test-7days","InstanceNetworkType":"vpc","InstanceType":"ecs.t5-lc2m1.nano","InstanceTypeFamily":"ecs.t5","InternetChargeType":"PayByBandwidth","InternetMaxBandwidthIn":-1,"InternetMaxBandwidthOut":0,"IoOptimized":true,"Memory":512,"NetworkInterfaces":{"NetworkInterface":[{"MacAddress":"00:16:3e:10:f0:c9","NetworkInterfaceId":"eni-2zecqsagtpztl6x5hu2r","PrimaryIpAddress":"192.168.220.214"}]},"OSName":"CentOS 7.4 64位","OSType":"linux","OperationLocks":{"LockReason":[]},"PublicIpAddress":{"IpAddress":[]},"Recyclable":false,"RegionId":"cn-beijing","ResourceGroupId":"","SaleCycle":"Week","SecurityGroupIds":{"SecurityGroupId":["sg-2zecqsagtpztl6x9zynl"]},"SerialNumber":"df05d9b4-df3d-4400-88d1-5f843f0dd088","SpotPriceLimit":0.000000,"SpotStrategy":"NoSpot","StartTime":"2018-05-23T07:58Z","Status":"Running","StoppedMode":"Not-applicable","VlanId":"","VpcAttributes":{"NatIpAddress":"","PrivateIpAddress":{"IpAddress":["192.168.220.214"]},"VSwitchId":"vsw-2ze9cqwza4upoyujq1thd","VpcId":"vpc-2zer4jy8ix3i8f0coc5uw"},"ZoneId":"cn-beijing-f"}
- func (self *SRegion) GetInstances(zoneId string, ids []string) ([]SInstance, error) {
- params := make(map[string]string)
- params["RegionId"] = self.RegionId
- params["MaxResults"] = "100"
- params["AdditionalAttributes.1"] = "NETWORK_PRIMARY_ENI_IP"
- if len(zoneId) > 0 {
- params["ZoneId"] = zoneId
- }
- if ids != nil && len(ids) > 0 {
- params["InstanceIds"] = jsonutils.Marshal(ids).String()
- }
- ret := make([]SInstance, 0)
- for {
- resp, err := self.ecsRequest("DescribeInstances", params)
- if err != nil {
- return nil, errors.Wrapf(err, "DescribeInstances")
- }
- part := struct {
- Instances struct {
- Instance []SInstance
- }
- NextToken string
- }{}
- err = resp.Unmarshal(&part)
- if err != nil {
- return nil, errors.Wrapf(err, "Unmarshal")
- }
- ret = append(ret, part.Instances.Instance...)
- if len(part.Instances.Instance) == 0 || len(part.NextToken) == 0 {
- break
- }
- params["NextToken"] = part.NextToken
- }
- return ret, nil
- }
- func (self *SInstance) GetSecurityGroupIds() ([]string, error) {
- return self.SecurityGroupIds.SecurityGroupId, nil
- }
- func (self *SInstance) GetIHost() cloudprovider.ICloudHost {
- return self.host
- }
- func (self *SInstance) GetId() string {
- return self.InstanceId
- }
- func (self *SInstance) GetName() string {
- if len(self.InstanceName) > 0 {
- return self.InstanceName
- }
- return self.InstanceId
- }
- func (self *SInstance) GetDescription() string {
- return self.Description
- }
- func (self *SInstance) GetHostname() string {
- return self.HostName
- }
- func (self *SInstance) GetGlobalId() string {
- return self.InstanceId
- }
- func (self *SInstance) IsEmulated() bool {
- return false
- }
- func (self *SInstance) GetInstanceType() string {
- return self.InstanceType
- }
- func (self *SInstance) getVpc() (*SVpc, error) {
- return self.host.zone.region.getVpc(self.VpcAttributes.VpcId)
- }
- type byAttachedTime []SDisk
- func (a byAttachedTime) Len() int { return len(a) }
- func (a byAttachedTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
- func (a byAttachedTime) Less(i, j int) bool {
- switch a[i].GetDiskType() {
- case api.DISK_TYPE_SYS:
- return true
- case api.DISK_TYPE_SWAP:
- switch a[j].GetDiskType() {
- case api.DISK_TYPE_SYS:
- return false
- case api.DISK_TYPE_DATA:
- return true
- }
- case api.DISK_TYPE_DATA:
- if a[j].GetDiskType() != api.DISK_TYPE_DATA {
- return false
- }
- }
- return a[i].AttachedTime.Before(a[j].AttachedTime)
- }
- func (self *SInstance) GetIDisks() ([]cloudprovider.ICloudDisk, error) {
- disks, err := self.host.zone.region.GetDisks(self.InstanceId, "", "", nil, "")
- if err != nil {
- return nil, errors.Wrapf(err, "GetDisks for %s", self.InstanceId)
- }
- sort.Sort(byAttachedTime(disks))
- 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) {
- var (
- networkInterfaces = self.NetworkInterfaces.NetworkInterface
- nics []cloudprovider.ICloudNic
- )
- for _, networkInterface := range networkInterfaces {
- nic := SInstanceNic{
- instance: self,
- id: networkInterface.NetworkInterfaceId,
- ipAddr: networkInterface.PrimaryIpAddress,
- macAddr: networkInterface.MacAddress,
- }
- for _, ipv6 := range networkInterface.Ipv6Sets.Ipv6Set {
- if len(ipv6.Ipv6Address) > 0 {
- nic.ip6Addr = ipv6.Ipv6Address
- }
- }
- nics = append(nics, &nic)
- }
- for _, classicIp := range self.InnerIpAddress.IpAddress {
- nic := SInstanceNic{
- instance: self,
- id: fmt.Sprintf("%s-%s", self.InstanceId, classicIp),
- ipAddr: classicIp,
- classic: true,
- }
- nics = append(nics, &nic)
- }
- return nics, 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 (ins *SInstance) getNormalizedOsInfo() *imagetools.ImageInfo {
- if ins.osInfo == nil {
- osInfo := imagetools.NormalizeImageInfo(ins.OSName, "", ins.OSType, "", "")
- ins.osInfo = &osInfo
- }
- return ins.osInfo
- }
- func (self *SInstance) GetOsType() cloudprovider.TOsType {
- return cloudprovider.TOsType(osprofile.NormalizeOSType(self.OSType))
- }
- func (self *SInstance) GetFullOsName() string {
- return self.OSName
- }
- func (ins *SInstance) GetBios() cloudprovider.TBiosType {
- return cloudprovider.ToBiosType(ins.getNormalizedOsInfo().OsBios)
- }
- func (ins *SInstance) GetOsArch() string {
- return ins.getNormalizedOsInfo().OsArch
- }
- func (ins *SInstance) GetOsDist() string {
- return ins.getNormalizedOsInfo().OsDistro
- }
- func (ins *SInstance) GetOsVersion() string {
- return ins.getNormalizedOsInfo().OsVersion
- }
- func (ins *SInstance) GetOsLang() string {
- return ins.getNormalizedOsInfo().OsLang
- }
- func (self *SInstance) GetMachine() string {
- return "pc"
- }
- func (self *SInstance) GetStatus() string {
- // Running:运行中
- //Starting:启动中
- //Stopping:停止中
- //Stopped:已停止
- switch self.Status {
- case InstanceStatusRunning:
- return api.VM_RUNNING
- case InstanceStatusStarting:
- return api.VM_STARTING
- case InstanceStatusStopping:
- return api.VM_STOPPING
- case InstanceStatusStopped:
- return api.VM_READY
- default:
- return api.VM_UNKNOWN
- }
- }
- type SInstanceStatus struct {
- InstanceId string
- HealthStatus struct {
- Name string
- }
- }
- func (region *SRegion) DescribeInstancesFullStatus(vmId string) (*SInstanceStatus, error) {
- params := map[string]string{
- "InstanceId": vmId,
- }
- resp, err := region.ecsRequest("DescribeInstancesFullStatus", params)
- if err != nil {
- return nil, err
- }
- ret := struct {
- InstanceFullStatusSet struct {
- InstanceFullStatusType []SInstanceStatus
- }
- }{}
- err = resp.Unmarshal(&ret)
- if err != nil {
- return nil, errors.Wrapf(err, "Unmarshal")
- }
- for i := range ret.InstanceFullStatusSet.InstanceFullStatusType {
- if ret.InstanceFullStatusSet.InstanceFullStatusType[i].InstanceId == vmId {
- return &ret.InstanceFullStatusSet.InstanceFullStatusType[i], nil
- }
- }
- return nil, errors.Wrapf(cloudprovider.ErrNotFound, "%s", vmId)
- }
- func (self *SInstance) GetHealthStatus() string {
- status, err := self.host.zone.region.DescribeInstancesFullStatus(self.InstanceId)
- if err != nil {
- return ""
- }
- return strings.ToLower(status.HealthStatus.Name)
- }
- func (self *SInstance) Refresh() error {
- ins, err := self.host.zone.region.GetInstance(self.InstanceId)
- if err != nil {
- return errors.Wrapf(err, "GetInstance %s", self.InstanceId)
- }
- return jsonutils.Update(self, ins)
- }
- func (self *SInstance) GetHypervisor() string {
- return api.HYPERVISOR_ALIYUN
- }
- func (self *SInstance) StartVM(ctx context.Context) error {
- return self.host.zone.region.StartVM(self.InstanceId)
- }
- func (self *SInstance) StopVM(ctx context.Context, opts *cloudprovider.ServerStopOptions) error {
- err := self.host.zone.region.StopVM(self.InstanceId, opts.IsForce, opts.StopCharging)
- if err != nil {
- return errors.Wrapf(err, "StopVM %s", self.InstanceId)
- }
- return cloudprovider.WaitStatus(self, api.VM_READY, 10*time.Second, 5*time.Minute)
- }
- func (self *SInstance) GetVNCInfo(input *cloudprovider.ServerVncInput) (*cloudprovider.ServerVncOutput, error) {
- url, err := self.host.zone.region.GetInstanceVNCUrl(self.InstanceId)
- if err != nil {
- return nil, err
- }
- ret := &cloudprovider.ServerVncOutput{
- Url: url,
- Protocol: "aliyun",
- InstanceId: self.InstanceId,
- Hypervisor: api.HYPERVISOR_ALIYUN,
- OsName: string(self.GetOsType()),
- }
- return ret, nil
- }
- func (self *SInstance) UpdateVM(ctx context.Context, input cloudprovider.SInstanceUpdateOptions) error {
- return self.host.zone.region.UpdateVM(self.InstanceId, input, self.OSType)
- }
- func (self *SInstance) DeployVM(ctx context.Context, opts *cloudprovider.SInstanceDeployOptions) error {
- return self.host.zone.region.DeployVM(self.InstanceId, opts)
- }
- func (self *SInstance) RebuildRoot(ctx context.Context, desc *cloudprovider.SManagedVMRebuildRootConfig) (string, error) {
- keypair := ""
- if len(desc.PublicKey) > 0 {
- var err error
- keypair, err = self.host.zone.region.syncKeypair(desc.PublicKey)
- if err != nil {
- return "", err
- }
- }
- diskId, err := self.host.zone.region.ReplaceSystemDisk(self.InstanceId, desc.ImageId, desc.Password, keypair, desc.SysSizeGB)
- if err != nil {
- return "", err
- }
- return diskId, nil
- }
- func (self *SInstance) ChangeConfig(ctx context.Context, config *cloudprovider.SManagedVMChangeConfig) error {
- isDowngrade, isPrepaid := false, self.GetBillingType() == billing_api.BILLING_TYPE_PREPAID
- if (self.GetVcpuCount() > config.Cpu && config.Cpu > 0) || (self.GetVmemSizeMB() > config.MemoryMB && config.MemoryMB > 0) {
- isDowngrade = true
- }
- instanceTypes := []string{}
- if len(config.InstanceType) > 0 {
- instanceTypes = []string{config.InstanceType}
- } else {
- specs, err := self.host.zone.region.GetMatchInstanceTypes(config.Cpu, config.MemoryMB, 0, self.ZoneId)
- if err != nil {
- return errors.Wrapf(err, "GetMatchInstanceTypes")
- }
- for _, spec := range specs {
- instanceTypes = append(instanceTypes, spec.InstanceTypeId)
- }
- }
- var err error
- for _, instanceType := range instanceTypes {
- if isPrepaid {
- err = self.host.zone.region.ChangePrepaidVMConfig(self.InstanceId, instanceType, isDowngrade)
- if err != nil {
- log.Errorf("ChangePrepaidVMConfig %s error: %v", instanceType, err)
- continue
- }
- } else {
- err = self.host.zone.region.ChangeVMConfig(self.InstanceId, instanceType)
- if err != nil {
- log.Errorf("ChangeVMConfig %s error: %v", instanceType, err)
- continue
- }
- }
- return nil
- }
- if err != nil {
- return errors.Wrapf(err, "ChangeVMConfig")
- }
- return fmt.Errorf("Failed to change vm config, specification not supported")
- }
- func (self *SInstance) GetModificationTypes() ([]cloudprovider.SInstanceModificationType, error) {
- return self.host.zone.region.GetInstanceModificationTypes(self.InstanceId)
- }
- func (self *SRegion) GetInstanceModificationTypes(instanceId string) ([]cloudprovider.SInstanceModificationType, error) {
- params := map[string]string{
- "RegionId": self.RegionId,
- "ResourceId": instanceId,
- "DestinationResource": "InstanceType",
- }
- body, err := self.ecsRequest("DescribeResourcesModification", params)
- if err != nil {
- return nil, errors.Wrapf(err, "DescribeResourcesModification")
- }
- parsed := struct {
- AvailableZones struct {
- AvailableZone []struct {
- AvailableResources struct {
- AvailableResource []struct {
- SupportedResources struct {
- SupportedResource []struct {
- Value string
- Status string
- StatusCategory string
- }
- }
- }
- }
- }
- }
- }{}
- if err := body.Unmarshal(&parsed); err != nil {
- return nil, errors.Wrapf(err, "body.Unmarshal")
- }
- ret := make([]cloudprovider.SInstanceModificationType, 0)
- seen := map[string]struct{}{}
- for _, zone := range parsed.AvailableZones.AvailableZone {
- for _, resource := range zone.AvailableResources.AvailableResource {
- for _, supportedResource := range resource.SupportedResources.SupportedResource {
- if len(supportedResource.Value) == 0 || len(supportedResource.Status) == 0 || strings.ToLower(supportedResource.Status) != "available" {
- continue
- }
- if _, ok := seen[supportedResource.Value]; ok {
- continue
- }
- seen[supportedResource.Value] = struct{}{}
- ret = append(ret, cloudprovider.SInstanceModificationType{InstanceType: supportedResource.Value})
- }
- }
- }
- return ret, nil
- }
- func (self *SInstance) AttachDisk(ctx context.Context, diskId string) error {
- return self.host.zone.region.AttachDisk(self.InstanceId, diskId)
- }
- func (self *SInstance) DetachDisk(ctx context.Context, diskId string) error {
- return cloudprovider.RetryOnError(
- func() error {
- return self.host.zone.region.DetachDisk(self.InstanceId, diskId)
- },
- []string{
- `"Code":"InvalidOperation.Conflict"`,
- },
- 4)
- }
- func (self *SRegion) GetInstance(instanceId string) (*SInstance, error) {
- instances, err := self.GetInstances("", []string{instanceId})
- if err != nil {
- return nil, err
- }
- for i := range instances {
- if instances[i].InstanceId == instanceId {
- return &instances[i], nil
- }
- }
- return nil, errors.Wrapf(cloudprovider.ErrNotFound, "%s", instanceId)
- }
- func (self *SRegion) CreateInstance(name, hostname string, imageId string, instanceType string, securityGroupIds []string,
- zoneId string, desc string, passwd string, disks []SDisk, vSwitchId string, ipAddr string,
- keypair string, userData string, bc *billing.SBillingCycle, projectId, osType string,
- tags map[string]string, publicIp cloudprovider.SPublicIpInfo,
- ) (string, error) {
- params := make(map[string]string)
- params["RegionId"] = self.RegionId
- params["ImageId"] = imageId
- params["InstanceType"] = instanceType
- for _, id := range securityGroupIds {
- params["SecurityGroupId"] = id
- }
- params["ZoneId"] = zoneId
- params["InstanceName"] = name
- if len(hostname) > 0 {
- params["HostName"] = hostname
- }
- params["Description"] = desc
- params["InternetChargeType"] = "PayByTraffic"
- if publicIp.PublicIpBw > 0 {
- params["InternetMaxBandwidthOut"] = fmt.Sprintf("%d", publicIp.PublicIpBw)
- params["InternetMaxBandwidthIn"] = "200"
- }
- if publicIp.PublicIpChargeType == cloudprovider.ElasticipChargeTypeByBandwidth {
- params["InternetChargeType"] = "PayByBandwidth"
- }
- if len(passwd) > 0 {
- params["Password"] = passwd
- } else {
- params["PasswordInherit"] = "True"
- }
- if len(projectId) > 0 {
- params["ResourceGroupId"] = projectId
- }
- //{"Code":"InvalidSystemDiskCategory.ValueNotSupported","HostId":"ecs.aliyuncs.com","Message":"The specified parameter 'SystemDisk.Category' is not support IoOptimized Instance. Valid Values: cloud_efficiency;cloud_ssd. ","RequestId":"9C9A4E99-5196-42A2-80B6-4762F8F75C90"}
- params["IoOptimized"] = "optimized"
- for i, d := range disks {
- if i == 0 {
- params["SystemDisk.Category"] = d.Category
- if d.Category == api.STORAGE_CLOUD_ESSD_PL0 {
- params["SystemDisk.Category"] = api.STORAGE_CLOUD_ESSD
- params["SystemDisk.PerformanceLevel"] = "PL0"
- }
- if d.Category == api.STORAGE_CLOUD_ESSD_PL2 {
- params["SystemDisk.Category"] = api.STORAGE_CLOUD_ESSD
- params["SystemDisk.PerformanceLevel"] = "PL2"
- }
- if d.Category == api.STORAGE_CLOUD_ESSD_PL3 {
- params["SystemDisk.Category"] = api.STORAGE_CLOUD_ESSD
- params["SystemDisk.PerformanceLevel"] = "PL3"
- }
- if d.Category == api.STORAGE_CLOUD_AUTO {
- params["SystemDisk.BurstingEnabled"] = "true"
- }
- params["SystemDisk.Size"] = fmt.Sprintf("%d", d.Size)
- params["SystemDisk.DiskName"] = d.GetName()
- params["SystemDisk.Description"] = d.Description
- } else {
- params[fmt.Sprintf("DataDisk.%d.Size", i)] = fmt.Sprintf("%d", d.Size)
- params[fmt.Sprintf("DataDisk.%d.Category", i)] = d.Category
- if d.Category == api.STORAGE_CLOUD_ESSD_PL0 {
- params[fmt.Sprintf("DataDisk.%d.Category", i)] = api.STORAGE_CLOUD_ESSD
- params[fmt.Sprintf("DataDisk.%d..PerformanceLevel", i)] = "PL0"
- }
- if d.Category == api.STORAGE_CLOUD_ESSD_PL2 {
- params[fmt.Sprintf("DataDisk.%d.Category", i)] = api.STORAGE_CLOUD_ESSD
- params[fmt.Sprintf("DataDisk.%d..PerformanceLevel", i)] = "PL2"
- }
- if d.Category == api.STORAGE_CLOUD_ESSD_PL3 {
- params[fmt.Sprintf("DataDisk.%d.Category", i)] = api.STORAGE_CLOUD_ESSD
- params[fmt.Sprintf("DataDisk.%d..PerformanceLevel", i)] = "PL3"
- }
- if d.Category == api.STORAGE_CLOUD_AUTO {
- params[fmt.Sprintf("DataDisk.%d.BurstingEnabled", i)] = "true"
- }
- params[fmt.Sprintf("DataDisk.%d.DiskName", i)] = d.GetName()
- params[fmt.Sprintf("DataDisk.%d.Description", i)] = d.Description
- params[fmt.Sprintf("DataDisk.%d.Encrypted", i)] = "false"
- }
- }
- params["VSwitchId"] = vSwitchId
- params["PrivateIpAddress"] = ipAddr
- if len(keypair) > 0 {
- params["KeyPairName"] = keypair
- }
- if len(userData) > 0 {
- params["UserData"] = userData
- }
- if len(tags) > 0 {
- tagIdx := 1
- for k, v := range tags {
- params[fmt.Sprintf("Tag.%d.Key", tagIdx)] = k
- params[fmt.Sprintf("Tag.%d.Value", tagIdx)] = v
- tagIdx += 1
- }
- }
- if bc != nil {
- params["InstanceChargeType"] = "PrePaid"
- err := billingCycle2Params(bc, params)
- if err != nil {
- return "", err
- }
- if bc.AutoRenew {
- params["AutoRenew"] = "true"
- params["AutoRenewPeriod"] = "1"
- } else {
- params["AutoRenew"] = "False"
- }
- } else {
- params["InstanceChargeType"] = "PostPaid"
- params["SpotStrategy"] = "NoSpot"
- }
- params["ClientToken"] = utils.GenRequestId(20)
- resp, err := self.ecsRequest("RunInstances", params)
- if err != nil {
- return "", errors.Wrapf(err, "RunInstances")
- }
- ids := []string{}
- err = resp.Unmarshal(&ids, "InstanceIdSets", "InstanceIdSet")
- if err != nil {
- return "", errors.Wrapf(err, "Unmarshal")
- }
- for _, id := range ids {
- err = cloudprovider.Wait(time.Second*3, time.Minute, func() (bool, error) {
- _, err := self.GetInstance(id)
- if err != nil {
- if errors.Cause(err) == cloudprovider.ErrNotFound {
- return false, nil
- }
- return false, err
- }
- return true, nil
- })
- if err != nil {
- return "", errors.Wrapf(cloudprovider.ErrNotFound, "after vm %s created", id)
- }
- return id, nil
- }
- return "", errors.Wrapf(cloudprovider.ErrNotFound, "after created")
- }
- func (self *SRegion) AllocatePublicIpAddress(instanceId string) (string, error) {
- params := map[string]string{
- "InstanceId": instanceId,
- }
- resp, err := self.ecsRequest("AllocatePublicIpAddress", params)
- if err != nil {
- return "", errors.Wrapf(err, "AllocatePublicIpAddress")
- }
- return resp.GetString("IpAddress")
- }
- func (self *SInstance) AllocatePublicIpAddress() (string, error) {
- return self.host.zone.region.AllocatePublicIpAddress(self.InstanceId)
- }
- func (self *SRegion) doStartVM(instanceId string) error {
- return self.instanceOperation(instanceId, "StartInstance", nil)
- }
- func (self *SRegion) doStopVM(instanceId string, isForce, stopCharging bool) error {
- params := make(map[string]string)
- if isForce {
- params["ForceStop"] = "true"
- } else {
- params["ForceStop"] = "false"
- }
- params["StoppedMode"] = "KeepCharging"
- if stopCharging {
- params["StoppedMode"] = "StopCharging"
- }
- return self.instanceOperation(instanceId, "StopInstance", params)
- }
- func (self *SRegion) doDeleteVM(instanceId string) error {
- params := make(map[string]string)
- params["TerminateSubscription"] = "true" // terminate expired prepaid instance
- params["Force"] = "true"
- return self.instanceOperation(instanceId, "DeleteInstance", params)
- }
- func (self *SRegion) StartVM(instanceId string) error {
- status, err := self.GetInstanceStatus(instanceId)
- if err != nil {
- return errors.Wrapf(err, "GetInstanceStatus")
- }
- if status != InstanceStatusStopped {
- return errors.Wrapf(cloudprovider.ErrInvalidStatus, "vm status is %s expect %s", status, InstanceStatusStopped)
- }
- return self.doStartVM(instanceId)
- }
- func (self *SRegion) StopVM(instanceId string, isForce, stopCharging bool) error {
- status, err := self.GetInstanceStatus(instanceId)
- if err != nil {
- return errors.Wrapf(err, "GetInstanceStatus")
- }
- if status == InstanceStatusStopped {
- return nil
- }
- if status != InstanceStatusRunning {
- return errors.Wrapf(cloudprovider.ErrInvalidStatus, "vm status is %s expect %s", status, InstanceStatusRunning)
- }
- return self.doStopVM(instanceId, isForce, stopCharging)
- }
- func (self *SRegion) DeleteVM(instanceId string) error {
- status, err := self.GetInstanceStatus(instanceId)
- if err != nil {
- return errors.Wrapf(err, "GetInstanceStatus")
- }
- if status != InstanceStatusStopped {
- log.Warningf("DeleteVM: vm status is %s expect %s", status, InstanceStatusStopped)
- }
- return self.doDeleteVM(instanceId)
- }
- func (self *SRegion) DeployVM(instanceId string, opts *cloudprovider.SInstanceDeployOptions) error {
- instance, err := self.GetInstance(instanceId)
- if err != nil {
- return errors.Wrapf(err, "GetInstance")
- }
- // 修改密钥时直接返回
- if opts.DeleteKeypair {
- err = self.DetachKeyPair(instanceId, instance.KeyPairName)
- if err != nil {
- return errors.Wrapf(err, "DetachKeyPair")
- }
- }
- var keypairName string
- if len(opts.PublicKey) > 0 {
- var err error
- keypairName, err = self.syncKeypair(opts.PublicKey)
- if err != nil {
- return errors.Wrapf(err, "syncKeypair")
- }
- err = self.AttachKeypair(instanceId, keypairName)
- if err != nil {
- return errors.Wrapf(err, "AttachKeypair")
- }
- }
- // 指定密码的情况下,使用指定的密码
- if len(opts.Password) > 0 {
- params := make(map[string]string)
- params["Password"] = opts.Password
- return self.modifyInstanceAttribute(instanceId, params)
- }
- return nil
- }
- func (self *SInstance) DeleteVM(ctx context.Context) error {
- // 未到期包年包月实例需要先转换到按量计费后, 进行删除
- if self.GetBillingType() == billing_api.BILLING_TYPE_PREPAID && self.GetExpiredAt().After(time.Now()) {
- err := self.host.zone.region.ConvertVmPostpaid([]string{self.InstanceId})
- if err != nil {
- log.Warningf("convert vm %s to postpaid error: %v", self.InstanceId, err)
- }
- }
- for {
- err := self.host.zone.region.DeleteVM(self.InstanceId)
- if err == nil {
- break
- }
- e, ok := errors.Cause(err).(*alierr.ServerError)
- if !ok {
- return errors.Wrapf(err, "DeleteVM")
- }
- switch e.ErrorCode() {
- case "IncorrectInstanceStatus.Initializing":
- time.Sleep(10 * time.Second)
- case "LastTokenProcessing": // 等待转换按量付费完成
- time.Sleep(10 * time.Second)
- default:
- return errors.Wrapf(err, "DeleteVM")
- }
- }
- return cloudprovider.WaitDeleted(self, 10*time.Second, 300*time.Second) // 5minutes
- }
- func (self *SRegion) ConvertVmPostpaid(instanceIds []string) error {
- params := map[string]string{
- "RegionId": self.RegionId,
- "InstanceIds": jsonutils.Marshal(instanceIds).String(),
- "InstanceChargeType": "PostPaid",
- "ClientToken": utils.GenRequestId(20),
- }
- _, err := self.ecsRequest("ModifyInstanceChargeType", params)
- return err
- }
- func (self *SRegion) UpdateVM(instanceId string, input cloudprovider.SInstanceUpdateOptions, osType string) error {
- /*
- api: ModifyInstanceAttribute
- https://help.aliyun.com/document_detail/25503.html?spm=a2c4g.11186623.4.1.DrgpjW
- */
- params := make(map[string]string)
- params["InstanceName"] = input.NAME
- if len(input.HostName) > 0 {
- params["HostName"] = input.HostName
- }
- if len(input.Description) > 0 {
- params["Description"] = input.Description
- }
- return self.modifyInstanceAttribute(instanceId, params)
- }
- func (self *SRegion) modifyInstanceAttribute(instanceId string, params map[string]string) error {
- return self.instanceOperation(instanceId, "ModifyInstanceAttribute", params)
- }
- func (self *SRegion) ReplaceSystemDisk(instanceId string, imageId string, passwd string, keypairName string, sysDiskSizeGB int) (string, error) {
- params := make(map[string]string)
- params["RegionId"] = self.RegionId
- params["InstanceId"] = instanceId
- params["ImageId"] = imageId
- if len(passwd) > 0 {
- params["Password"] = passwd
- } else {
- params["PasswordInherit"] = "True"
- }
- if len(keypairName) > 0 {
- params["KeyPairName"] = keypairName
- }
- if sysDiskSizeGB > 0 {
- params["SystemDisk.Size"] = fmt.Sprintf("%d", sysDiskSizeGB)
- }
- body, err := self.ecsRequest("ReplaceSystemDisk", params)
- if err != nil {
- return "", errors.Wrapf(err, "ReplaceSystemDisk")
- }
- return body.GetString("DiskId")
- }
- func (self *SRegion) ChangePrepaidVMConfig(instanceId string, instanceType string, isDowngrade bool) error {
- // todo: support change disk config?
- params := make(map[string]string)
- params["InstanceType"] = instanceType
- params["ClientToken"] = utils.GenRequestId(20)
- if isDowngrade {
- params["OperatorType"] = "downgrade"
- }
- err := self.instanceOperation(instanceId, "ModifyPrepayInstanceSpec", params)
- if err != nil {
- return errors.Wrapf(err, "ModifyPrepayInstanceSpec %s", instanceType)
- }
- return nil
- }
- func (self *SRegion) ChangeVMConfig(instanceId string, instanceType string) error {
- // todo: support change disk config?
- params := make(map[string]string)
- params["InstanceType"] = instanceType
- params["ClientToken"] = utils.GenRequestId(20)
- err := self.instanceOperation(instanceId, "ModifyInstanceSpec", params)
- if err != nil {
- return errors.Wrapf(err, "ModifyInstanceSpec %s", instanceType)
- }
- return nil
- }
- func (self *SRegion) DetachDisk(instanceId string, diskId string) error {
- params := make(map[string]string)
- params["InstanceId"] = instanceId
- params["DiskId"] = diskId
- log.Infof("Detach instance %s disk %s", instanceId, diskId)
- _, err := self.ecsRequest("DetachDisk", params)
- if err != nil {
- if strings.Contains(err.Error(), "The specified disk has not been attached on the specified instance") {
- return nil
- }
- return errors.Wrap(err, "DetachDisk")
- }
- return nil
- }
- func (self *SRegion) AttachDisk(instanceId string, diskId string) error {
- params := make(map[string]string)
- params["InstanceId"] = instanceId
- params["DiskId"] = diskId
- params["DeleteWithInstance"] = "true"
- _, err := self.ecsRequest("AttachDisk", params)
- if err != nil {
- return errors.Wrapf(err, "AttachDisk %s => %s", diskId, instanceId)
- }
- return nil
- }
- func (self *SInstance) GetIEIP() (cloudprovider.ICloudEIP, error) {
- if len(self.EipAddress.IpAddress) > 0 {
- return self.host.zone.region.GetEip(self.EipAddress.AllocationId)
- }
- 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.InstanceType = EIP_INSTANCE_TYPE_ECS
- eip.Status = EIP_STATUS_INUSE
- eip.AllocationId = self.InstanceId // fixed
- eip.AllocationTime = self.CreationTime
- eip.Bandwidth = self.InternetMaxBandwidthOut
- eip.ResourceGroupId = self.ResourceGroupId
- eip.InternetChargeType = self.InternetChargeType
- return &eip, nil
- }
- return nil, nil
- }
- func (self *SInstance) SetSecurityGroups(secgroupIds []string) error {
- return self.host.zone.region.SetSecurityGroups(secgroupIds, self.InstanceId)
- }
- func (self *SInstance) GetBillingType() string {
- return convertChargeType(self.InstanceChargeType)
- }
- func (self *SInstance) ChangeBillingType(billingType string) error {
- return self.host.zone.region.ModifyInstanceChargeType(self.InstanceId, billingType)
- }
- func (region *SRegion) ModifyInstanceChargeType(vmId string, billingType string) error {
- params := map[string]string{
- "RegionId": region.RegionId,
- "IncludeDataDisks": "true",
- "AutoPay": "true",
- "ClientToken": utils.GenRequestId(20),
- "InstanceIds": jsonutils.Marshal([]string{vmId}).String(),
- }
- switch billingType {
- case billing_api.BILLING_TYPE_POSTPAID:
- params["InstanceChargeType"] = "PostPaid"
- case billing_api.BILLING_TYPE_PREPAID:
- params["InstanceChargeType"] = "PrePaid"
- params["Period"] = "1"
- params["PeriodUnit"] = "Month"
- default:
- return fmt.Errorf("invalid billing_type %s", billingType)
- }
- _, err := region.ecsRequest("ModifyInstanceChargeType", params)
- if err != nil {
- return errors.Wrapf(err, "ModifyInstanceChargeType %v", params)
- }
- if billingType == billing_api.BILLING_TYPE_PREPAID {
- cycle := billing.SBillingCycle{
- AutoRenew: true,
- Count: 1,
- Unit: billing.BillingCycleMonth,
- }
- err = cloudprovider.Wait(time.Second*10, time.Minute*3, func() (bool, error) {
- err = region.SetInstanceAutoRenew(vmId, cycle)
- if err != nil {
- log.Errorf("set auto renew for instance %s error: %v", vmId, err)
- return false, nil
- }
- return true, nil
- })
- if err != nil {
- log.Errorf("set auto renew for %s error: %v", vmId, err)
- }
- }
- return nil
- }
- func (self *SInstance) GetCreatedAt() time.Time {
- return self.CreationTime
- }
- func (self *SInstance) GetExpiredAt() time.Time {
- return convertExpiredAt(self.ExpiredTime)
- }
- func (self *SInstance) UpdateUserData(userData string) error {
- return self.host.zone.region.updateInstance(self.InstanceId, "", "", "", "", userData)
- }
- func (self *SInstance) Renew(bc billing.SBillingCycle) error {
- return self.host.zone.region.RenewInstance(self.InstanceId, bc)
- }
- func (self *SInstance) GetThroughput() int {
- return self.Throughput
- }
- func (self *SInstance) GetInternetMaxBandwidthOut() int {
- return self.InternetMaxBandwidthOut
- }
- func billingCycle2Params(bc *billing.SBillingCycle, params map[string]string) error {
- if bc.GetMonths() > 0 {
- params["PeriodUnit"] = "Month"
- params["Period"] = fmt.Sprintf("%d", bc.GetMonths())
- } else if bc.GetWeeks() > 0 {
- params["PeriodUnit"] = "Week"
- params["Period"] = fmt.Sprintf("%d", bc.GetWeeks())
- } else {
- return fmt.Errorf("invalid renew time period %s", bc.String())
- }
- return nil
- }
- func (region *SRegion) RenewInstance(instanceId string, bc billing.SBillingCycle) error {
- params := make(map[string]string)
- params["InstanceId"] = instanceId
- err := billingCycle2Params(&bc, params)
- if err != nil {
- return errors.Wrapf(err, "billingCycle2Params %v", params)
- }
- params["ClientToken"] = utils.GenRequestId(20)
- _, err = region.ecsRequest("RenewInstance", params)
- if err != nil {
- return errors.Wrapf(err, "RenewInstance %v", params)
- }
- return nil
- }
- func (self *SInstance) GetProjectId() string {
- return self.ResourceGroupId
- }
- func (self *SInstance) GetError() error {
- return nil
- }
- func (region *SRegion) ConvertPublicIpToEip(instanceId string) error {
- params := make(map[string]string)
- params["InstanceId"] = instanceId
- params["RegionId"] = region.RegionId
- _, err := region.ecsRequest("ConvertNatPublicIpToEip", params)
- return err
- }
- func (self *SInstance) ConvertPublicIpToEip() error {
- err := self.host.zone.region.ConvertPublicIpToEip(self.InstanceId)
- if err != nil {
- return errors.Wrapf(err, "ConvertPublicIpToEip")
- }
- return cloudprovider.Wait(time.Second*5, time.Minute*5, func() (bool, error) {
- self.Refresh()
- iEip, err := self.GetIEIP()
- if err != nil {
- return false, errors.Wrapf(err, "GetIEIP")
- }
- if iEip == nil {
- return false, nil
- }
- if iEip.GetMode() == api.EIP_MODE_STANDALONE_EIP {
- return true, self.host.zone.region.VpcMoveResourceGroup("eip", self.ResourceGroupId, iEip.GetId())
- }
- return false, nil
- })
- }
- func (self *SRegion) VpcMoveResourceGroup(resType, groupId, resId string) error {
- params := map[string]string{
- "RegionId": self.RegionId,
- "ResourceType": resType,
- "NewResourceGroupId": groupId,
- "ResourceId": resId,
- }
- _, err := self.vpcRequest("MoveResourceGroup", params)
- return errors.Wrapf(err, "MoveResourceGroup")
- }
- // https://help.aliyun.com/document_detail/52843.html
- func (region *SRegion) SetInstanceAutoRenew(instanceId string, bc billing.SBillingCycle) error {
- params := make(map[string]string)
- params["InstanceId"] = instanceId
- params["RegionId"] = region.RegionId
- if bc.AutoRenew {
- params["RenewalStatus"] = "AutoRenewal"
- switch bc.Unit {
- case billing.BillingCycleYear:
- params["Duration"] = fmt.Sprintf("%d", bc.GetYears())
- params["PeriodUnit"] = "Year"
- case billing.BillingCycleMonth:
- params["Duration"] = fmt.Sprintf("%d", bc.GetMonths())
- params["PeriodUnit"] = "Month"
- case billing.BillingCycleWeek:
- params["Duration"] = fmt.Sprintf("%d", bc.GetWeeks())
- params["PeriodUnit"] = "Week"
- }
- } else {
- params["RenewalStatus"] = "Normal"
- }
- _, err := region.ecsRequest("ModifyInstanceAutoRenewAttribute", params)
- return err
- }
- type SAutoRenewAttr struct {
- Duration int
- AutoRenewEnabled bool
- RenewalStatus string
- PeriodUnit string
- }
- func (region *SRegion) GetInstanceAutoRenewAttribute(instanceId string) (*SAutoRenewAttr, error) {
- params := make(map[string]string)
- params["InstanceId"] = instanceId
- params["RegionId"] = region.RegionId
- resp, err := region.ecsRequest("DescribeInstanceAutoRenewAttribute", params)
- if err != nil {
- return nil, errors.Wrap(err, "DescribeInstanceAutoRenewAttribute")
- }
- attr := []SAutoRenewAttr{}
- err = resp.Unmarshal(&attr, "InstanceRenewAttributes", "InstanceRenewAttribute")
- if err != nil {
- return nil, errors.Wrap(err, "resp.Unmarshal")
- }
- if len(attr) == 1 {
- return &attr[0], nil
- }
- return nil, fmt.Errorf("get %d auto renew info", len(attr))
- }
- func (self *SInstance) IsAutoRenew() bool {
- attr, err := self.host.zone.region.GetInstanceAutoRenewAttribute(self.InstanceId)
- if err != nil {
- log.Errorf("failed to get instance %s auto renew info", self.InstanceId)
- return false
- }
- return attr.AutoRenewEnabled
- }
- func (self *SInstance) SetAutoRenew(bc billing.SBillingCycle) error {
- return self.host.zone.region.SetInstanceAutoRenew(self.InstanceId, bc)
- }
- func (self *SInstance) SetTags(tags map[string]string, replace bool) error {
- return self.host.zone.region.SetResourceTags(ALIYUN_SERVICE_ECS, "instance", self.InstanceId, tags, replace)
- }
- func (self *SRegion) SaveImage(instanceId string, opts *cloudprovider.SaveImageOptions) (*SImage, error) {
- params := map[string]string{
- "InstanceId": instanceId,
- "ImageName": opts.Name,
- "Description": opts.Notes,
- "ClientToken": utils.GenRequestId(20),
- }
- resp, err := self.ecsRequest("CreateImage", params)
- if err != nil {
- return nil, errors.Wrapf(err, "CreateImage")
- }
- ret := struct{ ImageId string }{}
- err = resp.Unmarshal(&ret)
- if err != nil {
- return nil, errors.Wrapf(err, "resp.Unmarshal")
- }
- 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(%s)", opts.Name)
- }
- return image, nil
- }
|