diff --git a/Gopkg.lock b/Gopkg.lock index e8ae57e3c2..7e20fb308d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1676,7 +1676,7 @@ [[projects]] branch = "master" - digest = "1:6277045f9c9a63d84e1aa48c7ad3c1f42ea6523bc54ea92e772d541c78e37404" + digest = "1:37d4e9b3030c077522fc5669b497a25ce53429da77d5e592226658e57cc604bb" name = "yunion.io/x/pkg" packages = [ "gotypes", @@ -1710,7 +1710,7 @@ "utils", ] pruneopts = "UT" - revision = "e2e42205d868c26829b9322b40cdc48f24fed379" + revision = "5fbe7b39e5f599aa49f6f597d57da460d93c26ff" [[projects]] branch = "master" @@ -1826,6 +1826,7 @@ "golang.org/x/net/ipv4", "golang.org/x/sys/unix", "gopkg.in/gin-gonic/gin.v1", + "gopkg.in/yaml.v2", "k8s.io/api/core/v1", "k8s.io/apimachinery/pkg/api/errors", "k8s.io/apimachinery/pkg/apis/meta/v1", diff --git a/pkg/compute/guestdrivers/huawei.go b/pkg/compute/guestdrivers/huawei.go index e516fa9a1a..65d532781c 100644 --- a/pkg/compute/guestdrivers/huawei.go +++ b/pkg/compute/guestdrivers/huawei.go @@ -192,11 +192,11 @@ func (self *SHuaweiGuestDriver) RequestDeployGuestOnHost(ctx context.Context, gu log.Debugf("VMrebuildRoot %s new diskID %s, wait status ready ...", iVM.GetGlobalId(), diskId) - err = cloudprovider.WaitStatus(iVM, models.VM_READY, time.Second*5, time.Second*1800) + err = cloudprovider.WaitStatus(iVM, models.VM_RUNNING, time.Second*5, time.Second*1800) if err != nil { return nil, err } - log.Debugf("VMrebuildRoot %s, and status is ready", iVM.GetGlobalId()) + log.Debugf("VMrebuildRoot %s, and status is %s", iVM.GetGlobalId(), iVM.GetStatus()) maxWaitSecs := 300 waited := 0 diff --git a/pkg/compute/guestdrivers/managedvirtual.go b/pkg/compute/guestdrivers/managedvirtual.go index 6e45485452..d096e21f4e 100644 --- a/pkg/compute/guestdrivers/managedvirtual.go +++ b/pkg/compute/guestdrivers/managedvirtual.go @@ -147,6 +147,12 @@ func (self *SManagedVirtualizedGuestDriver) RequestUndeployGuestOnHost(ctx conte log.Errorf("host.GetIHost fail %s", err) return nil, err } + + // 创建失败时external id为空。此时直接返回即可。不需要再调用公有云api + if len(guest.ExternalId) == 0 { + return nil, nil + } + ivm, err := ihost.GetIVMById(guest.ExternalId) if err != nil { if err == cloudprovider.ErrNotFound { diff --git a/pkg/util/huawei/client/modules/mod_orders.go b/pkg/util/huawei/client/modules/mod_orders.go index e75fa3f4ec..a6c563d2c2 100644 --- a/pkg/util/huawei/client/modules/mod_orders.go +++ b/pkg/util/huawei/client/modules/mod_orders.go @@ -52,11 +52,7 @@ func (self *SOrderManager) SetDomainId(domainId string) error { // 查询客户包周期资源列表 https://support.huaweicloud.com/api-oce/zh-cn_topic_0084961226.html func (self *SOrderManager) List(querys map[string]string) (*responses.ListResult, error) { - if self.orderCtx == nil { - return nil, fmt.Errorf("domainId is emtpy.Use SetDomainId method to set.") - } - - return self.ListInContextWithSpec(self.orderCtx, "resources/detail", querys, "data") + return nil, fmt.Errorf("Not Suppport List Order") } // 查询订单的资源开通详情 https://support.huaweicloud.com/api-oce/api_order_00001.html @@ -75,3 +71,11 @@ func (self *SOrderManager) PerformAction(action string, id string, params jsonut return self._do(request, "") } + +func (self *SOrderManager) GetPeriodResourceList(querys map[string]string) (*responses.ListResult, error) { + if self.orderCtx == nil { + return nil, fmt.Errorf("domainId is emtpy.Use SetDomainId method to set.") + } + + return self.ListInContextWithSpec(self.orderCtx, "resources/detail", querys, "data") +} diff --git a/pkg/util/huawei/consts.go b/pkg/util/huawei/consts.go index ae0cc1ca0e..683497fb9f 100644 --- a/pkg/util/huawei/consts.go +++ b/pkg/util/huawei/consts.go @@ -4,7 +4,10 @@ package huawei const DATETIME_FORMAT = "2006-01-02T15:04:05.999999999" // Task status -const TASK_SUCCESS = "SUCCESS" +const ( + TASK_SUCCESS = "SUCCESS" + TASK_FAIL = "FAIL" +) // Charging Type const ( @@ -20,3 +23,35 @@ const ( RESOURCE_TYPE_IP = "hws.resource.type.ip" // VPC公网IP RESOURCE_TYPE_IMAGE = "hws.resource.type.marketplace" // 市场镜像 ) + +// Not Found Error code +// 网络等资源直接通过http code 404即可判断资源不存在。另外有些资源可能不是返回404这里单独列出来 +const ( + VM_NOT_FOUND = "Ecs.0114" // 云服务器不存在 + ECS_NOT_FOUND = "Ecs.0614" // 弹性云服务器不存在 + IMG_ID_NOT_FOUND = "IMG.0027" // 请求的镜像ID不存在 + IMG_NOT_FOUND = "IMG.0027" // 镜像不存在 + IMG_ERR_NOT_FOUND = "IMG.0057" // 镜像文件不存在或者为空或者不是允许格式的文件 + IMG_BACKUP_NOT_FOUND = "IMG.0020" // 备份不存在 + IMG_VM_BACKUP_NOT_FOUND = "IMG.0127" // 云服务器备份不存在 + IMG_VM_NOT_FOUND = "IMG.0005" // 云主机不存在 + JOB_NOT_FOUND = "Common.0011" // jobId为空 + EVS_NOT_FOUND = "EVS.5404" // 磁盘、快照和备份等资源未找到。 + FIP_NOT_FOUND = "VPC.0504" // 未找到弹性公网IP。 + VPC_NOT_FOUND = "VPC.0012" // 未找到弹性公网VPC。 +) + +var NOT_FOUND_CODES = []string{ + VM_NOT_FOUND, + ECS_NOT_FOUND, + IMG_ID_NOT_FOUND, + IMG_NOT_FOUND, + IMG_ERR_NOT_FOUND, + IMG_BACKUP_NOT_FOUND, + IMG_VM_BACKUP_NOT_FOUND, + IMG_VM_NOT_FOUND, + JOB_NOT_FOUND, + EVS_NOT_FOUND, + FIP_NOT_FOUND, + VPC_NOT_FOUND, +} diff --git a/pkg/util/huawei/disk.go b/pkg/util/huawei/disk.go index f46411c784..58a131f9e1 100644 --- a/pkg/util/huawei/disk.go +++ b/pkg/util/huawei/disk.go @@ -233,6 +233,8 @@ func (self *SDisk) GetTemplateId() string { return self.VolumeImageMetadata.ImageID } +// Bootable 表示硬盘是否为启动盘。 +// 启动盘 != 系统盘(必须是启动盘且挂载在root device上) func (self *SDisk) GetDiskType() string { if self.Bootable == "true" { return models.DISK_TYPE_SYS @@ -268,6 +270,14 @@ func (self *SDisk) GetMountpoint() string { return "" } +func (self *SDisk) GetMountServerId() string { + if len(self.Attachments) > 0 { + return self.Attachments[0].ServerID + } + + return "" +} + func (self *SDisk) GetAccessPath() string { return "" } @@ -323,8 +333,55 @@ func (self *SDisk) Resize(ctx context.Context, newSizeMB int64) error { return self.storage.zone.region.resizeDisk(self.GetId(), sizeGb) } +func (self *SDisk) Detach() error { + err := self.storage.zone.region.DetachDisk(self.GetMountServerId(), self.GetId()) + if err != nil { + log.Debugf("detach server %s disk %s failed: %s", self.GetMountServerId(), self.GetId(), err) + return err + } + + return cloudprovider.WaitStatus(self, models.DISK_READY, 5*time.Second, 60*time.Second) +} + +func (self *SDisk) Attach(device string) error { + err := self.storage.zone.region.AttachDisk(self.GetMountServerId(), self.GetId(), device) + if err != nil { + log.Debugf("attach server %s disk %s failed: %s", self.GetMountServerId(), self.GetId(), err) + return err + } + + return cloudprovider.WaitStatus(self, models.DISK_READY, 5*time.Second, 60*time.Second) +} + +// 在线卸载磁盘 https://support.huaweicloud.com/usermanual-ecs/zh-cn_topic_0036046828.html +// 对于挂载在系统盘盘位(也就是“/dev/sda”或“/dev/vda”挂载点)上的磁盘,当前仅支持离线卸载 func (self *SDisk) Reset(ctx context.Context, snapshotId string) (string, error) { - return self.storage.zone.region.resetDisk(self.GetId(), snapshotId) + mountpoint := self.GetMountpoint() + if mountpoint == "/dev/sda" || mountpoint == "/dev/vda" { + err := self.Detach() + if err != nil { + return "", err + } + } + + diskId, err := self.storage.zone.region.resetDisk(self.GetId(), snapshotId) + if err != nil { + return diskId, err + } + + err = cloudprovider.WaitStatus(self, models.DISK_READY, 5*time.Second, 300*time.Second) + if err != nil { + return "", err + } + + if mountpoint == "/dev/sda" || mountpoint == "/dev/vda" { + err := self.Attach(mountpoint) + if err != nil { + return "", err + } + } + + return diskId, nil } // 华为云不支持重置 diff --git a/pkg/util/huawei/instance.go b/pkg/util/huawei/instance.go index 855f07fe21..811a6f0b94 100644 --- a/pkg/util/huawei/instance.go +++ b/pkg/util/huawei/instance.go @@ -91,12 +91,11 @@ type SInstance struct { Progress string `json:"progress"` HostID string `json:"hostId"` Updated string `json:"updated"` - Created string `json:"created"` + Created time.Time `json:"created"` Metadata VMMetadata `json:"metadata"` Tags []string `json:"tags"` Description string `json:"description"` Locked bool `json:"locked"` - Image Image `json:"image"` ConfigDrive string `json:"config_drive"` TenantID string `json:"tenant_id"` UserID string `json:"user_id"` @@ -115,13 +114,13 @@ type SInstance struct { OSEXTSRVATTRRamdiskID string `json:"OS-EXT-SRV-ATTR:ramdisk_id"` EnterpriseProjectID string `json:"enterprise_project_id"` OSEXTSRVATTRUserData string `json:"OS-EXT-SRV-ATTR:user_data"` - OSSRVUSGLaunchedAt string `json:"OS-SRV-USG:launched_at"` + OSSRVUSGLaunchedAt time.Time `json:"OS-SRV-USG:launched_at"` OSEXTSRVATTRKernelID string `json:"OS-EXT-SRV-ATTR:kernel_id"` OSEXTSRVATTRLaunchIndex int64 `json:"OS-EXT-SRV-ATTR:launch_index"` HostStatus string `json:"host_status"` OSEXTSRVATTRReservationID string `json:"OS-EXT-SRV-ATTR:reservation_id"` OSEXTSRVATTRHostname string `json:"OS-EXT-SRV-ATTR:hostname"` - OSSRVUSGTerminatedAt string `json:"OS-SRV-USG:terminated_at"` + OSSRVUSGTerminatedAt time.Time `json:"OS-SRV-USG:terminated_at"` SysTags []SysTag `json:"sys_tags"` SecurityGroups []SecurityGroup `json:"security_groups"` } @@ -156,6 +155,21 @@ func compareSet(currentSet []string, newSet []string) (add []string, remove []st return add, remove, keep } +// 启动盘 != 系统盘(必须是启动盘且挂载在root device上) +func isBootDisk(server *SInstance, disk *SDisk) bool { + if disk.GetDiskType() != models.DISK_TYPE_SYS { + return false + } + + for _, attachment := range disk.Attachments { + if attachment.ServerID == server.GetId() && attachment.Device == server.OSEXTSRVATTRRootDeviceName { + return true + } + } + + return false +} + func (self *SInstance) GetId() string { return self.ID } @@ -189,6 +203,12 @@ func (self *SInstance) Refresh() error { if err != nil { return err } + + if new.Status == InstanceStatusTerminated { + log.Debugf("Instance already terminated.") + return cloudprovider.ErrNotFound + } + return jsonutils.Update(self, new) } @@ -210,9 +230,9 @@ func (self *SInstance) GetMetadata() *jsonutils.JSONDict { priceKey := fmt.Sprintf("%s::%s::%s", self.host.zone.region.GetId(), self.GetInstanceType(), lowerOs) data.Add(jsonutils.NewString(priceKey), "price_key") data.Add(jsonutils.NewString(self.host.zone.GetGlobalId()), "zone_ext_id") - if len(self.Image.ID) > 0 { - if image, err := self.host.zone.region.GetImage(self.Image.ID); err != nil { - log.Errorf("Failed to find image %s for instance %s zone %s", self.Image.ID, self.GetId(), self.OSEXTAZAvailabilityZone) + if len(self.Metadata.MeteringImageID) > 0 { + if image, err := self.host.zone.region.GetImage(self.Metadata.MeteringImageID); err != nil { + log.Errorf("Failed to find image %s for instance %s zone %s", self.Metadata.MeteringImageID, self.GetId(), self.OSEXTAZAvailabilityZone) } else if meta := image.GetMetadata(); meta != nil { data.Update(meta) } @@ -241,14 +261,23 @@ func (self *SInstance) GetBillingType() string { } } +// charging_mode “0”:按需计费 “1”:按包年包月计费 func (self *SInstance) GetExpiredAt() time.Time { - t, _ := time.Parse(DATETIME_FORMAT, self.OSSRVUSGTerminatedAt) - return t + var expiredTime time.Time + if self.Metadata.ChargingMode == "1" { + res, err := self.host.zone.region.GetOrderResourceDetail(self.GetId()) + if err != nil { + log.Debugf(err.Error()) + } + + expiredTime = res.ExpireTime + } + + return expiredTime } func (self *SInstance) GetCreateTime() time.Time { - t, _ := time.Parse(DATETIME_FORMAT, self.Created) - return t + return self.Created } func (self *SInstance) GetIHost() cloudprovider.ICloudHost { @@ -275,6 +304,12 @@ func (self *SInstance) GetIDisks() ([]cloudprovider.ICloudDisk, error) { } disks[i].storage = storage idisks[i] = &disks[i] + // 将系统盘放到第0个位置 + if isBootDisk(self, &disks[i]) { + _temp := idisks[0] + idisks[0] = &disks[i] + idisks[i] = _temp + } } return idisks, nil } @@ -418,6 +453,11 @@ func (self *SInstance) StopVM(ctx context.Context, isForce bool) error { return nil } + if self.Status == InstanceStatusTerminated { + log.Debugf("Instance already terminated.") + return nil + } + err := self.host.zone.region.StopVM(self.GetId(), isForce) if err != nil { return err @@ -426,6 +466,10 @@ func (self *SInstance) StopVM(ctx context.Context, isForce bool) error { } func (self *SInstance) DeleteVM(ctx context.Context) error { + if self.Status == InstanceStatusTerminated { + return nil + } + for { err := self.host.zone.region.DeleteVM(self.GetId()) if err != nil && self.Status != InstanceStatusTerminated { @@ -457,20 +501,40 @@ func (self *SInstance) UpdateUserData(userData string) error { // todo: 支持注入user_data func (self *SInstance) RebuildRoot(ctx context.Context, imageId string, passwd string, publicKey string, sysSizeGB int) (string, error) { var err error - if self.Image.ID == imageId { - err = self.host.zone.region.RebuildRoot(ctx, self.GetId(), passwd, publicKey) + var jobId string + if self.Metadata.MeteringImageID == imageId { + jobId, err = self.host.zone.region.RebuildRoot(ctx, self.GetId(), passwd, publicKey) if err != nil { return "", err } } else { - err = self.host.zone.region.ChangeRoot(ctx, self.GetId(), imageId, passwd, publicKey) + jobId, err = self.host.zone.region.ChangeRoot(ctx, self.GetId(), imageId, passwd, publicKey) if err != nil { return "", err } } - // todo: wait job finished here - return "", nil + err = self.host.zone.region.waitTaskStatus(self.host.zone.region.ecsClient.Servers.ServiceType(), jobId, TASK_SUCCESS, 15*time.Second, 900*time.Second) + if err != nil { + log.Errorf("RebuildRoot task error %s", err) + return "", err + } + + err = self.Refresh() + if err != nil { + return "", err + } + + idisks, err := self.GetIDisks() + if err != nil { + return "", err + } + + if len(idisks) == 0 { + return "", fmt.Errorf("server %s has no volume attached.", self.GetId()) + } + + return idisks[0].GetId(), nil } func (self *SInstance) DeployVM(ctx context.Context, name string, password string, publicKey string, deleteKeypair bool, description string) error { @@ -670,6 +734,16 @@ func (self *SRegion) CreateInstance(name string, imageId string, instanceType st } else { // 包年包月 err = cloudprovider.WaitCreated(10*time.Second, 180*time.Second, func() bool { + order, e := self.GetOrder(_id) + if e != nil { + log.Debugf(err.Error()) + return false + } + + if order.TotalSize == 0 { + return false + } + ids, err = self.getAllResIdsByType(_id, RESOURCE_TYPE_VM) if err != nil { log.Debugf(err.Error()) @@ -827,10 +901,6 @@ func (self *SRegion) DeleteVM(instanceId string) error { return err } - if remoteStatus == InstanceStatusTerminated { - return nil - } - if remoteStatus != InstanceStatusStopped { log.Errorf("DeleteVM vm status is %s expect %s", remoteStatus, InstanceStatusStopped) return cloudprovider.ErrInvalidStatus @@ -862,7 +932,9 @@ func (self *SRegion) UpdateVM(instanceId, name string) error { return err } -func (self *SRegion) RebuildRoot(ctx context.Context, instanceId, passwd, publicKeyName string) error { +// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876349.html +// 返回job id +func (self *SRegion) RebuildRoot(ctx context.Context, instanceId, passwd, publicKeyName string) (string, error) { params := jsonutils.NewDict() reinstallObj := jsonutils.NewDict() // meta := jsonutils.NewDict() @@ -872,15 +944,21 @@ func (self *SRegion) RebuildRoot(ctx context.Context, instanceId, passwd, public } else if len(publicKeyName) > 0 { reinstallObj.Add(jsonutils.NewString(publicKeyName), "keyname") } else { - return fmt.Errorf("both password and publicKey are empty.") + return "", fmt.Errorf("both password and publicKey are empty.") } params.Add(reinstallObj, "os-reinstall") - _, err := self.ecsClient.Servers.PerformAction2("reinstallos", instanceId, params, "") - return err + ret, err := self.ecsClient.Servers.PerformAction2("reinstallos", instanceId, params, "") + if err != nil { + return "", err + } + + return ret.GetString("job_id") } -func (self *SRegion) ChangeRoot(ctx context.Context, instanceId, imageId, passwd, publicKeyName string) error { +// https://support.huaweicloud.com/api-ecs/zh-cn_topic_0067876971.html +// 返回job id +func (self *SRegion) ChangeRoot(ctx context.Context, instanceId, imageId, passwd, publicKeyName string) (string, error) { params := jsonutils.NewDict() changeOsObj := jsonutils.NewDict() // meta := jsonutils.NewDict() @@ -890,14 +968,18 @@ func (self *SRegion) ChangeRoot(ctx context.Context, instanceId, imageId, passwd } else if len(publicKeyName) > 0 { changeOsObj.Add(jsonutils.NewString(publicKeyName), "keyname") } else { - return fmt.Errorf("both password and publicKey are empty.") + return "", fmt.Errorf("both password and publicKey are empty.") } changeOsObj.Add(jsonutils.NewString(imageId), "imageid") params.Add(changeOsObj, "os-change") - _, err := self.ecsClient.Servers.PerformAction2("changeos", instanceId, params, "") - return err + ret, err := self.ecsClient.Servers.PerformAction2("changeos", instanceId, params, "") + if err != nil { + return "", err + } + + return ret.GetString("job_id") } // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0020212692.html diff --git a/pkg/util/huawei/order.go b/pkg/util/huawei/order.go index a6c906b194..c408cfb865 100644 --- a/pkg/util/huawei/order.go +++ b/pkg/util/huawei/order.go @@ -1,6 +1,19 @@ package huawei -import "fmt" +import ( + "fmt" + "strings" + "time" + + "yunion.io/x/onecloud/pkg/cloudprovider" +) + +type SOrder struct { + ErrorCode *string `json:"error_code"` // 只有失败时才返回此参数 + ErrorMsg *string `json:"error_msg"` //只有失败时才返回此参数 + TotalSize int `json:"totalSize"` // 只有成功时才返回此参数 + Resources []SResource `json:"resources"` +} type SResource struct { ResourceID string `json:"resourceId"` @@ -11,37 +24,123 @@ type SResource struct { Status int64 `json:"status"` } -/* -获取订单信息 https://support.huaweicloud.com/api-oce/api_order_00001.html -*/ -func (self *SRegion) GetAllResByOrderId(orderId string) ([]SResource, error) { +type SResourceDetail struct { + ID string `json:"id"` + Status int64 `json:"status"` + ResourceID string `json:"resource_id"` + ResourceName string `json:"resource_name"` + RegionCode string `json:"region_code"` + CloudServiceTypeCode string `json:"cloud_service_type_code"` + ResourceTypeCode string `json:"resource_type_code"` + ResourceSpecCode string `json:"resource_spec_code"` + ProjectCode string `json:"project_code"` + ProductID string `json:"product_id"` + MainResourceID string `json:"main_resource_id"` + IsMainResource int64 `json:"is_main_resource"` + ValidTime time.Time `json:"valid_time"` + ExpireTime time.Time `json:"expire_time"` + NextOperationPolicy string `json:"next_operation_policy"` +} + +func (self *SRegion) getDomianId() (string, error) { domains, err := self.client.getEnabledDomains() if err != nil { - return nil, err + return "", err } if domains == nil || len(domains) == 0 { - return nil, fmt.Errorf("GetAllResByOrderId domain is empty") + return "", fmt.Errorf("GetAllResByOrderId domain is empty") } else if len(domains) > 1 { // not supported?? - return nil, fmt.Errorf("GetAllResByOrderId mutliple domain(%d) found", len(domains)) + return "", fmt.Errorf("GetAllResByOrderId mutliple domain(%d) found", len(domains)) } - err = self.ecsClient.Orders.SetDomainId(domains[0].ID) + return domains[0].ID, nil +} + +/* +获取订单信息 https://support.huaweicloud.com/api-oce/api_order_00001.html +*/ +func (self *SRegion) GetOrder(orderId string) (SOrder, error) { + var order SOrder + domain, err := self.getDomianId() + if err != nil { + return order, err + } + + err = self.ecsClient.Orders.SetDomainId(domain) + if err != nil { + return order, err + } + + err = DoGet(self.ecsClient.Orders.Get, orderId, nil, &order) + return order, err +} + +/* +获取订单资源详情列表 https://support.huaweicloud.com/api-oce/zh-cn_topic_0084961226.html +*/ +func (self *SRegion) GetOrderResources(orderId string, resource_ids []string, only_main_resource bool) ([]SResourceDetail, error) { + domain, err := self.getDomianId() if err != nil { return nil, err } - resp, err := self.ecsClient.Orders.Get(orderId, nil) + err = self.ecsClient.Orders.SetDomainId(domain) if err != nil { return nil, err } - resources := make([]SResource, 0) - err = resp.Unmarshal(&resources, "resources") + resources := make([]SResourceDetail, 0) + queries := map[string]string{"customer_id": domain} + if len(orderId) > 0 { + queries["order_id"] = orderId + } + + if len(resource_ids) > 0 { + queries["resource_ids"] = strings.Join(resource_ids, ",") + } + + if only_main_resource { + queries["only_main_resource"] = "1" + } + + err = doListAll(self.ecsClient.Orders.GetPeriodResourceList, queries, &resources) return resources, err } +/* +获取资源详情 https://support.huaweicloud.com/api-oce/zh-cn_topic_0084961226.html +*/ +func (self *SRegion) GetOrderResourceDetail(resourceId string) (SResourceDetail, error) { + var res SResourceDetail + if len(resourceId) == 0 { + return res, fmt.Errorf("GetOrderResourceDetail resource id should not be empty") + } + + resources, err := self.GetOrderResources("", []string{resourceId}, false) + if err != nil { + return res, err + } + + switch len(resources) { + case 0: + return res, cloudprovider.ErrNotFound + case 1: + return resources[0], nil + default: + return res, fmt.Errorf("%d resources with id %s found, Expect 1", len(resources), resourceId) + } +} + +func (self *SRegion) GetAllResByOrderId(orderId string) ([]SResource, error) { + order, err := self.GetOrder(orderId) + if err != nil { + return nil, err + } + return order.Resources, nil +} + func (self *SRegion) getAllResByType(orderId string, resourceType string) ([]SResource, error) { res, err := self.GetAllResByOrderId(orderId) if err != nil { diff --git a/pkg/util/huawei/region.go b/pkg/util/huawei/region.go index 2362d869f2..4d686cd1f8 100644 --- a/pkg/util/huawei/region.go +++ b/pkg/util/huawei/region.go @@ -338,6 +338,8 @@ func (self *SRegion) SyncSecurityGroup(secgroupId string, vpcId string, name str secgroupId = extID } + // 华为云默认deny。不需要显式指定 + rules = SecurityRuleSetToAllowSet(rules) return secgroupId, self.syncSecgroupRules(secgroupId, rules) } @@ -608,6 +610,7 @@ func (self *SRegion) addSecurityGroupRules(secGrpId string, rule *secrules.Secur return nil } +// todo: icmp协议目前存在差异,华为云能指定icmp code,onecloud不支持 func (self *SRegion) addSecurityGroupRule(secGrpId, direction, portStart, portEnd, protocol, ipNet string) error { params := jsonutils.NewDict() secgroupObj := jsonutils.NewDict() @@ -615,10 +618,11 @@ func (self *SRegion) addSecurityGroupRule(secGrpId, direction, portStart, portEn secgroupObj.Add(jsonutils.NewString(direction), "direction") secgroupObj.Add(jsonutils.NewString(ipNet), "remote_ip_prefix") secgroupObj.Add(jsonutils.NewString("IPV4"), "ethertype") - if len(portStart) > 0 && portStart != "0" { + // 端口为空或者1-65535 + if len(portStart) > 0 && portStart != "0" && portStart != "-1" { secgroupObj.Add(jsonutils.NewString(portStart), "port_range_min") } - if len(portEnd) > 0 && portEnd != "0" { + if len(portEnd) > 0 && portEnd != "0" && portEnd != "-1" { secgroupObj.Add(jsonutils.NewString(portEnd), "port_range_max") } if len(protocol) > 0 { diff --git a/pkg/util/huawei/securitygroup.go b/pkg/util/huawei/securitygroup.go index 49845a5148..96f852a263 100644 --- a/pkg/util/huawei/securitygroup.go +++ b/pkg/util/huawei/securitygroup.go @@ -13,6 +13,8 @@ https://support.huaweicloud.com/usermanual-vpc/zh-cn_topic_0073379079.html import ( "net" + "sort" + "yunion.io/x/jsonutils" "yunion.io/x/pkg/util/secrules" ) @@ -68,6 +70,33 @@ func compatibleSecurityGroupRule(r SecurityGroupRule) bool { return true } +// 将安全组规则全部转换为等价的allow规则 +func SecurityRuleSetToAllowSet(srs secrules.SecurityRuleSet) secrules.SecurityRuleSet { + inRuleSet := secrules.SecurityRuleSet{} + outRuleSet := secrules.SecurityRuleSet{} + + for _, rule := range srs { + if rule.Direction == secrules.SecurityRuleIngress { + inRuleSet = append(inRuleSet, rule) + } + + if rule.Direction == secrules.SecurityRuleEgress { + outRuleSet = append(outRuleSet, rule) + } + } + + sort.Sort(inRuleSet) + sort.Sort(outRuleSet) + + inRuleSet = inRuleSet.AllowList() + outRuleSet = outRuleSet.AllowList() + + ret := secrules.SecurityRuleSet{} + ret = append(ret, inRuleSet...) + ret = append(ret, outRuleSet...) + return ret +} + func (self *SSecurityGroup) GetId() string { return self.ID } @@ -192,7 +221,7 @@ func (self *SSecurityGroup) GetSecurityRule(ruleId string, withRuleId bool) (sec } else { desc = remoteRule.Description } - // todo: icmp 可能不兼容 + // todo: icmp 可能不兼容。华为云能指定icmp code,但是onecloud端不支持 rule := secrules.SecurityRule{ Priority: 1, Action: secrules.SecurityRuleAllow, diff --git a/pkg/util/huawei/task.go b/pkg/util/huawei/task.go index 0d3128c7b9..d62904094f 100644 --- a/pkg/util/huawei/task.go +++ b/pkg/util/huawei/task.go @@ -1,7 +1,10 @@ package huawei import ( + "fmt" "time" + + "yunion.io/x/log" ) func (self *SRegion) waitTaskStatus(serviceType string, taskId string, targetStatus string, interval time.Duration, timeout time.Duration) error { @@ -13,6 +16,8 @@ func (self *SRegion) waitTaskStatus(serviceType string, taskId string, targetSta } if status == targetStatus { break + } else if status == TASK_FAIL { + return fmt.Errorf("task %s failed", taskId) } else { time.Sleep(interval) } @@ -27,7 +32,12 @@ func (self *SRegion) GetTaskStatus(serviceType string, taskId string) (string, e return "", err } - return task.GetString("status") + status, err := task.GetString("status") + if status == TASK_FAIL { + log.Debugf("task %s failed: %s", taskId, task.String()) + } + + return status, err } // https://support.huaweicloud.com/api-ecs/zh-cn_topic_0022225398.html diff --git a/pkg/util/huawei/utils.go b/pkg/util/huawei/utils.go index e12cbca2b2..79b839befb 100644 --- a/pkg/util/huawei/utils.go +++ b/pkg/util/huawei/utils.go @@ -11,6 +11,7 @@ import ( "yunion.io/x/onecloud/pkg/util/httputils" "yunion.io/x/onecloud/pkg/util/huawei/client/manager" "yunion.io/x/onecloud/pkg/util/huawei/client/responses" + "yunion.io/x/pkg/utils" ) // 常用的方法 @@ -28,7 +29,7 @@ func unmarshalResult(resp jsonutils.JSONObject, respErr error, result interface{ if respErr != nil { switch e := respErr.(type) { case *httputils.JSONClientError: - if e.Code == 404 { + if e.Code == 404 || utils.IsInStringArray(e.Class, NOT_FOUND_CODES) { return cloudprovider.ErrNotFound } return e diff --git a/vendor/yunion.io/x/pkg/util/regutils/regutils.go b/vendor/yunion.io/x/pkg/util/regutils/regutils.go index b196f2772f..6fd20266fc 100644 --- a/vendor/yunion.io/x/pkg/util/regutils/regutils.go +++ b/vendor/yunion.io/x/pkg/util/regutils/regutils.go @@ -27,6 +27,7 @@ var FULLISO_TIME_REG *regexp.Regexp var COMPACT_TIME_REG *regexp.Regexp var MYSQL_TIME_REG *regexp.Regexp var NORMAL_TIME_REG *regexp.Regexp +var FULLNORMAL_TIME_REG *regexp.Regexp var RFC2882_TIME_REG *regexp.Regexp var EMAIL_REG *regexp.Regexp var CHINA_MOBILE_REG *regexp.Regexp @@ -56,6 +57,7 @@ func init() { COMPACT_TIME_REG = regexp.MustCompile(`^\d{14}$`) MYSQL_TIME_REG = regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`) NORMAL_TIME_REG = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$`) + FULLNORMAL_TIME_REG = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}$`) RFC2882_TIME_REG = regexp.MustCompile(`[A-Z][a-z]{2}, [0-9]{1,2} [A-Z][a-z]{2} [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} [A-Z]{3}`) EMAIL_REG = regexp.MustCompile(`^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$`) CHINA_MOBILE_REG = regexp.MustCompile(`^1[0-9-]{10}$`) @@ -171,6 +173,10 @@ func MatchNormalTime(str string) bool { return NORMAL_TIME_REG.MatchString(str) } +func MatchFullNormalTime(str string) bool { + return FULLNORMAL_TIME_REG.MatchString(str) +} + func MatchRFC2882Time(str string) bool { return RFC2882_TIME_REG.MatchString(str) } diff --git a/vendor/yunion.io/x/pkg/util/timeutils/timeutils.go b/vendor/yunion.io/x/pkg/util/timeutils/timeutils.go index ed6522dd37..d92e34844f 100644 --- a/vendor/yunion.io/x/pkg/util/timeutils/timeutils.go +++ b/vendor/yunion.io/x/pkg/util/timeutils/timeutils.go @@ -26,6 +26,7 @@ const ( FullIsoTimeFormat = "2006-01-02T15:04:05.000000Z" MysqlTimeFormat = "2006-01-02 15:04:05" NormalTimeFormat = "2006-01-02T15:04:05" + FullNormalTimeFormat = "2006-01-02T15:04:05.000000" CompactTimeFormat = "20060102150405" DateFormat = "2006-01-02" ShortDateFormat = "20060102" @@ -84,6 +85,10 @@ func ParseNormalTime(str string) (time.Time, error) { return time.Parse(NormalTimeFormat, str) } +func ParseFullNormalTime(str string) (time.Time, error) { + return time.Parse(FullNormalTimeFormat, str) +} + func ParseCompactTime(str string) (time.Time, error) { return time.Parse(CompactTimeFormat, str) } @@ -111,6 +116,8 @@ func ParseTimeStr(str string) (time.Time, error) { return ParseMysqlTime(str) } else if regutils.MatchNormalTime(str) { return ParseNormalTime(str) + } else if regutils.MatchFullNormalTime(str) { + return ParseFullNormalTime(str) } else if regutils.MatchRFC2882Time(str) { return ParseRFC2882Time(str) } else if regutils.MatchCompactTime(str) {