diff --git a/CHANGELOG.md b/CHANGELOG.md index e1da910..8044d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Version 1.16.4 +## Version 1.16.5 ### Deprecated @@ -18,98 +18,62 @@ | `rg/AccessRevoke` | :heavy_check_mark: | :heavy_check_mark: | | `sep/ConfigFieldEdit` | :x: | :heavy_check_mark: | + ### Добавлено -#### apiaccess +#### audit | Идентификатор задачи | Описание | | --- | --- | -| BGOS-1055 | Структура `SDNEndpoints` и опциональное поле `SDN` в структуру `APIsEndpoints` в cloudbroker/apiaccess | -| BGOS-1055 | Опциональные поля `Audit`, `DPDKNet`, `SecurityGroup`, `SEP`, `StoragePolicy`, `Trunk`, `VFPool`, `VMFolder`, `Zone` в структуру `CloudAPIEndpoints` в cloudbroker/apiaccess | -| BGOS-1055 | Опциональные поля `Backup`, `DPDKNet`, `SecurityGroup`, `StoragePolicy`, `Trunk`, `VFPool`, `VMFolder`, `Zone` в структуру `CloudBrokerEndpoints` в cloudbroker/apiaccess | -| BGOS-1055 | Опциональные поля `Cache`, `InfoWellknown` в структуру `SystemEndpoints` в cloudbroker/apiaccess | +| BGOS-1072 | Вычисляемые поля `ResgroupID`, `AccountID`, `ComputeID`, `VINSID`, `ServiceID`, `K8SID`, `FLIPGroupID`, `LBID`, `SEPID`, `NodeIDs`, `DiskIDs` и `APITask` в структуру ответа `ItemAudit` в cloudapi/audit | +| BGOS-1072 | Вычисляемые поля `ResgroupID`, `AccountID`, `ComputeID`, `VINSID`, `ServiceID`, `K8SID`, `FLIPGroupID`, `LBID`, `SEPID`, `NodeIDs`, `DiskIDs` и `APITask` в структуру ответа `ItemAudit` в cloudbroker/audit | #### compute | Идентификатор задачи | Описание | | --- | --- | -| BGOS-1043 | Опциональное поле `AsyncMode` в структуру запроса `SnapshotEvictDiskRequest` в cloudbroker/compute | -| BGOS-1044 | Методы `AsyncAffinityRuleAdd` и `AsyncAntiAffinityRuleAdd` в cloudapi/compute и cloudbroker/compute | -| BGOS-1054 | Опциональные поля `FolderID` и `IncludeDescendants` в структуру запроса `ListRequest` в cloudapi/compute | -| BGOS-1054 | Опциональные поля `FolderID` и `IncludeDescendants` в структуру запроса `ListRequest` в cloudbroker/compute | -| BGOS-1046 | Опциональное поле `Migrate` в структуры запросов `AffinityRuleAddRequest` и `AntiAffinityRuleAddRequest` в cloudapi/compute | -| BGOS-1046 | Опциональное поле `Migrate` в структуры запросов `AffinityRuleAddRequest` и `AntiAffinityRuleAddRequest` в cloudbroker/compute | - -#### session -| Идентификатор задачи | Описание | -| --- | --- | -| BGOS-1048 | Опциональное поле `IncludeArchive` в структуру запроса `ListRequest` в cloudbroker/session | - -#### stpolicy -| Идентификатор задачи | Описание | -| --- | --- | -| BGOS-1047 | Вычисляемое поле `ReplicationProfileID` в структуры ответа `InfoStoragePolicy` и `ItemStoragePolicy` в cloudapi/stpolicy и в cloudbroker/stpolicy | -| BGOS-1047 | Вычисляемое поле `ReplicationProfileID` в структуру ответа `InfoStoragePolicyWithID` в cloudbroker/stpolicy | -| BGOS-1047 | Методы `GetReplicationProfile`, `ListReplicationProfile`, структуры запроса `GetReplicationProfileRequest`, `ListReplicationProfileRequest` и структуры ответа `InfoReplicationProfile`, `ListReplicationProfiles`, `ItemReplicationProfile`, `ListSchedules`, `Schedule`, `Calendar` в cloudapi/stpolicy и в cloudbroker/stpolicy | -| BGOS-1047 | Метод `CreateReplicationProfile`, структуры запроса `CreateReplicationProfileRequest`, `SchedulesRequest`, `CalendarRequest` в cloudbroker/stpolicy | -| BGOS-1047 | Метод `UpdateReplicationProfile`, структуры запроса `UpdateReplicationProfileRequest`, `ConfigRequest` в cloudbroker/stpolicy | -| BGOS-1047 | Метод `DeleteReplicationProfile` и структура запроса `DeleteReplicationProfileRequest` в cloudbroker/stpolicy | -| BGOS-1047 | Методы `AddAccessReplicationProfile`, `RevokeAccessReplicationProfile` и структуры запроса `AddAccessReplicationProfileRequest`, `RevokeAccessReplicationProfileRequest` в cloudbroker/stpolicy | - -#### vmfolder -| Идентификатор задачи | Описание | -| --- | --- | -| BGOS-1049 | Группа апи методов vmfolder в cloudapi и cloudbroker | - -### Изменено - -#### backup -| Идентификатор задачи | Описание | -| --- | --- | -| BGOS-1045 | JSON-тэг опционального поля `AsyncMode` с `asyncMode` на `async_mode` в структурах запроса `CreateDiskBackupRequest`, `CreateDisksBackupRequest`, `DeleteDiskBackupRequest`, `RestoreDiskFromBackupRequest` и `RestoreDisksFromBackupRequest` в cloudbroker/backup | - -#### compute -| Идентификатор задачи | Описание | -| --- | --- | -| BGOS-1043 | JSON-тэг опционального поля `AsyncMode` с `asyncMode` на `async_mode` в структурах запроса `AbortSharedSnapshotMergeRequest`, `CDEjectRequest`, `CDInsertRequest`, `ChangeIPRequest`, `ChangeLinkStateRequest`, `ChangeMTURequest`, `ChangeQoSPoliciesRequest`, `ChangeSecGroupsRequest`, `CloneRequest`, `CreateTemplateRequest`, `CreateTemplateFromBlankRequest`, `DeleteRequest`, `DisableRequest`, `DiskAddRequest`, `DiskAttachRequest`, `DiskDelRequest`, `DiskDetachRequest`, `DiskQOSRequest`, `DiskResizeRequest`, `DiskSwitchToReplicationRequest`, `GuestAgentDisableRequest`, `GuestAgentEnableRequest`, `GuestAgentExecuteRequest`, `GuestAgentFeatureUpdateRequest`, `MigrateToZoneRequest`, `MoveToRGRequest`, `NetAttachRequest`, `NetDetachRequest`, `PauseRequest`, `PFWAddRequest`, `PFWDelRequest`, `PinToNodeRequest`, `PowerCycleRequest`, `RebootRequest`, `RedeployRequest`, `ResetRequest`, `ResizeRequest`, `RestoreRequest`, `ResumeRequest`, `SnapshotCreateRequest`, `SnapshotDeleteRequest`, `SnapshotRollbackRequest`, `StartRequest` и `StopRequest` в cloudapi/compute | -| BGOS-1043 | JSON-тэг опционального поля `AsyncMode` с `asyncMode` на `async_mode` в структурах запроса `AbortSharedSnapshotMergeRequest`, `CDEjectRequest`, `CDInsertRequest`, `ChangeIPRequest`, `ChangeLinkStateRequest`, `ChangeMTURequest`, `ChangeQoSPoliciesRequest`, `ChangeSecGroupsRequest`, `CloneRequest`, `CreateTemplateRequest`, `CreateTemplateFromBlankRequest`, `DeleteRequest`, `DisableRequest`, `DiskAddRequest`, `DiskAttachRequest`, `DiskDelRequest`, `DiskDetachRequest`, `DiskQOSRequest`, `DiskResizeRequest`, `DiskSwitchToReplicationRequest`, `GuestAgentDisableRequest`, `GuestAgentEnableRequest`, `GuestAgentExecuteRequest`, `GuestAgentFeatureUpdateRequest`, `MassDeleteRequest`, `MassRebootRequest`, `MassStartRequest`, `MassStopRequest`, `MigrateAbortRequest`, `MigrateStorageAbortRequest`, `MigrateStorageCleanUpRequest`, `MigrateToZoneRequest`, `MoveToRGRequest`, `NetAttachRequest`, `NetDetachRequest`, `NetQOSRequest`, `PauseRequest`, `PFWAddRequest`, `PFWDelRequest`, `PinToNodeRequest`, `PowerCycleRequest`, `RaiseDownRequest`, `RebootRequest`, `RedeployRequest`, `ResetRequest`, `ResizeRequest`, `RestoreRequest`, `ResumeRequest`, `SnapshotCreateRequest`, `SnapshotDeleteRequest`, `SnapshotRollbackRequest`, `StartRequest` и `StopRequest` в cloudbroker/compute | -| BGOS-1043 | Опциональное поле `SyncMode` на `AsyncMode` в структуре запроса `MigrateRequest` в cloudbroker/compute | -| BGOS-1043 | Опциональное поле `Sync` на `Async` в структуре запроса `MigrateStorageRequest` в cloudbroker/compute | +| BGOS-1069 | Вычисляемое поле `FolderID` в структуры ответа `RecordCompute` и `ItemCompute` в cloudapi/compute | +| BGOS-1069 | Вычисляемое поле `FolderID` в структуры ответа `RecordCompute` и `InfoCompute` в cloudapi/compute | +| BGOS-1068 | Структуры ответа `MigrationReport`, `MigrationReportItem`, `AffinityRuleAddResult` и `AntiAffinityRuleAddResult` в cloudapi/compute | +| BGOS-1068 | Структуры ответа `MigrationReport`, `MigrationReportItem`, `AffinityRuleAddResult` и `AntiAffinityRuleAddResult` в cloudbroker/compute | +| BGOS-1063 | Структуры запроса `MigrateV2Request`, `MigrateV2AbortRequest`, `MigrateV2CleanupRequest`, `MigrateV2GetRequest`, `MigrateV2ListRequest` и `MigrateV2ResolveRequest` и методы `MigrateV2`, `MigrateV2Abort`, `MigrateV2Cleanup`, `MigrateV2Get`, `MigrateV2List` и `MigrateV2ResolveRequest` в cloudbroker/compute | +| BGOS-1080 | Опциональное поле `AccountID` из структуры запроса `CreateTemplateFromBlankRequest` в cloudapi/compute | #### disks | Идентификатор задачи | Описание | | --- | --- | -| BGOS-1045 | JSON-тэг опционального поля `AsyncMode` с `asyncMode` на `async_mode` в структуре запроса `FromPlatformDiskRequest` в cloudapi/disks и в cloudbroker/disks | +| BGOS-1080 | Опциональное поле `AccountID` из структуры запроса `FromPlatformDiskRequest` в cloudapi/disks | -#### image +#### extnet | Идентификатор задачи | Описание | | --- | --- | -| BGOS-1045 | JSON-тэг опционального поля `AsyncMode` с `asyncMode` на `async_mode` в структуре запроса `CreateRequest` в cloudapi/image и в cloudbroker/image | -| BGOS-1045 | JSON-тэг опционального поля `AsyncMode` с `asyncMode` на `async_mode` в структуре запроса `CreateCDROMImageRequest` в cloudbroker/image | +| BGOS-1073 | Вычисляемое поле `AutoAssignable` в структуру ответа `ItemReservation` в cloudapi/extnet | +| BGOS-1073 | Вычисляемые поля `AccountID` и `AutoAssignable` в структуру ответа `Excluded` в cloudapi/extnet | +| BGOS-1073 | Вычисляемое поле `AutoAssignable` в структуру ответа `ItemReservation` в cloudbroker/extnet | + +#### grid +| Идентификатор задачи | Описание | +| --- | --- | +| BGOS-1074 | Вычисляемое поле `VMFoldersEnabled` в структуры ответа `RecordGrid` и `ItemGridList` в cloudbroker/grid | #### kvmx86 | Идентификатор задачи | Описание | | --- | --- | -| BGOS-1045 | JSON-тэг опционального поля `AsyncMode` с `asyncMode` на `async_mode` в структуре запроса `MassCreateRequest` в cloudbroker/kvmx86 | +| BGOS-1067 | Опциональное поле `PCIdevices` в структуры запросов `CreateRequest` и `CreateBlankRequest` в cloudapi/kvmx86 | +| BGOS-1067 | Опциональное поле `PCIdevices` в структуры запросов `CreateRequest` и `CreateBlankRequest` в cloudbroker/kvmx86 | +| BGOS-1081 | Опциональное поле `VGPUs` в структуры запросов `CreateRequest` и `CreateBlankRequest` в cloudapi/kvmx86 | +| BGOS-1081 | Опциональное поле `VGPUs` в структуры запросов `CreateRequest` и `CreateBlankRequest` в cloudbroker/kvmx86 | -#### node -| Идентификатор задачи | Описание | -| --- | --- | -| BGOS-1045 | JSON-тэг опционального поля `AsyncMode` с `asyncMode` на `async_mode` в структуре запроса `EnableRequest` в cloudbroker/node | - -### Исправлено +### Изменено #### apiaccess | Идентификатор задачи | Описание | | --- | --- | -| BGOS-1055 | Ошибка при вызове метода `Update` в cloudbroker/apiaccess | -| BGOS-1055 | Исправлен тип ответа метода `DescUpdate` в cloudbroker/apiaccess | +| BGOS-1082 | Тип поля `APIs` с опционального на обязательный в структуре запроса `UpdateRequest` в cloudbroker/apiaccess | - -#### Удалено - -#### apiaccess +#### compute | Идентификатор задачи | Описание | | --- | --- | -| BGOS-1055 | Структура `LibCloudEndpoints` и опциональное поле `LibCloud` из структуры `APIsEndpoints` в cloudbroker/apiaccess | -| BGOS-1055 | Опциональные поля `CloudSpace`, `ComputeCI`, `DPDK`, `Machine`, `Openshift`, `OpenshiftCI`, `PortForwarding`, `Sizes` из структуры `CloudAPIEndpoints` в cloudbroker/apiaccess | -| BGOS-1055 | Опциональные поля `CloudSpace`, `ComputeCI`, `Desnode`, `Diagnostics`, `DPDK`, `IaaS`, `Machine`, `Openshift`, `OpenshiftCI`, `Ovsnode`, `QOS` из структуры `CloudBrokerEndpoints` в cloudbroker/apiaccess | -| BGOS-1055 | Опциональные поля `AgentController`, `Alerts`, `ErrorConditionHandler`, `Job`, `Log`, `Logo` из структуры `SystemEndpoints` в cloudbroker/apiaccess | +| BGOS-1068 | Тип ответа метода `AffinityRuleAdd` с `bool` на структуру `AffinityRuleAddResult` в cloudapi/compute | +| BGOS-1068 | Тип ответа метода `AffinityRuleAdd` с `bool` на структуру `AffinityRuleAddResult` в cloudbroker/compute | +| BGOS-1068 | Тип ответа метода `AntiAffinityRuleAdd` с `bool` на структуру `AntiAffinityRuleAddResult` в cloudapi/compute | +| BGOS-1068 | Тип ответа метода `AntiAffinityRuleAdd` с `bool` на структуру `AntiAffinityRuleAddResult` в cloudbroker/compute | + diff --git a/internal/validators/custom.go b/internal/validators/custom.go index 8df3f3c..d4dc02d 100644 --- a/internal/validators/custom.go +++ b/internal/validators/custom.go @@ -500,3 +500,8 @@ func memoryBalloonModeValidator(fe validator.FieldLevel) bool { return IsInSlice(fieldValue, memoryBalloonModeValues) } + +func migrateV2ModeValidator(fe validator.FieldLevel) bool { + fieldValue := fe.Field().String() + return IsInSlice(fieldValue, migrateV2ModeValues) +} diff --git a/internal/validators/validator.go b/internal/validators/validator.go index 9ee1cd2..f4d31ff 100644 --- a/internal/validators/validator.go +++ b/internal/validators/validator.go @@ -336,5 +336,7 @@ func registerAllValidators(validate *validator.Validate) error { return err } + err = validate.RegisterValidation("migrateV2Mode", migrateV2ModeValidator) + return nil } diff --git a/internal/validators/values.go b/internal/validators/values.go index 73ccbfe..d2f202c 100644 --- a/internal/validators/values.go +++ b/internal/validators/values.go @@ -84,6 +84,8 @@ var ( watchdogActionValues = []string{"reset", "shutdown", "poweroff", "pause", "none"} memoryBalloonModeValues = []string{"auto", "legacy", "none"} + + migrateV2ModeValues = []string{"compute", "disks", "compute_and_disks"} ) const ( diff --git a/pkg/cloudapi/audit/models.go b/pkg/cloudapi/audit/models.go index 7bf866f..ef99fdb 100644 --- a/pkg/cloudapi/audit/models.go +++ b/pkg/cloudapi/audit/models.go @@ -88,6 +88,42 @@ type ItemAudit struct { // TTL TTL string `json:"_ttl"` + + // Resource group ID + ResgroupID uint64 `json:"resgroup_id"` + + // Account ID + AccountID uint64 `json:"account_id"` + + // Compute ID + ComputeID uint64 `json:"compute_id"` + + // VINS ID + VINSID uint64 `json:"vins_id"` + + // Service ID + ServiceID uint64 `json:"service_id"` + + // K8S ID + K8SID uint64 `json:"k8s_id"` + + // FLIPGroup ID + FLIPGroupID uint64 `json:"flipgroup_id"` + + // Load balancer ID + LBID uint64 `json:"lb_id"` + + // SEP ID + SEPID uint64 `json:"sep_id"` + + // Node IDs + NodeIDs []uint64 `json:"node_ids"` + + // Persistent disk IDs associated with the operation + DiskIDs []uint64 `json:"disk_ids"` + + // API task ID for asynchronous calls + APITask string `json:"apitask"` } // List of audits diff --git a/pkg/cloudapi/compute/affinity_rule_add.go b/pkg/cloudapi/compute/affinity_rule_add.go index c660496..cb764dd 100644 --- a/pkg/cloudapi/compute/affinity_rule_add.go +++ b/pkg/cloudapi/compute/affinity_rule_add.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/constants" "repository.basistech.ru/BASIS/decort-golang-sdk/internal/validators" ) @@ -51,14 +52,14 @@ type AffinityRuleAddRequest struct { type wrapperAffinityRuleAddRequest struct { AffinityRuleAddRequest - AsyncMode bool `url:"async_mode"` + AsyncMode bool `url:"async_mode" json:"async_mode"` } // AffinityRuleAdd add affinity rule -func (c Compute) AffinityRuleAdd(ctx context.Context, req AffinityRuleAddRequest) (bool, error) { +func (c Compute) AffinityRuleAdd(ctx context.Context, req AffinityRuleAddRequest) (AffinityRuleAddResult, error) { err := validators.ValidateRequest(req) if err != nil { - return false, validators.ValidationErrors(validators.GetErrors(err)) + return AffinityRuleAddResult{}, validators.ValidationErrors(validators.GetErrors(err)) } url := "/cloudapi/compute/affinityRuleAdd" @@ -68,17 +69,24 @@ func (c Compute) AffinityRuleAdd(ctx context.Context, req AffinityRuleAddRequest AsyncMode: false, } - res, err := c.client.DecortApiCall(ctx, http.MethodPost, url, aReq) + res, err := c.client.DecortApiCallCtype(ctx, http.MethodPost, url, constants.MIMEJSON, aReq) if err != nil { - return false, err + return AffinityRuleAddResult{}, err } - result, err := strconv.ParseBool(string(res)) - if err != nil { - return false, err + if !req.Migrate { + result, err := strconv.ParseBool(string(res)) + return AffinityRuleAddResult{Ok: result}, err } - return result, nil + var report MigrationReport + + err = json.Unmarshal(res, &report) + if err != nil { + return AffinityRuleAddResult{}, err + } + + return AffinityRuleAddResult{Ok: report.Applied, MigrationReport: &report}, nil } // AsyncAffinityRuleAdd add affinity rule in async mode @@ -95,7 +103,7 @@ func (c Compute) AsyncAffinityRuleAdd(ctx context.Context, req AffinityRuleAddRe AsyncMode: true, } - res, err := c.client.DecortApiCall(ctx, http.MethodPost, url, aReq) + res, err := c.client.DecortApiCallCtype(ctx, http.MethodPost, url, constants.MIMEJSON, aReq) if err != nil { return "", err } diff --git a/pkg/cloudapi/compute/anti_affinity_rule_add.go b/pkg/cloudapi/compute/anti_affinity_rule_add.go index 17ab104..e28386c 100644 --- a/pkg/cloudapi/compute/anti_affinity_rule_add.go +++ b/pkg/cloudapi/compute/anti_affinity_rule_add.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/constants" "repository.basistech.ru/BASIS/decort-golang-sdk/internal/validators" ) @@ -51,14 +52,14 @@ type AntiAffinityRuleAddRequest struct { type wrapperAntiAffinityRuleAddRequest struct { AntiAffinityRuleAddRequest - AsyncMode bool `url:"async_mode"` + AsyncMode bool `url:"async_mode" json:"async_mode"` } // AntiAffinityRuleAdd add anti affinity rule -func (c Compute) AntiAffinityRuleAdd(ctx context.Context, req AntiAffinityRuleAddRequest) (bool, error) { +func (c Compute) AntiAffinityRuleAdd(ctx context.Context, req AntiAffinityRuleAddRequest) (AntiAffinityRuleAddResult, error) { err := validators.ValidateRequest(req) if err != nil { - return false, validators.ValidationErrors(validators.GetErrors(err)) + return AntiAffinityRuleAddResult{}, validators.ValidationErrors(validators.GetErrors(err)) } url := "/cloudapi/compute/antiAffinityRuleAdd" @@ -68,17 +69,24 @@ func (c Compute) AntiAffinityRuleAdd(ctx context.Context, req AntiAffinityRuleAd AsyncMode: false, } - res, err := c.client.DecortApiCall(ctx, http.MethodPost, url, aReq) + res, err := c.client.DecortApiCallCtype(ctx, http.MethodPost, url, constants.MIMEJSON, aReq) if err != nil { - return false, err + return AntiAffinityRuleAddResult{}, err } - result, err := strconv.ParseBool(string(res)) - if err != nil { - return false, err + if !req.Migrate { + result, err := strconv.ParseBool(string(res)) + return AntiAffinityRuleAddResult{Ok: result}, err } - return result, nil + var report MigrationReport + + err = json.Unmarshal(res, &report) + if err != nil { + return AntiAffinityRuleAddResult{}, err + } + + return AntiAffinityRuleAddResult{Ok: report.Applied, MigrationReport: &report}, nil } // AsyncAntiAffinityRuleAdd add anti affinity rule in async mode @@ -95,7 +103,7 @@ func (c Compute) AsyncAntiAffinityRuleAdd(ctx context.Context, req AntiAffinityR AsyncMode: true, } - res, err := c.client.DecortApiCall(ctx, http.MethodPost, url, aReq) + res, err := c.client.DecortApiCallCtype(ctx, http.MethodPost, url, constants.MIMEJSON, aReq) if err != nil { return "", err } diff --git a/pkg/cloudapi/compute/create_template_from_blank.go b/pkg/cloudapi/compute/create_template_from_blank.go index 63f81c9..b94552d 100644 --- a/pkg/cloudapi/compute/create_template_from_blank.go +++ b/pkg/cloudapi/compute/create_template_from_blank.go @@ -39,10 +39,6 @@ type CreateTemplateFromBlankRequest struct { // Required: false Password string `url:"password,omitempty" json:"password,omitempty"` - // Account ID to make the image exclusive - // Required: false - AccountID uint64 `url:"accountId,omitempty" json:"accountId,omitempty"` - // Pool for image create // Required: false PoolName string `url:"poolName,omitempty" json:"poolName,omitempty"` diff --git a/pkg/cloudapi/compute/models.go b/pkg/cloudapi/compute/models.go index f3a89b0..1d248f8 100644 --- a/pkg/cloudapi/compute/models.go +++ b/pkg/cloudapi/compute/models.go @@ -575,6 +575,9 @@ type RecordCompute struct { // Zone ID // Required: false ZoneID uint64 `json:"zoneId"` + + // Folder ID + FolderID uint64 `json:"folder_id"` } // Detailed information about ACL Access in a compute @@ -1564,6 +1567,9 @@ type ItemCompute struct { // Zone ID ZoneID uint64 `json:"zoneId"` + + // Folder ID + FolderID uint64 `json:"folder_id"` } // ListInfoDisks @@ -1743,3 +1749,47 @@ type CloneStatus struct { // Progress percent ProgressPercent int `json:"progress_percent"` } + +type MigrationReportItem struct { + // ID of the compute instance + ComputeID uint64 `json:"compute_id"` + + // Per-VM action result, not workflow status + // Should be one of: + // - migrated: migration completed; the rule may still be unsatisfied + // - skipped: migration was not needed or applicable + // - not_started: submission was not attempted + // - not_completed: completion is unconfirmed; the VM may have moved + Status string `json:"status"` + + // Action details, errors and final rule-placement warnings + Message string `json:"message"` + + // Workflow ID for tracking, or nil if submission was not attempted. + // A non-nil ID does not confirm job acceptance + WorkflowJobID *string `json:"workflow_job_id"` +} + +type MigrationReport struct { + // Rules were saved; does not confirm migration success or rule satisfaction + Applied bool `json:"applied"` + + // Per-compute migration results + Migrations []MigrationReportItem `json:"migrations"` +} + +type AffinityRuleAddResult struct { + // Result of the rule addition + Ok bool + + // Migration report. Populated only when the request was made with Migrate=true, nil otherwise + MigrationReport *MigrationReport +} + +type AntiAffinityRuleAddResult struct { + // Result of the rule addition + Ok bool + + // Migration report. Populated only when the request was made with Migrate=true, nil otherwise + MigrationReport *MigrationReport +} diff --git a/pkg/cloudapi/disks/from_platform_disk.go b/pkg/cloudapi/disks/from_platform_disk.go index fb1b3d4..3d46968 100644 --- a/pkg/cloudapi/disks/from_platform_disk.go +++ b/pkg/cloudapi/disks/from_platform_disk.go @@ -35,10 +35,6 @@ type FromPlatformDiskRequest struct { // Required: false Password string `url:"password,omitempty" json:"password,omitempty"` - // Account ID to make the image exclusive - // Required: false - AccountID uint64 `url:"accountId,omitempty" json:"accountId,omitempty"` - // Pool for image create // Required: false PoolName string `url:"poolName,omitempty" json:"poolName,omitempty"` diff --git a/pkg/cloudapi/extnet/models.go b/pkg/cloudapi/extnet/models.go index 4474d60..6162eed 100644 --- a/pkg/cloudapi/extnet/models.go +++ b/pkg/cloudapi/extnet/models.go @@ -93,6 +93,9 @@ type ItemReservation struct { // Account ID AccountID uint64 `json:"account_id"` + // Auto assignable + AutoAssignable bool `json:"auto_assignable"` + // ClientType ClientType string `json:"clientType"` @@ -127,6 +130,12 @@ type VNFs struct { } type Excluded struct { + // Account ID + AccountID uint64 `json:"account_id"` + + // Auto assignable + AutoAssignable bool `json:"auto_assignable"` + // ClientType ClientType string `json:"clientType"` diff --git a/pkg/cloudapi/kvmx86/create.go b/pkg/cloudapi/kvmx86/create.go index c42a9c7..6c12d93 100644 --- a/pkg/cloudapi/kvmx86/create.go +++ b/pkg/cloudapi/kvmx86/create.go @@ -230,6 +230,10 @@ type CreateRequest struct { // Required: false PreferredCPU []int64 `url:"preferredCpu,omitempty" json:"preferredCpu,omitempty" validate:"omitempty,preferredCPU"` + // List of PCI device IDs to attach to this VM + // Required: false + PCIdevices []uint64 `url:"pcidevices,omitempty" json:"pcidevices,omitempty"` + // Zone ID // Required: false ZoneID uint64 `url:"zoneId,omitempty" json:"zoneId,omitempty"` @@ -252,6 +256,10 @@ type CreateRequest struct { // Required: false // Default: "auto" MemoryBalloonMode string `url:"memory_balloon_mode,omitempty" json:"memory_balloon_mode,omitempty" validate:"omitempty,memoryBalloonMode"` + + // List of vGPU IDs to attach to the VM + // Required: false + VGPUs []uint64 `url:"vgpus,omitempty" json:"vgpus,omitempty"` } // GetRAM returns RAM field values diff --git a/pkg/cloudapi/kvmx86/create_blank.go b/pkg/cloudapi/kvmx86/create_blank.go index 3fe66fc..88743d6 100644 --- a/pkg/cloudapi/kvmx86/create_blank.go +++ b/pkg/cloudapi/kvmx86/create_blank.go @@ -92,6 +92,10 @@ type CreateBlankRequest struct { // Required: false PreferredCPU []int64 `url:"preferredCpu,omitempty" json:"preferredCpu,omitempty" validate:"omitempty,preferredCPU"` + // List of PCI device IDs to attach to this VM + // Required: false + PCIdevices []uint64 `url:"pcidevices,omitempty" json:"pcidevices,omitempty"` + // VM type linux, windows or unknown // Required: false LoaderType string `url:"loaderType,omitempty" json:"loaderType,omitempty" validate:"omitempty,loaderType"` @@ -134,6 +138,10 @@ type CreateBlankRequest struct { // Required: false // Default: "auto" MemoryBalloonMode string `url:"memory_balloon_mode,omitempty" json:"memory_balloon_mode,omitempty" validate:"omitempty,memoryBalloonMode"` + + // List of vGPU IDs to attach to the VM + // Required: false + VGPUs []uint64 `url:"vgpus,omitempty" json:"vgpus,omitempty"` } // GetRAM returns RAM field values diff --git a/pkg/cloudbroker/apiaccess/update.go b/pkg/cloudbroker/apiaccess/update.go index 2b15655..5d2dd7d 100644 --- a/pkg/cloudbroker/apiaccess/update.go +++ b/pkg/cloudbroker/apiaccess/update.go @@ -15,7 +15,7 @@ type UpdateRequest struct { APIAccessID uint64 `url:"apiaccess_id" json:"apiaccess_id" validate:"required"` // APIs to remove from APIAccess group - // Required: false + // Required: true APIs APIsEndpoints `url:"-" json:"-"` } diff --git a/pkg/cloudbroker/audit/models.go b/pkg/cloudbroker/audit/models.go index 556514e..cbf6729 100644 --- a/pkg/cloudbroker/audit/models.go +++ b/pkg/cloudbroker/audit/models.go @@ -43,6 +43,42 @@ type ItemAudit struct { // TTL TTL string `json:"_ttl"` + + // Resource group ID + ResgroupID uint64 `json:"resgroup_id"` + + // Account ID + AccountID uint64 `json:"account_id"` + + // Compute ID + ComputeID uint64 `json:"compute_id"` + + // VINS ID + VINSID uint64 `json:"vins_id"` + + // Service ID + ServiceID uint64 `json:"service_id"` + + // K8S ID + K8SID uint64 `json:"k8s_id"` + + // FLIPGroup ID + FLIPGroupID uint64 `json:"flipgroup_id"` + + // Load balancer ID + LBID uint64 `json:"lb_id"` + + // SEP ID + SEPID uint64 `json:"sep_id"` + + // Node IDs + NodeIDs []uint64 `json:"node_ids"` + + // Persistent disk IDs associated with the operation + DiskIDs []uint64 `json:"disk_ids"` + + // API task ID for asynchronous calls + APITask string `json:"apitask"` } // List of audits diff --git a/pkg/cloudbroker/compute/affinity_rule_add.go b/pkg/cloudbroker/compute/affinity_rule_add.go index 7461e49..b0cabb4 100644 --- a/pkg/cloudbroker/compute/affinity_rule_add.go +++ b/pkg/cloudbroker/compute/affinity_rule_add.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/constants" "repository.basistech.ru/BASIS/decort-golang-sdk/internal/validators" ) @@ -53,14 +54,14 @@ type AffinityRuleAddRequest struct { type wrapperAffinityRuleAddRequest struct { AffinityRuleAddRequest - AsyncMode bool `url:"async_mode"` + AsyncMode bool `url:"async_mode" json:"async_mode"` } // AffinityRuleAdd adds affinity rule -func (c Compute) AffinityRuleAdd(ctx context.Context, req AffinityRuleAddRequest) (bool, error) { +func (c Compute) AffinityRuleAdd(ctx context.Context, req AffinityRuleAddRequest) (AffinityRuleAddResult, error) { err := validators.ValidateRequest(req) if err != nil { - return false, validators.ValidationErrors(validators.GetErrors(err)) + return AffinityRuleAddResult{}, validators.ValidationErrors(validators.GetErrors(err)) } url := "/cloudbroker/compute/affinityRuleAdd" @@ -70,17 +71,24 @@ func (c Compute) AffinityRuleAdd(ctx context.Context, req AffinityRuleAddRequest AsyncMode: false, } - res, err := c.client.DecortApiCall(ctx, http.MethodPost, url, aReq) + res, err := c.client.DecortApiCallCtype(ctx, http.MethodPost, url, constants.MIMEJSON, aReq) if err != nil { - return false, err + return AffinityRuleAddResult{}, err } - result, err := strconv.ParseBool(string(res)) - if err != nil { - return false, err + if !req.Migrate { + result, err := strconv.ParseBool(string(res)) + return AffinityRuleAddResult{Ok: result}, err } - return result, nil + var report MigrationReport + + err = json.Unmarshal(res, &report) + if err != nil { + return AffinityRuleAddResult{}, err + } + + return AffinityRuleAddResult{Ok: report.Applied, MigrationReport: &report}, nil } // AsyncAffinityRuleAdd add affinity rule in async mode @@ -97,7 +105,7 @@ func (c Compute) AsyncAffinityRuleAdd(ctx context.Context, req AffinityRuleAddRe AsyncMode: true, } - res, err := c.client.DecortApiCall(ctx, http.MethodPost, url, aReq) + res, err := c.client.DecortApiCallCtype(ctx, http.MethodPost, url, constants.MIMEJSON, aReq) if err != nil { return "", err } diff --git a/pkg/cloudbroker/compute/anti_affinity_rule_add.go b/pkg/cloudbroker/compute/anti_affinity_rule_add.go index cc6dd98..6793448 100644 --- a/pkg/cloudbroker/compute/anti_affinity_rule_add.go +++ b/pkg/cloudbroker/compute/anti_affinity_rule_add.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/constants" "repository.basistech.ru/BASIS/decort-golang-sdk/internal/validators" ) @@ -51,14 +52,14 @@ type AntiAffinityRuleAddRequest struct { type wrapperAntiAffinityRuleAddRequest struct { AntiAffinityRuleAddRequest - AsyncMode bool `url:"async_mode"` + AsyncMode bool `url:"async_mode" json:"async_mode"` } // AntiAffinityRuleAdd adds anti affinity rule -func (c Compute) AntiAffinityRuleAdd(ctx context.Context, req AntiAffinityRuleAddRequest) (bool, error) { +func (c Compute) AntiAffinityRuleAdd(ctx context.Context, req AntiAffinityRuleAddRequest) (AntiAffinityRuleAddResult, error) { err := validators.ValidateRequest(req) if err != nil { - return false, validators.ValidationErrors(validators.GetErrors(err)) + return AntiAffinityRuleAddResult{}, validators.ValidationErrors(validators.GetErrors(err)) } url := "/cloudbroker/compute/antiAffinityRuleAdd" @@ -68,17 +69,24 @@ func (c Compute) AntiAffinityRuleAdd(ctx context.Context, req AntiAffinityRuleAd AsyncMode: false, } - res, err := c.client.DecortApiCall(ctx, http.MethodPost, url, aReq) + res, err := c.client.DecortApiCallCtype(ctx, http.MethodPost, url, constants.MIMEJSON, aReq) if err != nil { - return false, err + return AntiAffinityRuleAddResult{}, err } - result, err := strconv.ParseBool(string(res)) - if err != nil { - return false, err + if !req.Migrate { + result, err := strconv.ParseBool(string(res)) + return AntiAffinityRuleAddResult{Ok: result}, err } - return result, nil + var report MigrationReport + + err = json.Unmarshal(res, &report) + if err != nil { + return AntiAffinityRuleAddResult{}, err + } + + return AntiAffinityRuleAddResult{Ok: report.Applied, MigrationReport: &report}, nil } // AsyncAntiAffinityRuleAdd add anti affinity rule in async mode @@ -95,7 +103,7 @@ func (c Compute) AsyncAntiAffinityRuleAdd(ctx context.Context, req AntiAffinityR AsyncMode: true, } - res, err := c.client.DecortApiCall(ctx, http.MethodPost, url, aReq) + res, err := c.client.DecortApiCallCtype(ctx, http.MethodPost, url, constants.MIMEJSON, aReq) if err != nil { return "", err } diff --git a/pkg/cloudbroker/compute/migrate_v2.go b/pkg/cloudbroker/compute/migrate_v2.go new file mode 100644 index 0000000..af5fffc --- /dev/null +++ b/pkg/cloudbroker/compute/migrate_v2.go @@ -0,0 +1,83 @@ +package compute + +import ( + "context" + "encoding/json" + "net/http" + + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/constants" + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/validators" +) + +// Target describes a target for migration +type Target struct { + // SEP ID + // Required: true + SepID uint64 `url:"sep_id" json:"sep_id" validate:"required"` + + // Name of a pool + // Required: true + PoolName string `url:"pool_name" json:"pool_name" validate:"required"` + + // Storage policy ID + // Required: false + StoragePolicyID uint64 `url:"storage_policy_id,omitempty" json:"storage_policy_id,omitempty"` + + // Disk IDs + // Required: false + DiskIDs []uint64 `url:"disk_ids,omitempty" json:"disk_ids,omitempty"` +} + +// Storage describes a destination for a disk +type Storage struct { + // Target for migration + // Required: false + Targets []Target `url:"targets,omitempty" json:"targets,omitempty"` + + // Whether a separate source-disk cleanup job should be started after full success + // Required: false + AutoCleanup interface{} `url:"auto_cleanup,omitempty" json:"auto_cleanup,omitempty" validate:"omitempty,isBool"` +} + +// MigrateV2Request to start a migration of a compute or a disk +type MigrateV2Request struct { + // ID of compute to migrate + // Required: true + ComputeID uint64 `url:"compute_id" json:"compute_id" validate:"required"` + + // Stateless load balancers support only compute mode, including their local system disk. + // Required: false + Mode string `url:"mode,omitempty" json:"mode,omitempty" validate:"omitempty,migrateV2Mode"` + + // ID of node to migrate to + // Required: false + TargetNodeID uint64 `url:"target_node_id,omitempty" json:"target_node_id,omitempty"` + + // Destination for a disk + // Required: false + Storage Storage `url:"storage,omitempty" json:"storage,omitempty"` +} + +// MigrateV2 starts a migration of a compute or a disk +func (c Compute) MigrateV2(ctx context.Context, req MigrateV2Request) (string, error) { + err := validators.ValidateRequest(req) + if err != nil { + return "", validators.ValidationErrors(validators.GetErrors(err)) + } + + url := "/cloudbroker/compute/migrate_v2" + + res, err := c.client.DecortApiCallCtype(ctx, http.MethodPost, url, constants.MIMEJSON, req) + if err != nil { + return "", err + } + + var resp string + + err = json.Unmarshal(res, &resp) + if err != nil { + return "", err + } + + return resp, nil +} diff --git a/pkg/cloudbroker/compute/migrate_v2_abort.go b/pkg/cloudbroker/compute/migrate_v2_abort.go new file mode 100644 index 0000000..5fbea8b --- /dev/null +++ b/pkg/cloudbroker/compute/migrate_v2_abort.go @@ -0,0 +1,47 @@ +package compute + +import ( + "context" + "encoding/json" + "net/http" + + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/constants" + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/validators" +) + +// MigrateV2AbortRequest to abort a migration of a compute +type MigrateV2AbortRequest struct { + // Original migration workflow IDs, not agent, cleanup or recovery job IDs + // Required: true + WorkflowJobIDs []string `url:"workflow_job_ids" json:"workflow_job_ids" validate:"required"` +} + +// MigrateV2Abort aborts a migration of a compute +func (c Compute) MigrateV2Abort(ctx context.Context, req MigrateV2AbortRequest) (*MigrateV2AbortRecord, error) { + res, err := c.MigrateV2AbortRaw(ctx, req) + if err != nil { + return nil, err + } + + info := MigrateV2AbortRecord{} + + err = json.Unmarshal(res, &info) + if err != nil { + return nil, err + } + + return &info, nil +} + +// MigrateV2AbortRaw aborts a migration of a compute and returns an array of bytes +func (c Compute) MigrateV2AbortRaw(ctx context.Context, req MigrateV2AbortRequest) ([]byte, error) { + err := validators.ValidateRequest(req) + if err != nil { + return nil, validators.ValidationErrors(validators.GetErrors(err)) + } + + url := "/cloudbroker/compute/migrate_v2_abort" + + res, err := c.client.DecortApiCallCtype(ctx, http.MethodPost, url, constants.MIMEJSON, req) + return res, err +} diff --git a/pkg/cloudbroker/compute/migrate_v2_cleanup.go b/pkg/cloudbroker/compute/migrate_v2_cleanup.go new file mode 100644 index 0000000..f00f734 --- /dev/null +++ b/pkg/cloudbroker/compute/migrate_v2_cleanup.go @@ -0,0 +1,41 @@ +package compute + +import ( + "context" + "encoding/json" + "net/http" + + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/constants" + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/validators" +) + +// MigrateV2CleanupRequest to clean up after a migration of a compute +type MigrateV2CleanupRequest struct { + // Original migration workflow IDs, not agent, cleanup or recovery job IDs + // Required: true + WorkflowJobID string `url:"workflow_job_id" json:"workflow_job_id" validate:"required"` +} + +// MigrateV2Cleanup cleans up after a migration of a compute +func (c Compute) MigrateV2Cleanup(ctx context.Context, req MigrateV2CleanupRequest) (string, error) { + err := validators.ValidateRequest(req) + if err != nil { + return "", validators.ValidationErrors(validators.GetErrors(err)) + } + + url := "/cloudbroker/compute/migrate_v2_cleanup" + + res, err := c.client.DecortApiCallCtype(ctx, http.MethodPost, url, constants.MIMEJSON, req) + if err != nil { + return "", err + } + + var resp string + + err = json.Unmarshal(res, &resp) + if err != nil { + return "", err + } + + return resp, nil +} diff --git a/pkg/cloudbroker/compute/migrate_v2_get.go b/pkg/cloudbroker/compute/migrate_v2_get.go new file mode 100644 index 0000000..44c67b8 --- /dev/null +++ b/pkg/cloudbroker/compute/migrate_v2_get.go @@ -0,0 +1,46 @@ +package compute + +import ( + "context" + "encoding/json" + "net/http" + + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/validators" +) + +// MigrateV2GetRequest to get the information about a workflow job +type MigrateV2GetRequest struct { + // Original migration workflow IDs + // Required: true + WorkflowJobID string `url:"workflow_job_id" json:"workflow_job_id" validate:"required"` +} + +// MigrateV2Get gets information about a workflow job as a MigrateV2Record struct +func (c Compute) MigrateV2Get(ctx context.Context, req MigrateV2GetRequest) (*MigrateV2Record, error) { + res, err := c.MigrateV2GetRaw(ctx, req) + if err != nil { + return nil, err + } + + info := MigrateV2Record{} + + err = json.Unmarshal(res, &info) + if err != nil { + return nil, err + } + + return &info, nil +} + +// GetRaw gets information about a workflow job as an array of bytes +func (c Compute) MigrateV2GetRaw(ctx context.Context, req MigrateV2GetRequest) ([]byte, error) { + err := validators.ValidateRequest(req) + if err != nil { + return nil, validators.ValidationErrors(validators.GetErrors(err)) + } + + url := "/cloudbroker/compute/migrate_v2_get" + + res, err := c.client.DecortApiCall(ctx, http.MethodGet, url, req) + return res, err +} diff --git a/pkg/cloudbroker/compute/migrate_v2_list.go b/pkg/cloudbroker/compute/migrate_v2_list.go new file mode 100644 index 0000000..e8ae26b --- /dev/null +++ b/pkg/cloudbroker/compute/migrate_v2_list.go @@ -0,0 +1,67 @@ +package compute + +import ( + "context" + "encoding/json" + "net/http" + + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/validators" +) + +// MigrateV2ListRequest to list migrations +type MigrateV2ListRequest struct { + // Exact APIServer migration workflow job ID + // Required: false + WorkflowJobID string `url:"workflow_job_id,omitempty" json:"workflow_job_id,omitempty"` + + // Compute ID + // Required: false + ComputeID uint64 `url:"compute_id,omitempty" json:"compute_id,omitempty"` + + // Exact business migration status + // Required: false + Status string `url:"status,omitempty" json:"status,omitempty"` + + // Source node ID + // Required: false + SourceNodeID uint64 `url:"source_node_id,omitempty" json:"source_node_id,omitempty"` + + // Target node ID + // Required: false + TargetNodeID uint64 `url:"target_node_id,omitempty" json:"target_node_id,omitempty"` + + // Whether retained source disks still require cleanup + // Required: false + CleanupRequired interface{} `url:"cleanup_required,omitempty" json:"cleanup_required,omitempty" validate:"omitempty,isBool"` + + // Page number + // Required: false + Page uint64 `url:"page,omitempty" json:"page,omitempty"` + + // Page size + // Required: false + Size uint64 `url:"size,omitempty" json:"size,omitempty"` +} + +// MigrateV2List gets list of the jobs +func (c Compute) MigrateV2List(ctx context.Context, req MigrateV2ListRequest) (*ListMigrateV2, error) { + if err := validators.ValidateRequest(req); err != nil { + return nil, validators.ValidationErrors(validators.GetErrors(err)) + } + + url := "/cloudbroker/compute/migrate_v2_list" + + res, err := c.client.DecortApiCall(ctx, http.MethodGet, url, req) + if err != nil { + return nil, err + } + + list := ListMigrateV2{} + + err = json.Unmarshal(res, &list) + if err != nil { + return nil, err + } + + return &list, nil +} diff --git a/pkg/cloudbroker/compute/migrate_v2_resolve.go b/pkg/cloudbroker/compute/migrate_v2_resolve.go new file mode 100644 index 0000000..8367008 --- /dev/null +++ b/pkg/cloudbroker/compute/migrate_v2_resolve.go @@ -0,0 +1,51 @@ +package compute + +import ( + "context" + "encoding/json" + "net/http" + + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/constants" + "repository.basistech.ru/BASIS/decort-golang-sdk/internal/validators" +) + +// MigrateResolveRequest to resolve a migration +type MigrateV2ResolveRequest struct { + // Workflow job ID + // Required: true + WorkflowJobID string `url:"workflow_job_id" json:"workflow_job_id" validate:"required"` + + // Take over cleanup + // Required: false + TakeOverCleanup interface{} `url:"take_over_cleanup,omitempty" json:"take_over_cleanup,omitempty" validate:"omitempty,isBool"` +} + +// MigrateV2Resolve resolves a migration of a compute +func (c Compute) MigrateV2Resolve(ctx context.Context, req MigrateV2ResolveRequest) (*MigrateV2ResolveRecord, error) { + res, err := c.MigrateV2ResolveRaw(ctx, req) + if err != nil { + return nil, err + } + + info := MigrateV2ResolveRecord{} + + err = json.Unmarshal(res, &info) + if err != nil { + return nil, err + } + + return &info, nil +} + +// MigrateV2ResolveRaw resolves a migration of a compute and returns an array of bytes +func (c Compute) MigrateV2ResolveRaw(ctx context.Context, req MigrateV2ResolveRequest) ([]byte, error) { + err := validators.ValidateRequest(req) + if err != nil { + return nil, validators.ValidationErrors(validators.GetErrors(err)) + } + + url := "/cloudbroker/compute/migrate_v2_resolve" + + res, err := c.client.DecortApiCallCtype(ctx, http.MethodPost, url, constants.MIMEJSON, req) + return res, err +} diff --git a/pkg/cloudbroker/compute/models.go b/pkg/cloudbroker/compute/models.go index b7adc8c..a7d10aa 100644 --- a/pkg/cloudbroker/compute/models.go +++ b/pkg/cloudbroker/compute/models.go @@ -938,6 +938,9 @@ type InfoCompute struct { // Zone ID ZoneID uint64 `json:"zoneId"` + + // Folder ID + FolderID uint64 `json:"folder_id"` } type QemuQuest struct { @@ -1252,6 +1255,9 @@ type RecordCompute struct { //todo _ uint64 `json:"nodeId"` + + // Folder ID + FolderID uint64 `json:"folder_id"` } // Detailed information about ACL Access in a compute @@ -2272,3 +2278,376 @@ type CheckComputePlacementItem struct { } type CheckComputePlacementResult map[uint64]CheckComputePlacementItem + +type Result struct { + // Workflow ID + WorkflowJobID string `json:"workflow_job_id"` + + // Compute ID + ComputeID uint64 `json:"compute_id"` + + // Status + Status string `json:"status"` + + // Job ID + JobID string `json:"job_id"` + + // Message + Message string `json:"message"` +} + +// MigrateV2 abort record +type MigrateV2AbortRecord struct { + // Results + Results []Result `json:"results"` +} + +type Origin struct { + // Type + Type string `json:"type"` + + // Correlation ID + CorrelationID string `json:"correlation_id"` +} + +type Observation struct { + // Updated at + UpdatedAt uint64 `json:"updated_at"` + + // Agent job ID + AgentJobID string `json:"agent_job_id"` + + // Attempt + Attempt uint64 `json:"attempt"` + + // Operation + Operation string `json:"operation"` +} + +type Memory struct { + // Sampled at + SampledAt uint64 `json:"sampled_at"` + + // Percent + Percent uint64 `json:"percent"` + + // Bytes per second + BytesPerSecond uint64 `json:"bytes_per_second"` + + // Processed bytes + ProcessedBytes uint64 `json:"processed_bytes"` + + // Remaining bytes + RemainingBytes uint64 `json:"remaining_bytes"` +} + +type ProgressDisks struct { + // Scope + Scope string `json:"scope"` + + // Target devs + TargetDevs []string `json:"target_devs"` + + // Sampled at + SampledAt uint64 `json:"sampled_at"` + + // Percent + Percent uint64 `json:"percent"` + + // Bytes per second + BytesPerSecond uint64 `json:"bytes_per_second"` + + // Processed bytes + ProcessedBytes uint64 `json:"processed_bytes"` + + // Remaining bytes + RemainingBytes uint64 `json:"remaining_bytes"` +} + +type SystemDisk struct { + // Target dev + TargetDev string `json:"target_dev"` + + // Sampled at + SampledAt uint64 `json:"sampled_at"` + + // Percent + Percent uint64 `json:"percent"` + + // Bytes per second + BytesPerSecond uint64 `json:"bytes_per_second"` + + // Processed bytes + ProcessedBytes uint64 `json:"processed_bytes"` + + // Remaining bytes + RemainingBytes uint64 `json:"remaining_bytes"` +} + +type Progress struct { + // Observation + Observation Observation `json:"observation"` + + // Memory + Memory Memory `json:"memory"` + + // Disks + Disks ProgressDisks `json:"disks"` + + // System disk + SystemDisk SystemDisk `json:"system_disk"` +} + +type Disk struct { + // Source disk ID + SourceDiskID uint64 `json:"source_disk_id"` + + // Target disk ID + TargetDiskID uint64 `json:"target_disk_id"` + + // Target dev + TargetDev string `json:"target_dev"` + + // SEP ID + SEPID uint64 `json:"sep_id"` + + // Pool name + PoolName string `json:"pool_name"` + + // Storage policy ID + StoragePolicyID uint64 `json:"storage_policy_id"` + + // State + State string `json:"state"` +} + +type Cleanup struct { + // Job ID + JobID string `json:"job_id"` + + // Status + Status string `json:"status"` + + // Error + Error string `json:"error"` +} + +type Resolution struct { + // Resolved at + ResolvedAt string `json:"resolved_at"` + + // Resolved by + ResolvedBy string `json:"resolved_by"` + + // RequestID + RequestId string `json:"request_id"` + + // Take over cleanup + TakeOverCleanup bool `json:"take_over_cleanup"` + + // Previous status + PreviousStatus string `json:"previous_status"` + + // Previous cleanup required + PreviousCleanupRequired bool `json:"previous_cleanup_required"` + + // Previous error + PreviousError string `json:"previous_error"` +} + +type MigrateV2Item struct { + // Workflow Job ID + WorkflowJobID string `json:"workflow_job_id"` + + // Mode + Mode string `json:"mode"` + + // Origin + Origin Origin `json:"origin"` + + // Compute ID + ComputeID uint64 `json:"compute_id"` + + // Progress + Progress Progress `json:"progress"` + + // Domain name + DomainName string `json:"domain_name"` + + // Domain UUID + DomainUUID string `json:"domain_uuid"` + + // Source node ID + SourceNodeID uint64 `json:"source_node_id"` + + // Target node ID + TargetNodeID uint64 `json:"target_node_id"` + + // Source tech status + SourceTechStatus string `json:"source_tech_status"` + + // Status + Status string `json:"status"` + + // Migration result confirmed + MigrationResultConfirmed bool `json:"migration_result_confirmed"` + + // Disks + Disks []Disk `json:"disks"` + + // Disk IDs not copied + DiskIDsNotCopied []uint64 `json:"disk_ids_not_copied"` + + // Error + Error string `json:"error"` + + // Cleanup required + CleanupRequired bool `json:"cleanup_required"` + + // Cleanup + Cleanup Cleanup `json:"cleanup"` + + // Resolution + Resolution Resolution `json:"resolution"` + + // Created at + CreatedAt string `json:"created_at"` + + // Updated at + UpdatedAt string `json:"updated_at"` +} + +type ListMigrateV2 struct { + // Entry count + EntryCount uint64 `json:"entry_count"` + + // Data + Data []MigrateV2Item `json:"data"` +} + +type MigrateV2Record struct { + // Workflow Job ID + WorkflowJobID string `json:"workflow_job_id"` + + // Mode + Mode string `json:"mode"` + + // Origin + Origin Origin `json:"origin"` + + // Compute ID + ComputeID uint64 `json:"compute_id"` + + // Progress + Progress Progress `json:"progress"` + + // Domain name + DomainName string `json:"domain_name"` + + // Domain UUID + DomainUUID string `json:"domain_uuid"` + + // Source node ID + SourceNodeID uint64 `json:"source_node_id"` + + // Target node ID + TargetNodeID uint64 `json:"target_node_id"` + + // Source tech status + SourceTechStatus string `json:"source_tech_status"` + + // Status + Status string `json:"status"` + + // Migration result confirmed + MigrationResultConfirmed bool `json:"migration_result_confirmed"` + + // Disks + Disks []Disk `json:"disks"` + + // Disk IDs not copied + DiskIDsNotCopied []uint64 `json:"disk_ids_not_copied"` + + // Error + Error string `json:"error"` + + // Cleanup required + CleanupRequired bool `json:"cleanup_required"` + + // Cleanup + Cleanup Cleanup `json:"cleanup"` + + // Resolution + Resolution Resolution `json:"resolution"` + + // Created at + CreatedAt string `json:"created_at"` + + // Updated at + UpdatedAt string `json:"updated_at"` + + // Logs + Logs []string `json:"logs"` +} + +// Migrate V2 resolve record +type MigrateV2ResolveRecord struct { + // Workflow job ID + WorkflowJobID string `json:"workflow_job_id"` + + // Compute ID + ComputeID uint64 `json:"compute_id"` + + // Status + Status string `json:"status"` + + // Cleanup required + CleanupRequired bool `json:"cleanup_required"` + + // Resolution + Resolution Resolution `json:"resolution"` +} + +type MigrationReportItem struct { + // ID of the compute instance + ComputeID uint64 `json:"compute_id"` + + // Per-VM action result, not workflow status + // Should be one of: + // - migrated: migration completed; the rule may still be unsatisfied + // - skipped: migration was not needed or applicable + // - not_started: submission was not attempted + // - not_completed: completion is unconfirmed; the VM may have moved + Status string `json:"status"` + + // Action details, errors and final rule-placement warnings + Message string `json:"message"` + + // Workflow ID for tracking, or nil if submission was not attempted. + // A non-nil ID does not confirm job acceptance + WorkflowJobID *string `json:"workflow_job_id"` +} + +type MigrationReport struct { + // Rules were saved; does not confirm migration success or rule satisfaction + Applied bool `json:"applied"` + + // Per-compute migration results + Migrations []MigrationReportItem `json:"migrations"` +} + +type AffinityRuleAddResult struct { + // Result of the rule addition + Ok bool + + // Migration report. Populated only when the request was made with Migrate=true, nil otherwise + MigrationReport *MigrationReport +} + +type AntiAffinityRuleAddResult struct { + // Result of the rule addition + Ok bool + + // Migration report. Populated only when the request was made with Migrate=true, nil otherwise + MigrationReport *MigrationReport +} diff --git a/pkg/cloudbroker/extnet/models.go b/pkg/cloudbroker/extnet/models.go index 01d7f8c..63d11b1 100644 --- a/pkg/cloudbroker/extnet/models.go +++ b/pkg/cloudbroker/extnet/models.go @@ -20,12 +20,21 @@ type ItemReservation struct { // Account ID AccountID uint64 `json:"account_id"` + // Auto assignable + AutoAssignable bool `json:"auto_assignable"` + // Client type ClientType string `json:"clientType"` // Description Description string `json:"desc"` + // Domain name + DomainName string `json:"domainname"` + + // Hostname + Hostname string `json:"hostname"` + // IP IP string `json:"ip"` @@ -37,12 +46,6 @@ type ItemReservation struct { // Virtual machine ID VMID uint64 `json:"vmId"` - - // Domain name - DomainName string `json:"domainname"` - - // Hostname - Hostname string `json:"hostname"` } // List reservations diff --git a/pkg/cloudbroker/grid/models.go b/pkg/cloudbroker/grid/models.go index d71fd10..9475df7 100644 --- a/pkg/cloudbroker/grid/models.go +++ b/pkg/cloudbroker/grid/models.go @@ -117,6 +117,9 @@ type RecordGrid struct { // Budget Budget bool `json:"budget"` + + // Whether VM folders are enabled + VMFoldersEnabled bool `json:"vm_folders_enabled"` } // Information about grid @@ -165,6 +168,9 @@ type ItemGridList struct { // Budget Budget bool `json:"budget"` + + // Whether VM folders are enabled + VMFoldersEnabled bool `json:"vm_folders_enabled"` } // List Grids diff --git a/pkg/cloudbroker/kvmx86/create.go b/pkg/cloudbroker/kvmx86/create.go index 46229f6..39903d4 100644 --- a/pkg/cloudbroker/kvmx86/create.go +++ b/pkg/cloudbroker/kvmx86/create.go @@ -240,6 +240,10 @@ type CreateRequest struct { // Required: false PreferredCPU []int64 `url:"preferredCpu,omitempty" json:"preferredCpu,omitempty" validate:"omitempty,preferredCPU"` + // List of PCI device IDs to attach to this VM + // Required: false + PCIdevices []uint64 `url:"pcidevices,omitempty" json:"pcidevices,omitempty"` + // Zone ID // Required: false ZoneID uint64 `url:"zoneId,omitempty" json:"zoneId,omitempty"` @@ -275,6 +279,10 @@ type CreateRequest struct { // Required: false // Default: "auto" MemoryBalloonMode string `url:"memory_balloon_mode,omitempty" json:"memory_balloon_mode,omitempty" validate:"omitempty,memoryBalloonMode"` + + // List of vGPU IDs to attach to the VM + // Required: false + VGPUs []uint64 `url:"vgpus,omitempty" json:"vgpus,omitempty"` } // GetRAM returns RAM field values diff --git a/pkg/cloudbroker/kvmx86/create_blank.go b/pkg/cloudbroker/kvmx86/create_blank.go index 542340d..adb7bb3 100644 --- a/pkg/cloudbroker/kvmx86/create_blank.go +++ b/pkg/cloudbroker/kvmx86/create_blank.go @@ -92,6 +92,10 @@ type CreateBlankRequest struct { // Required: false PreferredCPU []int64 `url:"preferredCpu,omitempty" json:"preferredCpu,omitempty" validate:"omitempty,preferredCPU"` + // List of PCI device IDs to attach to this VM + // Required: false + PCIdevices []uint64 `url:"pcidevices,omitempty" json:"pcidevices,omitempty"` + // VM type linux, windows or unknown // Required: false LoaderType string `url:"loaderType,omitempty" json:"loaderType,omitempty" validate:"omitempty,loaderType"` @@ -147,6 +151,10 @@ type CreateBlankRequest struct { // Required: false // Default: "auto" MemoryBalloonMode string `url:"memory_balloon_mode,omitempty" json:"memory_balloon_mode,omitempty" validate:"omitempty,memoryBalloonMode"` + + // List of vGPU IDs to attach to the VM + // Required: false + VGPUs []uint64 `url:"vgpus,omitempty" json:"vgpus,omitempty"` } // GetRAM returns RAM field values diff --git a/samples/client/client_test.go b/samples/client/client_test.go index dc67fb5..42b5c62 100644 --- a/samples/client/client_test.go +++ b/samples/client/client_test.go @@ -68,7 +68,7 @@ func TestMigrator(t *testing.T) { ComputeID: dxVMID, TargetNodeID: nodeID, }, - SyncMode: true})).Return(b, nil).AnyTimes() + AsyncMode: false})).Return(b, nil).AnyTimes() ok, err := migrator.Migrate(context.Background(), dxVMID, nodeID) assert.NoError(t, err) diff --git a/tests/platform_upgrade/cloud_test.go b/tests/platform_upgrade/cloud_test.go index c35ac9f..28dcf32 100644 --- a/tests/platform_upgrade/cloud_test.go +++ b/tests/platform_upgrade/cloud_test.go @@ -87,8 +87,10 @@ func TestGetListCloudAPI(t *testing.T) { getResult("Account list", bytes, account.ListAccounts{}, t) // Get - listAcc, _ := client.CloudAPI().Account().List(context.Background(), account.ListRequest{}) - if len(listAcc.Data) > 0 { + listAcc, err := client.CloudAPI().Account().List(context.Background(), account.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.Account.List failed: %v", err) + } else if len(listAcc.Data) > 0 { id := listAcc.Data[0].ID bytes, err = client.CloudAPI().Account().GetRaw(context.Background(), account.GetRequest{AccountID: id}) if err != nil { @@ -107,8 +109,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("Audit list", bytes, audit.ListAudits{}, t) // Get - listAudits, _ := client.CloudAPI().Audit().List(context.Background(), audit.ListRequest{}) - if len(listAudits.Data) > 0 { + listAudits, err := client.CloudAPI().Audit().List(context.Background(), audit.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.Audit.List failed: %v", err) + } else if len(listAudits.Data) > 0 { id := listAudits.Data[0].GUID bytes, err = client.CloudAPI().Audit().GetRaw(context.Background(), audit.GetRequest{AuditGuid: id}) if err != nil { @@ -127,8 +131,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("Bservice list", bytes, bservice.ListBasicServices{}, t) // Get - listBServ, _ := client.CloudAPI().BService().List(context.Background(), bservice.ListRequest{}) - if len(listBServ.Data) > 0 { + listBServ, err := client.CloudAPI().BService().List(context.Background(), bservice.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.BService.List failed: %v", err) + } else if len(listBServ.Data) > 0 { id := listBServ.Data[0].ID bytes, err = client.CloudAPI().BService().GetRaw(context.Background(), bservice.GetRequest{ServiceID: id}) if err != nil { @@ -147,8 +153,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("Compute list", bytes, compute.ListComputes{}, t) // Get - listComp, _ := client.CloudAPI().Compute().List(context.Background(), compute.ListRequest{}) - if listComp != nil && len(listComp.Data) > 0 { + listComp, err := client.CloudAPI().Compute().List(context.Background(), compute.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.Compute.List failed: %v", err) + } else if listComp != nil && len(listComp.Data) > 0 { id := listComp.Data[0].ID bytes, err = client.CloudAPI().Compute().GetRaw(context.Background(), compute.GetRequest{ComputeID: id}) if err != nil { @@ -173,8 +181,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("Disk list", bytes, disks.ListDisks{}, t) // Get - listDisk, _ := client.CloudAPI().Disks().List(context.Background(), disks.ListRequest{}) - if len(listDisk.Data) > 0 { + listDisk, err := client.CloudAPI().Disks().List(context.Background(), disks.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.Disks.List failed: %v", err) + } else if len(listDisk.Data) > 0 { id := listDisk.Data[0].ID bytes, err = client.CloudAPI().Disks().GetRaw(context.Background(), disks.GetRequest{DiskID: id}) if err != nil { @@ -193,8 +203,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("ExtNet list", bytes, extnet.ListExtNets{}, t) // Get - listExtNet, _ := client.CloudAPI().ExtNet().List(context.Background(), extnet.ListRequest{}) - if len(listExtNet.Data) > 0 { + listExtNet, err := client.CloudAPI().ExtNet().List(context.Background(), extnet.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.ExtNet.List failed: %v", err) + } else if len(listExtNet.Data) > 0 { id := listExtNet.Data[0].ID bytes, err = client.CloudAPI().ExtNet().GetRaw(context.Background(), extnet.GetRequest{NetID: id}) if err != nil { @@ -213,8 +225,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("FLIPGroup list", bytes, flipgroup.ListFLIPGroups{}, t) // Get - listFG, _ := client.CloudAPI().FLIPGroup().List(context.Background(), flipgroup.ListRequest{}) - if len(listFG.Data) > 0 { + listFG, err := client.CloudAPI().FLIPGroup().List(context.Background(), flipgroup.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.FLIPGroup.List failed: %v", err) + } else if len(listFG.Data) > 0 { id := listFG.Data[0].ID bytes, err = client.CloudAPI().FLIPGroup().GetRaw(context.Background(), flipgroup.GetRequest{FLIPGroupID: id}) if err != nil { @@ -233,8 +247,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("Image list", bytes, image.ListImages{}, t) // Get - listImg, _ := client.CloudAPI().Image().List(context.Background(), image.ListRequest{}) - if len(listImg.Data) > 0 { + listImg, err := client.CloudAPI().Image().List(context.Background(), image.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.Image.List failed: %v", err) + } else if len(listImg.Data) > 0 { id := listImg.Data[0].ID bytes, err = client.CloudAPI().Image().GetRaw(context.Background(), image.GetRequest{ImageID: id}) if err != nil { @@ -253,8 +269,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("K8CI list", bytes, k8ci.ListK8CI{}, t) // Get - listk8ci, _ := client.CloudAPI().K8CI().List(context.Background(), k8ci.ListRequest{}) - if len(listk8ci.Data) > 0 { + listk8ci, err := client.CloudAPI().K8CI().List(context.Background(), k8ci.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.K8CI.List failed: %v", err) + } else if len(listk8ci.Data) > 0 { id := listk8ci.Data[0].ID bytes, err = client.CloudAPI().K8CI().GetRaw(context.Background(), k8ci.GetRequest{K8CIID: id}) if err != nil { @@ -273,8 +291,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("K8S list", bytes, k8s.ListK8SClusters{}, t) // Get - listk8s, _ := client.CloudAPI().K8S().List(context.Background(), k8s.ListRequest{}) - if len(listk8s.Data) > 0 { + listk8s, err := client.CloudAPI().K8S().List(context.Background(), k8s.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.K8S.List failed: %v", err) + } else if len(listk8s.Data) > 0 { id := listk8s.Data[0].ID bytes, err = client.CloudAPI().K8S().GetRaw(context.Background(), k8s.GetRequest{K8SID: id}) if err != nil { @@ -293,8 +313,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("LB list", bytes, lb.ListLB{}, t) // Get - listLB, _ := client.CloudAPI().LB().List(context.Background(), lb.ListRequest{}) - if len(listLB.Data) > 0 { + listLB, err := client.CloudAPI().LB().List(context.Background(), lb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.LB.List failed: %v", err) + } else if len(listLB.Data) > 0 { id := listLB.Data[0].ID bytes, err = client.CloudAPI().LB().GetRaw(context.Background(), lb.GetRequest{LBID: id}) if err != nil { @@ -321,8 +343,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("QoS policy list", bytes, qospolicy.ListQoSPolicies{}, t) // Get - listQosPolicies, _ := client.CloudAPI().QoSPolicy().List(context.Background(), qospolicy.ListRequest{}) - if len(listQosPolicies.Data) > 0 { + listQosPolicies, err := client.CloudAPI().QoSPolicy().List(context.Background(), qospolicy.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.QoSPolicy.List failed: %v", err) + } else if len(listQosPolicies.Data) > 0 { id := listQosPolicies.Data[0].ID bytes, err = client.CloudAPI().QoSPolicy().GetRaw(context.Background(), qospolicy.GetRequest{QoSPolicyID: id}) if err != nil { @@ -341,8 +365,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("RG list", bytes, rg.ListResourceGroups{}, t) // Get - listRG, _ := client.CloudAPI().RG().List(context.Background(), rg.ListRequest{}) - if len(listRG.Data) > 0 { + listRG, err := client.CloudAPI().RG().List(context.Background(), rg.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.RG.List failed: %v", err) + } else if len(listRG.Data) > 0 { id := listRG.Data[0].ID bytes, err = client.CloudAPI().RG().GetRaw(context.Background(), rg.GetRequest{RGID: id}) if err != nil { @@ -369,8 +395,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("Storage policy list", bytes, stpolicy.ListStoragePolicies{}, t) // Get - listStoragePolicies, _ := client.CloudAPI().StPolicy().List(context.Background(), stpolicy.ListRequest{}) - if len(listStoragePolicies.Data) > 0 { + listStoragePolicies, err := client.CloudAPI().StPolicy().List(context.Background(), stpolicy.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.StPolicy.List failed: %v", err) + } else if len(listStoragePolicies.Data) > 0 { id := listStoragePolicies.Data[0].ID bytes, err = client.CloudAPI().StPolicy().GetRaw(context.Background(), stpolicy.GetRequest{StoragePolicyID: id}) if err != nil { @@ -389,8 +417,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("Replication profile list", bytes, stpolicy.ListReplicationProfiles{}, t) // Get - listReplicationProfiles, _ := client.CloudAPI().StPolicy().ListReplicationProfile(context.Background(), stpolicy.ListReplicationProfileRequest{}) - if len(listReplicationProfiles.Data) > 0 { + listReplicationProfiles, err := client.CloudAPI().StPolicy().ListReplicationProfile(context.Background(), stpolicy.ListReplicationProfileRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.StPolicy.ListReplicationProfile failed: %v", err) + } else if len(listReplicationProfiles.Data) > 0 { id := listReplicationProfiles.Data[0].ID bytes, err = client.CloudAPI().StPolicy().GetReplicationProfileRaw(context.Background(), stpolicy.GetReplicationProfileRequest{ProfileID: id}) if err != nil { @@ -409,8 +439,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("Security group list", bytes, secgroup.ListSecurityGroups{}, t) // Get - listSecurityGroups, _ := client.CloudAPI().SecurityGroup().List(context.Background(), secgroup.ListRequest{}) - if len(listSecurityGroups.Data) > 0 { + listSecurityGroups, err := client.CloudAPI().SecurityGroup().List(context.Background(), secgroup.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.SecurityGroup.List failed: %v", err) + } else if len(listSecurityGroups.Data) > 0 { id := listSecurityGroups.Data[0].ID bytes, err = client.CloudAPI().SecurityGroup().GetRaw(context.Background(), secgroup.GetRequest{SecurityGroupID: id}) if err != nil { @@ -429,8 +461,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("Tasks list", bytes, tasks.ListTasks{}, t) // Get - listTasks, _ := client.CloudAPI().Tasks().List(context.Background(), tasks.ListRequest{}) - if len(listTasks.Data) > 0 { + listTasks, err := client.CloudAPI().Tasks().List(context.Background(), tasks.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.Tasks.List failed: %v", err) + } else if len(listTasks.Data) > 0 { id := listTasks.Data[0].AuditID bytes, err = client.CloudAPI().Tasks().GetRaw(context.Background(), tasks.GetRequest{AuditID: id}) if err != nil { @@ -449,8 +483,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("Trunk list", bytes, trunk.ListTrunks{}, t) // Get - listTrunk, _ := client.CloudAPI().Trunk().List(context.Background(), trunk.ListRequest{}) - if len(listTrunk.Data) > 0 { + listTrunk, err := client.CloudAPI().Trunk().List(context.Background(), trunk.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.Trunk.List failed: %v", err) + } else if len(listTrunk.Data) > 0 { id := listTrunk.Data[0].ID bytes, err = client.CloudAPI().Trunk().GetRaw(context.Background(), trunk.GetRequest{TrunkID: id}) if err != nil { @@ -469,8 +505,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("VINS list", bytes, vins.ListVINS{}, t) // Get - listVINS, _ := client.CloudAPI().VINS().List(context.Background(), vins.ListRequest{}) - if len(listVINS.Data) > 0 { + listVINS, err := client.CloudAPI().VINS().List(context.Background(), vins.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.VINS.List failed: %v", err) + } else if len(listVINS.Data) > 0 { id := listVINS.Data[0].ID bytes, err = client.CloudAPI().VINS().GetRaw(context.Background(), vins.GetRequest{VINSID: id}) if err != nil { @@ -481,24 +519,31 @@ func TestGetListCloudAPI(t *testing.T) { t.Errorf("Can not test VINS get because VINS list is empty") } - // VM folder - // List - bytes, err = client.CloudAPI().VMFolder().ListRaw(context.Background(), vmfolder.ListRequest{}) - if err != nil { - t.Error(err) - } - getResult("VM folder list", bytes, vmfolder.ListVMFolder{}, t) - // Get - listVMFolders, _ := client.CloudAPI().VMFolder().List(context.Background(), vmfolder.ListRequest{}) - if len(listVMFolders.Data) > 0 { - id := listVMFolders.Data[0].ID - bytes, err = client.CloudAPI().VMFolder().GetRaw(context.Background(), vmfolder.GetRequest{FolderID: id}) + // VM folder (list requires rg_id) + if listRG == nil || len(listRG.Data) == 0 { + t.Errorf("Can not test VM folder because VM folder list is empty") + } else { + rgID := listRG.Data[0].ID + // List + bytes, err = client.CloudAPI().VMFolder().ListRaw(context.Background(), vmfolder.ListRequest{RGID: rgID}) if err != nil { t.Error(err) } - getResult("VM folder get", bytes, vmfolder.RecordVMFolder{}, t) - } else { - t.Errorf("Can not test VM folder get because VM folder list is empty") + getResult("VM folder list", bytes, vmfolder.ListVMFolder{}, t) + // Get + listVMFolders, err := client.CloudAPI().VMFolder().List(context.Background(), vmfolder.ListRequest{RGID: rgID}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.VMFolder.List failed: %v", err) + } else if len(listVMFolders.Data) > 0 { + id := listVMFolders.Data[0].ID + bytes, err = client.CloudAPI().VMFolder().GetRaw(context.Background(), vmfolder.GetRequest{FolderID: id}) + if err != nil { + t.Error(err) + } + getResult("VM folder get", bytes, vmfolder.RecordVMFolder{}, t) + } else { + t.Errorf("Can not test VM folder get because VM folder list is empty") + } } // VGPU @@ -517,8 +562,10 @@ func TestGetListCloudAPI(t *testing.T) { } getResult("Zone list", bytes, zone.ListZones{}, t) // Get - ListZones, _ := client.CloudAPI().Zone().List(context.Background(), zone.ListRequest{}) - if len(ListZones.Data) > 0 { + ListZones, err := client.CloudAPI().Zone().List(context.Background(), zone.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudAPI.Zone.List failed: %v", err) + } else if len(ListZones.Data) > 0 { id := ListZones.Data[0].ID bytes, err = client.CloudAPI().Zone().GetRaw(context.Background(), zone.GetRequest{ID: id}) if err != nil { @@ -549,8 +596,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("Account list", bytes, account_cb.ListAccounts{}, t) // Get - listAcc, _ := client.CloudBroker().Account().List(context.Background(), account_cb.ListRequest{}) - if len(listAcc.Data) > 0 { + listAcc, err := client.CloudBroker().Account().List(context.Background(), account_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.Account.List failed: %v", err) + } else if len(listAcc.Data) > 0 { id := listAcc.Data[0].ID bytes, err = client.CloudBroker().Account().GetRaw(context.Background(), account_cb.GetRequest{AccountID: id}) if err != nil { @@ -569,8 +618,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("Audit list", bytes, audit_cb.ListAudits{}, t) // Get - listAudits, _ := client.CloudBroker().Audit().List(context.Background(), audit_cb.ListRequest{}) - if len(listAudits.Data) > 0 { + listAudits, err := client.CloudBroker().Audit().List(context.Background(), audit_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.Audit.List failed: %v", err) + } else if len(listAudits.Data) > 0 { id := listAudits.Data[0].GUID bytes, err = client.CloudBroker().Audit().GetRaw(context.Background(), audit_cb.GetRequest{AuditGuid: id}) if err != nil { @@ -589,8 +640,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("Bservice list", bytes, bservice_cb.ListBasicServices{}, t) // Get - listBServ, _ := client.CloudBroker().BService().List(context.Background(), bservice_cb.ListRequest{}) - if len(listBServ.Data) > 0 { + listBServ, err := client.CloudBroker().BService().List(context.Background(), bservice_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.BService.List failed: %v", err) + } else if len(listBServ.Data) > 0 { id := listBServ.Data[0].ID bytes, err = client.CloudBroker().BService().GetRaw(context.Background(), bservice_cb.GetRequest{ServiceID: id}) if err != nil { @@ -609,8 +662,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("Compute list", bytes, compute_cb.ListComputes{}, t) // Get - listComp, _ := client.CloudBroker().Compute().List(context.Background(), compute_cb.ListRequest{}) - if len(listComp.Data) > 0 { + listComp, err := client.CloudBroker().Compute().List(context.Background(), compute_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.Compute.List failed: %v", err) + } else if len(listComp.Data) > 0 { id := listComp.Data[0].ID bytes, err = client.CloudBroker().Compute().GetRaw(context.Background(), compute_cb.GetRequest{ComputeID: id}) if err != nil { @@ -635,8 +690,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("Disk list", bytes, disks_cb.ListDisks{}, t) // Get - listDisk, _ := client.CloudBroker().Disks().List(context.Background(), disks_cb.ListRequest{}) - if len(listDisk.Data) > 0 { + listDisk, err := client.CloudBroker().Disks().List(context.Background(), disks_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.Disks.List failed: %v", err) + } else if len(listDisk.Data) > 0 { id := listDisk.Data[0].ID bytes, err = client.CloudBroker().Disks().GetRaw(context.Background(), disks_cb.GetRequest{DiskID: id}) if err != nil { @@ -655,8 +712,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("ExtNet list", bytes, extnet_cb.ListExtNet{}, t) // Get - listExtNet, _ := client.CloudBroker().ExtNet().List(context.Background(), extnet_cb.ListRequest{}) - if len(listExtNet.Data) > 0 { + listExtNet, err := client.CloudBroker().ExtNet().List(context.Background(), extnet_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.ExtNet.List failed: %v", err) + } else if len(listExtNet.Data) > 0 { id := listExtNet.Data[0].ID bytes, err = client.CloudBroker().ExtNet().GetRaw(context.Background(), extnet_cb.GetRequest{NetID: id}) if err != nil { @@ -675,8 +734,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("FLIPGroup list", bytes, flipgroup_cb.ListFLIPGroups{}, t) // Get - listFG, _ := client.CloudBroker().FLIPGroup().List(context.Background(), flipgroup_cb.ListRequest{}) - if len(listFG.Data) > 0 { + listFG, err := client.CloudBroker().FLIPGroup().List(context.Background(), flipgroup_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.FLIPGroup.List failed: %v", err) + } else if len(listFG.Data) > 0 { id := listFG.Data[0].ID bytes, err = client.CloudBroker().FLIPGroup().GetRaw(context.Background(), flipgroup_cb.GetRequest{FLIPGroupID: id}) if err != nil { @@ -695,8 +756,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("Grid list", bytes, grid_cb.ListGrids{}, t) // Get - listGrid, _ := client.CloudBroker().Grid().List(context.Background(), grid_cb.ListRequest{}) - if len(listGrid.Data) > 0 { + listGrid, err := client.CloudBroker().Grid().List(context.Background(), grid_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.Grid.List failed: %v", err) + } else if len(listGrid.Data) > 0 { id := listGrid.Data[0].ID bytes, err = client.CloudBroker().Grid().GetRaw(context.Background(), grid_cb.GetRequest{GID: id}) if err != nil { @@ -715,8 +778,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("Image list", bytes, image_cb.ListImages{}, t) // Get - listImg, _ := client.CloudBroker().Image().List(context.Background(), image_cb.ListRequest{}) - if len(listImg.Data) > 0 { + listImg, err := client.CloudBroker().Image().List(context.Background(), image_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.Image.List failed: %v", err) + } else if len(listImg.Data) > 0 { id := listImg.Data[0].ID bytes, err = client.CloudBroker().Image().GetRaw(context.Background(), image_cb.GetRequest{ImageID: id}) if err != nil { @@ -735,8 +800,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("K8CI list", bytes, k8ci_cb.ListK8CI{}, t) // Get - listk8ci, _ := client.CloudBroker().K8CI().List(context.Background(), k8ci_cb.ListRequest{}) - if len(listk8ci.Data) > 0 { + listk8ci, err := client.CloudBroker().K8CI().List(context.Background(), k8ci_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.K8CI.List failed: %v", err) + } else if len(listk8ci.Data) > 0 { id := listk8ci.Data[0].ID bytes, err = client.CloudBroker().K8CI().GetRaw(context.Background(), k8ci_cb.GetRequest{K8CIID: id}) if err != nil { @@ -755,8 +822,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("K8S list", bytes, k8s_cb.ListK8S{}, t) // Get - listk8s, _ := client.CloudBroker().K8S().List(context.Background(), k8s_cb.ListRequest{}) - if len(listk8s.Data) > 0 { + listk8s, err := client.CloudBroker().K8S().List(context.Background(), k8s_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.K8S.List failed: %v", err) + } else if len(listk8s.Data) > 0 { id := listk8s.Data[0].ID bytes, err = client.CloudBroker().K8S().GetRaw(context.Background(), k8s_cb.GetRequest{K8SID: id}) if err != nil { @@ -775,8 +844,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("LB list", bytes, lb_cb.ListLB{}, t) // Get - listLB, _ := client.CloudBroker().LB().List(context.Background(), lb_cb.ListRequest{}) - if len(listLB.Data) > 0 { + listLB, err := client.CloudBroker().LB().List(context.Background(), lb_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.LB.List failed: %v", err) + } else if len(listLB.Data) > 0 { id := listLB.Data[0].ID bytes, err = client.CloudBroker().LB().GetRaw(context.Background(), lb_cb.GetRequest{LBID: id}) if err != nil { @@ -795,8 +866,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("Node list", bytes, node_cb.ListNodes{}, t) // Get - listNode, _ := client.CloudBroker().Node().List(context.Background(), node_cb.ListRequest{}) - if listNode != nil && len(listNode.Data) > 0 { + listNode, err := client.CloudBroker().Node().List(context.Background(), node_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.Node.List failed: %v", err) + } else if listNode != nil && len(listNode.Data) > 0 { id := listNode.Data[0].ID bytes, err = client.CloudBroker().Node().GetRaw(context.Background(), node_cb.GetRequest{NID: id}) if err != nil { @@ -841,8 +914,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("QoS policy list", bytes, qospolicy_cb.ListQoSPolicies{}, t) // Get - listQosPolicies, _ := client.CloudBroker().QoSPolicy().List(context.Background(), qospolicy_cb.ListRequest{}) - if len(listQosPolicies.Data) > 0 { + listQosPolicies, err := client.CloudBroker().QoSPolicy().List(context.Background(), qospolicy_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.QoSPolicy.List failed: %v", err) + } else if len(listQosPolicies.Data) > 0 { id := listQosPolicies.Data[0].ID bytes, err = client.CloudBroker().QoSPolicy().GetRaw(context.Background(), qospolicy_cb.GetRequest{QoSPolicyID: id}) if err != nil { @@ -861,8 +936,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("RG list", bytes, rg_cb.ListRG{}, t) // Get - listRG, _ := client.CloudBroker().RG().List(context.Background(), rg_cb.ListRequest{}) - if len(listRG.Data) > 0 { + listRG, err := client.CloudBroker().RG().List(context.Background(), rg_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.RG.List failed: %v", err) + } else if len(listRG.Data) > 0 { id := listRG.Data[0].ID bytes, err = client.CloudBroker().RG().GetRaw(context.Background(), rg_cb.GetRequest{RGID: id}) if err != nil { @@ -881,8 +958,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("Security group list", bytes, secgroup_cb.ListSecurityGroups{}, t) // Get - listSecurityGroups, _ := client.CloudBroker().SecurityGroup().List(context.Background(), secgroup_cb.ListRequest{}) - if len(listSecurityGroups.Data) > 0 { + listSecurityGroups, err := client.CloudBroker().SecurityGroup().List(context.Background(), secgroup_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.SecurityGroup.List failed: %v", err) + } else if len(listSecurityGroups.Data) > 0 { id := listSecurityGroups.Data[0].ID bytes, err = client.CloudBroker().SecurityGroup().GetRaw(context.Background(), secgroup_cb.GetRequest{SecurityGroupID: id}) if err != nil { @@ -901,8 +980,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("SEP list", bytes, sep_cb.ListSEP{}, t) // Get - listSEP, _ := client.CloudBroker().SEP().List(context.Background(), sep_cb.ListRequest{}) - if len(listSEP.Data) > 0 { + listSEP, err := client.CloudBroker().SEP().List(context.Background(), sep_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.SEP.List failed: %v", err) + } else if len(listSEP.Data) > 0 { id := listSEP.Data[0].ID bytes, err = client.CloudBroker().SEP().GetRaw(context.Background(), sep_cb.GetRequest{SEPID: id}) if err != nil { @@ -927,8 +1008,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("Storage policy list", bytes, stpolicy_cb.ListStoragePolicies{}, t) // Get - listStoragePolicies, _ := client.CloudBroker().StPolicy().List(context.Background(), stpolicy_cb.ListRequest{}) - if len(listStoragePolicies.Data) > 0 { + listStoragePolicies, err := client.CloudBroker().StPolicy().List(context.Background(), stpolicy_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.StPolicy.List failed: %v", err) + } else if len(listStoragePolicies.Data) > 0 { id := listStoragePolicies.Data[0].ID bytes, err = client.CloudBroker().StPolicy().GetRaw(context.Background(), stpolicy_cb.GetRequest{StoragePolicyID: id}) if err != nil { @@ -947,8 +1030,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("Replication profile list", bytes, stpolicy_cb.ListReplicationProfiles{}, t) // Get - listReplicationProfiles, _ := client.CloudBroker().StPolicy().ListReplicationProfile(context.Background(), stpolicy_cb.ListReplicationProfileRequest{}) - if len(listReplicationProfiles.Data) > 0 { + listReplicationProfiles, err := client.CloudBroker().StPolicy().ListReplicationProfile(context.Background(), stpolicy_cb.ListReplicationProfileRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.StPolicy.ListReplicationProfile failed: %v", err) + } else if len(listReplicationProfiles.Data) > 0 { id := listReplicationProfiles.Data[0].ID bytes, err = client.CloudBroker().StPolicy().GetReplicationProfileRaw(context.Background(), stpolicy_cb.GetReplicationProfileRequest{ProfileID: id}) if err != nil { @@ -975,8 +1060,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("Trunk list", bytes, trunk_cb.ListTrunks{}, t) // Get - listTrunks, _ := client.CloudBroker().Trunk().List(context.Background(), trunk_cb.ListRequest{}) - if len(listTrunks.Data) > 0 { + listTrunks, err := client.CloudBroker().Trunk().List(context.Background(), trunk_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.Trunk.List failed: %v", err) + } else if len(listTrunks.Data) > 0 { id := listTrunks.Data[0].ID bytes, err = client.CloudBroker().Trunk().GetRaw(context.Background(), trunk_cb.GetRequest{TrunkID: id}) if err != nil { @@ -995,8 +1082,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("VINS list", bytes, vins_cb.ListVINS{}, t) // Get - listVINS, _ := client.CloudBroker().VINS().List(context.Background(), vins_cb.ListRequest{}) - if len(listVINS.Data) > 0 { + listVINS, err := client.CloudBroker().VINS().List(context.Background(), vins_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.VINS.List failed: %v", err) + } else if len(listVINS.Data) > 0 { id := listVINS.Data[0].ID bytes, err = client.CloudBroker().VINS().GetRaw(context.Background(), vins_cb.GetRequest{VINSID: id}) if err != nil { @@ -1007,24 +1096,31 @@ func TestGetListCloudbroker(t *testing.T) { t.Errorf("Can not test VINS get because VINS list is empty") } - // VM folder - // List - bytes, err = client.CloudBroker().VMFolder().ListRaw(context.Background(), vmfolder_cb.ListRequest{}) - if err != nil { - t.Error(err) - } - getResult("VM folder list", bytes, vmfolder_cb.ListVMFolder{}, t) - // Get - listVMFolders, _ := client.CloudBroker().VMFolder().List(context.Background(), vmfolder_cb.ListRequest{}) - if len(listVMFolders.Data) > 0 { - id := listVMFolders.Data[0].ID - bytes, err = client.CloudBroker().VMFolder().GetRaw(context.Background(), vmfolder_cb.GetRequest{FolderID: id}) + // VM folder (list requires rg_id) + if listRG == nil || len(listRG.Data) == 0 { + t.Errorf("Can not test VM folder because VM folder list is empty") + } else { + rgID := listRG.Data[0].ID + // List + bytes, err = client.CloudBroker().VMFolder().ListRaw(context.Background(), vmfolder_cb.ListRequest{RGID: rgID}) if err != nil { t.Error(err) } - getResult("VM folder get", bytes, vmfolder_cb.RecordVMFolder{}, t) - } else { - t.Errorf("Can not test VM folder get because VM folder list is empty") + getResult("VM folder list", bytes, vmfolder_cb.ListVMFolder{}, t) + // Get + listVMFolders, err := client.CloudBroker().VMFolder().List(context.Background(), vmfolder_cb.ListRequest{RGID: rgID}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.VMFolder.List failed: %v", err) + } else if len(listVMFolders.Data) > 0 { + id := listVMFolders.Data[0].ID + bytes, err = client.CloudBroker().VMFolder().GetRaw(context.Background(), vmfolder_cb.GetRequest{FolderID: id}) + if err != nil { + t.Error(err) + } + getResult("VM folder get", bytes, vmfolder_cb.RecordVMFolder{}, t) + } else { + t.Errorf("Can not test VM folder get because VM folder list is empty") + } } // Zone @@ -1035,8 +1131,10 @@ func TestGetListCloudbroker(t *testing.T) { } getResult("Zone list", bytes, zone.ListZones{}, t) // Get - ListZones, _ := client.CloudBroker().Zone().List(context.Background(), zone_cb.ListRequest{}) - if len(ListZones.Data) > 0 { + ListZones, err := client.CloudBroker().Zone().List(context.Background(), zone_cb.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because CloudBroker.Zone.List failed: %v", err) + } else if len(ListZones.Data) > 0 { id := ListZones.Data[0].ID bytes, err = client.CloudBroker().Zone().GetRaw(context.Background(), zone_cb.GetRequest{ID: id}) if err != nil { @@ -1082,8 +1180,10 @@ func TestGetListSDNAPI(t *testing.T) { // Users AccessGroups // list - listGroups, _ := client.SDN().AccessGroups().List(context.Background(), acsgroups.ListGroupsRequest{}) - if len(listGroups.AccessGroups) > 0 { + listGroups, err := client.SDN().AccessGroups().List(context.Background(), acsgroups.ListGroupsRequest{}) + if err != nil { + t.Errorf("Can not test get because SDN.AccessGroups.List failed: %v", err) + } else if len(listGroups.AccessGroups) > 0 { id := listGroups.AccessGroups[0].ID bytes, err = client.SDN().AccessGroups().UserListRaw(context.Background(), acsgroups.UsersListRequest{AccessGroupID: id}) if err != nil { @@ -1111,8 +1211,10 @@ func TestGetListSDNAPI(t *testing.T) { } getResult("Address pools list", bytes, adrspools.AddressPoolsList{}, t) // Get - listStoragePolicies, _ := client.SDN().AddressPools().List(context.Background(), adrspools.ListAddressPoolsRequest{}) - if len(listStoragePolicies.Pools) > 0 { + listStoragePolicies, err := client.SDN().AddressPools().List(context.Background(), adrspools.ListAddressPoolsRequest{}) + if err != nil { + t.Errorf("Can not test get because SDN.AddressPools.List failed: %v", err) + } else if len(listStoragePolicies.Pools) > 0 { id := listStoragePolicies.Pools[0].ID bytes, err = client.SDN().AddressPools().GetRaw(context.Background(), adrspools.GetRequest{ID: id}) if err != nil { @@ -1131,8 +1233,10 @@ func TestGetListSDNAPI(t *testing.T) { } getResult("Security policies list", bytes, secpolicies.SecurityPolicyList{}, t) // Get - listPolicies, _ := client.SDN().SecurityPolicies().List(context.Background(), secpolicies.ListRequest{}) - if len(listPolicies) > 0 { + listPolicies, err := client.SDN().SecurityPolicies().List(context.Background(), secpolicies.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because SDN.SecurityPolicies.List failed: %v", err) + } else if len(listPolicies) > 0 { id := listPolicies[0].ID bytes, err = client.SDN().SecurityPolicies().GetRaw(context.Background(), secpolicies.GetRequest{ID: id}) if err != nil { @@ -1150,8 +1254,10 @@ func TestGetListSDNAPI(t *testing.T) { } getResult("Security policy rule list", bytes, rule.SecurityRulesList{}, t) // Get - listRules, _ := client.SDN().SecurityPolicies().Rule().List(context.Background(), rule.ListRequest{}) - if len(listRules) > 0 { + listRules, err := client.SDN().SecurityPolicies().Rule().List(context.Background(), rule.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because SDN.SecurityPolicies.Rule.List failed: %v", err) + } else if len(listRules) > 0 { id := listRules[0].ID securityPolicyID := listRules[0].SecurityPolicyID bytes, err = client.SDN().SecurityPolicies().Rule().GetRaw(context.Background(), rule.GetRequest{ @@ -1173,8 +1279,10 @@ func TestGetListSDNAPI(t *testing.T) { } getResult("ExtNet list", bytes, extnet_sdn.ListExtNet{}, t) // Get - listExtNet, _ := client.SDN().ExtNet().List(context.Background(), extnet_sdn.ListRequest{}) - if len(listExtNet) > 0 { + listExtNet, err := client.SDN().ExtNet().List(context.Background(), extnet_sdn.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because SDN.ExtNet.List failed: %v", err) + } else if len(listExtNet) > 0 { id := listExtNet[0].ID bytes, err = client.SDN().ExtNet().GetRaw(context.Background(), extnet_sdn.GetRequest{ExtNetID: id}) if err != nil { @@ -1193,8 +1301,10 @@ func TestGetListSDNAPI(t *testing.T) { } getResult("NetObjGroup list", bytes, netobjgroups.NetObjGroupList{}, t) // Get - listNetObjGroups, _ := client.SDN().NetworkObjectGroups().List(context.Background(), netobjgroups.ListRequest{}) - if len(listNetObjGroups.Objects) > 0 { + listNetObjGroups, err := client.SDN().NetworkObjectGroups().List(context.Background(), netobjgroups.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because SDN.NetworkObjectGroups.List failed: %v", err) + } else if len(listNetObjGroups.Objects) > 0 { id := listNetObjGroups.Objects[0].ID bytes, err = client.SDN().NetworkObjectGroups().GetRaw(context.Background(), netobjgroups.GetRequest{NetObjGroupID: id}) if err != nil { @@ -1213,8 +1323,10 @@ func TestGetListSDNAPI(t *testing.T) { } getResult("Segments list", bytes, segments.ListSegment{}, t) // Get - listSegments, _ := client.SDN().Segments().List(context.Background(), segments.ListRequest{}) - if len(listSegments) > 0 { + listSegments, err := client.SDN().Segments().List(context.Background(), segments.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because SDN.Segments.List failed: %v", err) + } else if len(listSegments) > 0 { id := listSegments[0].ID bytes, err = client.SDN().Segments().GetRaw(context.Background(), segments.GetRequest{SegmentID: id}) if err != nil { @@ -1233,8 +1345,10 @@ func TestGetListSDNAPI(t *testing.T) { } getResult("Hypervisors list", bytes, hypervisors.HypervisorsList{}, t) // Get - listHypervisors, _ := client.SDN().Hypervisors().List(context.Background(), hypervisors.ListRequest{}) - if len(listHypervisors) > 0 { + listHypervisors, err := client.SDN().Hypervisors().List(context.Background(), hypervisors.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because SDN.Hypervisors.List failed: %v", err) + } else if len(listHypervisors) > 0 { id := listHypervisors[0].Name bytes, err = client.SDN().Hypervisors().GetRaw(context.Background(), hypervisors.GetRequest{Name: id}) if err != nil { @@ -1253,8 +1367,10 @@ func TestGetListSDNAPI(t *testing.T) { } getResult("FloatingIPs list", bytes, flips.FloatingIPsList{}, t) // Get - listFloatingIPs, _ := client.SDN().FloatingIPs().List(context.Background(), flips.ListRequest{}) - if len(listFloatingIPs.Objects) > 0 { + listFloatingIPs, err := client.SDN().FloatingIPs().List(context.Background(), flips.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because SDN.FloatingIPs.List failed: %v", err) + } else if len(listFloatingIPs.Objects) > 0 { id := listFloatingIPs.Objects[0].ID bytes, err = client.SDN().FloatingIPs().GetRaw(context.Background(), flips.GetRequest{FloatingIPID: id}) if err != nil { @@ -1273,8 +1389,10 @@ func TestGetListSDNAPI(t *testing.T) { } getResult("Logical ports list", bytes, logicalports.LogicalPortsList{}, t) // Get - listLP, _ := client.SDN().LogicalPorts().List(context.Background(), logicalports.ListRequest{}) - if len(listLP.Ports) > 0 { + listLP, err := client.SDN().LogicalPorts().List(context.Background(), logicalports.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because SDN.LogicalPorts.List failed: %v", err) + } else if len(listLP.Ports) > 0 { id := listStoragePolicies.Pools[0].ID bytes, err = client.SDN().LogicalPorts().GetRaw(context.Background(), logicalports.GetRequest{ID: id}) if err != nil { @@ -1293,8 +1411,10 @@ func TestGetListSDNAPI(t *testing.T) { } getResult("Routers list", bytes, routers.RoutersList{}, t) // Get - routerList, _ := client.SDN().Routers().List(context.Background(), routers.ListRequest{}) - if len(routerList) > 0 { + routerList, err := client.SDN().Routers().List(context.Background(), routers.ListRequest{}) + if err != nil { + t.Errorf("Can not test get because SDN.Routers.List failed: %v", err) + } else if len(routerList) > 0 { id := routerList[0].ID bytes, err = client.SDN().Routers().GetRaw(context.Background(), routers.GetRequest{ID: id}) if err != nil { diff --git a/tests/platform_upgrade/request_map.go b/tests/platform_upgrade/request_map.go index 14a45af..f59e25d 100644 --- a/tests/platform_upgrade/request_map.go +++ b/tests/platform_upgrade/request_map.go @@ -694,6 +694,12 @@ func getRequestsMapCloudbroker() map[string]interface{} { "/restmachine/cloudbroker/compute/migrate_abort": compute_cb.MigrateAbortRequest{}, "/restmachine/cloudbroker/compute/start_migration_out": compute_cb.StartMigrationOutRequest{}, "/restmachine/cloudbroker/compute/stop_migration_out": compute_cb.StopMigrationOutRequest{}, + "/restmachine/cloudbroker/compute/migrate_v2_resolve": compute_cb.MigrateV2ResolveRequest{}, + "/restmachine/cloudbroker/compute/migrate_v2_list": compute_cb.MigrateV2ListRequest{}, + "/restmachine/cloudbroker/compute/migrate_v2_get": compute_cb.MigrateV2GetRequest{}, + "/restmachine/cloudbroker/compute/migrate_v2_abort": compute_cb.MigrateV2AbortRequest{}, + "/restmachine/cloudbroker/compute/migrate_v2_cleanup": compute_cb.MigrateV2CleanupRequest{}, + "/restmachine/cloudbroker/compute/migrate_v2": compute_cb.MigrateV2Request{}, // disks "/restmachine/cloudbroker/disks/change_disk_storage_policy": disks_cb.ChangeDiskStoragePolicyRequest{}, @@ -1173,16 +1179,6 @@ func getRequestsMapCloudbroker() map[string]interface{} { "/restmachine/cloudbroker/zone/test_cpu_alignment_profile": zone_cb.TestCPUAlignmentProfileRequest{}, "/restmachine/cloudbroker/zone/set_resources_limit": zone_cb.SetResourcesLimitRequest{}, "/restmachine/cloudbroker/zone/update_cpu_alignment_profile": zone_cb.UpdateCPUAlignmentProfileRequest{}, - - // security policy - "/sdn/security_policy/create": secpolicies.CreateRequest{}, - "/sdn/security_policy/delete": secpolicies.DeleteRequest{}, - "/sdn/security_policy/get": secpolicies.GetRequest{}, - "/sdn/security_policy/list": secpolicies.ListRequest{}, - "/sdn/security_policy/move": secpolicies.MoveRequest{}, - "/sdn/security_policy/update": secpolicies.UpdateRequest{}, - "/sdn/security_policy/rule/get": rule.GetRequest{}, - "/sdn/security_policy/rule/list": rule.ListRequest{}, } } @@ -1211,6 +1207,16 @@ func getRequestsMapSDN() map[string]interface{} { "/restmachine/sdn/default_security_policy/list": defsecpolicies.ListRequest{}, "/restmachine/sdn/default_security_policy/update": defsecpolicies.UpdateRequest{}, + // security policy + "/restmachine/sdn/security_policy/create": secpolicies.CreateRequest{}, + "/restmachine/sdn/security_policy/delete": secpolicies.DeleteRequest{}, + "/restmachine/sdn/security_policy/get": secpolicies.GetRequest{}, + "/restmachine/sdn/security_policy/list": secpolicies.ListRequest{}, + "/restmachine/sdn/security_policy/move": secpolicies.MoveRequest{}, + "/restmachine/sdn/security_policy/update": secpolicies.UpdateRequest{}, + "/restmachine/sdn/security_policy/rule/get": rule.GetRequest{}, + "/restmachine/sdn/security_policy/rule/list": rule.ListRequest{}, + // extnet "/restmachine/sdn/external_network/port_add": extnet_sdn.PortAddRequest{}, "/restmachine/sdn/external_network/port_update": extnet_sdn.PortUpdateRequest{}, diff --git a/tests/platform_upgrade/sdk_calls.go b/tests/platform_upgrade/sdk_calls.go new file mode 100644 index 0000000..9895111 --- /dev/null +++ b/tests/platform_upgrade/sdk_calls.go @@ -0,0 +1,446 @@ +package test + +import ( + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "reflect" + "strconv" + "strings" + "sync" +) + +// transport defines which struct tags are used to serialize request on the wire +const ( + transportForm = "form" + transportJSON = "json" +) + +// sentField describes one request parameter exactly as SDK puts it on the wire +type sentField struct { + // Name of the parameter on the wire + Name string + // Name of the go field (for messages) + GoName string + // Go type of the field, nil if it can not be resolved + Type reflect.Type + // Field is not sent when it has zero value + OmitEmpty bool + // Value of `validate` tag + Validate string +} + +// sdkCall describes one DecortApiCall* invocation found in SDK sources +type sdkCall struct { + // Directory of package the call was found in + Dir string + // Name of the type passed as params (may be unexported wrapper), empty for nil params + TypeName string + Transport string + // Name of function the call was found in + FuncName string +} + +// sdkIndex stores parsed SDK packages: struct declarations and API calls by url +type sdkIndex struct { + // dir -> type name -> type expression + types map[string]map[string]ast.Expr + // url (e.g. "/cloudapi/compute/start") -> calls + calls map[string][]sdkCall +} + +var ( + sdkIndexOnce sync.Once + sdkIndexCached *sdkIndex + sdkIndexErr error +) + +// getSDKIndex parses SDK sources in ../../pkg once per test run +func getSDKIndex() (*sdkIndex, error) { + sdkIndexOnce.Do(func() { + sdkIndexCached, sdkIndexErr = buildSDKIndex("../../pkg") + }) + return sdkIndexCached, sdkIndexErr +} + +func buildSDKIndex(root string) (*sdkIndex, error) { + idx := newSDKIndex() + fset := token.NewFileSet() + err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { + return nil + } + file, err := parser.ParseFile(fset, path, nil, 0) + if err != nil { + return err + } + idx.addFile(filepath.Dir(path), file) + return nil + }) + if err != nil { + return nil, err + } + return idx, nil +} + +func newSDKIndex() *sdkIndex { + return &sdkIndex{ + types: make(map[string]map[string]ast.Expr), + calls: make(map[string][]sdkCall), + } +} + +// addFile collects type declarations and API calls of one file +func (idx *sdkIndex) addFile(dir string, file *ast.File) { + if idx.types[dir] == nil { + idx.types[dir] = make(map[string]ast.Expr) + } + + for _, decl := range file.Decls { + switch d := decl.(type) { + case *ast.GenDecl: + for _, spec := range d.Specs { + if ts, ok := spec.(*ast.TypeSpec); ok { + idx.types[dir][ts.Name.Name] = ts.Type + } + } + case *ast.FuncDecl: + if d.Body != nil { + idx.addFuncCalls(dir, d) + } + } + } +} + +// addFuncCalls finds DecortApiCall* invocations in function body and resolves url and type of params +func (idx *sdkIndex) addFuncCalls(dir string, fn *ast.FuncDecl) { + // variable name -> type name + varTypes := make(map[string]string) + for _, field := range fn.Type.Params.List { + for _, name := range field.Names { + varTypes[name.Name] = typeNameOf(field.Type) + } + } + // variable name -> last assigned string literal + strVars := make(map[string]string) + + ast.Inspect(fn.Body, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.AssignStmt: + for i, lhs := range node.Lhs { + ident, ok := lhs.(*ast.Ident) + if !ok || i >= len(node.Rhs) { + continue + } + if s, ok := stringLiteral(node.Rhs[i]); ok { + strVars[ident.Name] = s + } + if name := typeNameOfValue(node.Rhs[i]); name != "" { + varTypes[ident.Name] = name + } + } + case *ast.ValueSpec: + for i, name := range node.Names { + if node.Type != nil { + varTypes[name.Name] = typeNameOf(node.Type) + } + if i < len(node.Values) { + if s, ok := stringLiteral(node.Values[i]); ok { + strVars[name.Name] = s + } + if tn := typeNameOfValue(node.Values[i]); tn != "" { + varTypes[name.Name] = tn + } + } + } + case *ast.CallExpr: + sel, ok := node.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + var urlArg, paramsArg ast.Expr + transport := transportForm + switch sel.Sel.Name { + case "DecortApiCall", "DecortApiCallMP": + if len(node.Args) != 4 { + return true + } + urlArg, paramsArg = node.Args[2], node.Args[3] + case "DecortApiCallCtype": + if len(node.Args) != 5 { + return true + } + urlArg, paramsArg = node.Args[2], node.Args[4] + if ctype, ok := node.Args[3].(*ast.SelectorExpr); ok && ctype.Sel.Name == "MIMEJSON" { + transport = transportJSON + } + default: + return true + } + + url, ok := stringLiteral(urlArg) + if !ok { + ident, isIdent := urlArg.(*ast.Ident) + if !isIdent { + return true + } + if url, ok = strVars[ident.Name]; !ok { + return true + } + } + + call := sdkCall{Dir: dir, Transport: transport, FuncName: fn.Name.Name} + switch p := paramsArg.(type) { + case *ast.Ident: + if p.Name != "nil" { + call.TypeName = varTypes[p.Name] + } + default: + call.TypeName = typeNameOfValue(paramsArg) + } + idx.calls[url] = append(idx.calls[url], call) + } + return true + }) +} + +func stringLiteral(e ast.Expr) (string, bool) { + lit, ok := e.(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + return "", false + } + s, err := strconv.Unquote(lit.Value) + return s, err == nil +} + +// typeNameOfValue returns type name of expressions like T{...} or &T{...} +func typeNameOfValue(e ast.Expr) string { + switch v := e.(type) { + case *ast.CompositeLit: + return typeNameOf(v.Type) + case *ast.UnaryExpr: + return typeNameOfValue(v.X) + } + return "" +} + +// typeNameOf returns name of local named type (T or *T), empty string otherwise +func typeNameOf(e ast.Expr) string { + switch v := e.(type) { + case *ast.Ident: + return v.Name + case *ast.StarExpr: + return typeNameOf(v.X) + } + return "" +} + +// callsForPath returns SDK calls for platform path like "/restmachine/cloudapi/compute/start" +func (idx *sdkIndex) callsForPath(path string) []sdkCall { + return idx.calls[strings.TrimPrefix(path, "/restmachine")] +} + +// sentFields returns flattened list of parameters SDK puts on the wire for the call +func (idx *sdkIndex) sentFields(call sdkCall) ([]sentField, bool) { + if call.TypeName == "" { + return nil, true + } + expr, ok := idx.types[call.Dir][call.TypeName] + if !ok { + return nil, false + } + st, ok := expr.(*ast.StructType) + if !ok { + return nil, false + } + fields := idx.structFields(call.Dir, st, call.Transport, map[string]bool{call.TypeName: true}) + + // serialized wrapper field inherits `validate` of the skipped (url:"-") request field with the same json name + if call.Transport == transportForm { + skipped := idx.skippedFormFields(call.Dir, st, map[string]bool{call.TypeName: true}) + for i := range fields { + if fields[i].Validate != "" { + continue + } + if validate, ok := skipped[strings.TrimSpace(fields[i].Name)]; ok { + fields[i].Validate = validate + } + } + } + + return fields, true +} + +// skippedFormFields returns json name -> validate tag of fields that are not sent in form requests (url:"-") +func (idx *sdkIndex) skippedFormFields(dir string, st *ast.StructType, visited map[string]bool) map[string]string { + result := make(map[string]string) + for _, field := range st.Fields.List { + var tag reflect.StructTag + if field.Tag != nil { + if s, err := strconv.Unquote(field.Tag.Value); err == nil { + tag = reflect.StructTag(s) + } + } + urlTag, hasURLTag := tag.Lookup("url") + + if len(field.Names) == 0 && !hasURLTag { + name := typeNameOf(field.Type) + if embedded, ok := idx.types[dir][name].(*ast.StructType); ok && !visited[name] { + visited[name] = true + for k, v := range idx.skippedFormFields(dir, embedded, visited) { + result[k] = v + } + delete(visited, name) + } + continue + } + + if urlTag != "-" { + continue + } + jsonName := strings.TrimSpace(strings.Split(tag.Get("json"), ",")[0]) + if jsonName != "" && jsonName != "-" { + result[jsonName] = tag.Get("validate") + } + } + return result +} + +func (idx *sdkIndex) structFields(dir string, st *ast.StructType, transport string, visited map[string]bool) []sentField { + var result []sentField + for _, field := range st.Fields.List { + var tag reflect.StructTag + if field.Tag != nil { + if s, err := strconv.Unquote(field.Tag.Value); err == nil { + tag = reflect.StructTag(s) + } + } + wireTag, hasWireTag := tag.Lookup(transportTagKey(transport)) + + // embedded struct without tag is flattened by both encoding/json and go-querystring + if len(field.Names) == 0 && !hasWireTag { + name := typeNameOf(field.Type) + if embedded, ok := idx.types[dir][name].(*ast.StructType); ok && !visited[name] { + visited[name] = true + result = append(result, idx.structFields(dir, embedded, transport, visited)...) + delete(visited, name) + } + continue + } + + goNames := make([]string, 0, len(field.Names)) + for _, n := range field.Names { + goNames = append(goNames, n.Name) + } + if len(goNames) == 0 { + goNames = append(goNames, typeNameOf(field.Type)) + } + + for _, goName := range goNames { + if !ast.IsExported(goName) { + continue + } + name, omitEmpty, skip := parseWireTag(wireTag, goName) + if skip { + continue + } + result = append(result, sentField{ + Name: name, + GoName: goName, + Type: idx.resolveType(dir, field.Type, map[string]bool{}), + OmitEmpty: omitEmpty, + Validate: tag.Get("validate"), + }) + } + } + return result +} + +func transportTagKey(transport string) string { + if transport == transportJSON { + return "json" + } + return "url" +} + +// parseWireTag returns wire name of the field, omitempty flag and whether the field is skipped (tag "-") +func parseWireTag(tag, goName string) (string, bool, bool) { + parts := strings.Split(tag, ",") + // keep tag name as is (including spaces) so that typos in tags are reported + name := parts[0] + if name == "-" && len(parts) == 1 { + return "", false, true + } + if name == "" { + name = goName + } + var omitEmpty bool + for _, opt := range parts[1:] { + if strings.TrimSpace(opt) == "omitempty" { + omitEmpty = true + } + } + return name, omitEmpty, false +} + +var ( + emptyInterfaceType = reflect.TypeOf((*interface{})(nil)).Elem() + structPlaceholder = reflect.TypeOf(struct{}{}) + basicTypes = map[string]reflect.Type{ + "bool": reflect.TypeOf(false), + "string": reflect.TypeOf(""), + "int": reflect.TypeOf(int(0)), + "int8": reflect.TypeOf(int8(0)), + "int16": reflect.TypeOf(int16(0)), + "int32": reflect.TypeOf(int32(0)), + "int64": reflect.TypeOf(int64(0)), + "uint": reflect.TypeOf(uint(0)), + "uint8": reflect.TypeOf(uint8(0)), + "uint16": reflect.TypeOf(uint16(0)), + "uint32": reflect.TypeOf(uint32(0)), + "uint64": reflect.TypeOf(uint64(0)), + "float32": reflect.TypeOf(float32(0)), + "float64": reflect.TypeOf(float64(0)), + "any": emptyInterfaceType, + } +) + +// resolveType converts type expression to reflect.Type for kind comparison (nil for types from other packages) +func (idx *sdkIndex) resolveType(dir string, e ast.Expr, visited map[string]bool) reflect.Type { + switch v := e.(type) { + case *ast.Ident: + if t, ok := basicTypes[v.Name]; ok { + return t + } + if visited[v.Name] { + return nil + } + underlying, ok := idx.types[dir][v.Name] + if !ok { + return nil + } + visited[v.Name] = true + return idx.resolveType(dir, underlying, visited) + case *ast.StarExpr: + return idx.resolveType(dir, v.X, visited) + case *ast.StructType: + return structPlaceholder + case *ast.InterfaceType: + return emptyInterfaceType + case *ast.ArrayType: + elem := idx.resolveType(dir, v.Elt, visited) + if elem == nil { + elem = emptyInterfaceType + } + return reflect.SliceOf(elem) + case *ast.MapType: + return reflect.MapOf(reflect.TypeOf(""), emptyInterfaceType) + } + return nil +} diff --git a/tests/platform_upgrade/utils_requests.go b/tests/platform_upgrade/utils_requests.go index 65ad118..93ca77c 100644 --- a/tests/platform_upgrade/utils_requests.go +++ b/tests/platform_upgrade/utils_requests.go @@ -5,27 +5,72 @@ import ( "io" "os" "reflect" + "sort" "strings" "testing" ) -// extractTypeFromSchema extracts type from schema, handling oneOf cases -// When oneOf contains a type and null, it returns the non-null type +// platformParam describes one request parameter from platform swagger +type platformParam struct { + Name string + // OpenAPI type: integer, string, boolean, number, array, object or empty if unknown + Type string + // OpenAPI type of array items, empty if unknown + Items string + Required bool + // Value of "default" from swagger, nil if not set + Default interface{} +} + +// extractTypeFromSchema extracts non-null type from "type" (string or array), "oneOf"/"anyOf" or "$ref" (object) func extractTypeFromSchema(schema map[string]interface{}) string { - // Check for direct type - if paramType, ok := schema["type"].(string); ok { - return paramType + switch typ := schema["type"].(type) { + case string: + return typ + case []interface{}: + for _, item := range typ { + if s, ok := item.(string); ok && s != "null" { + return s + } + } } - // Check for oneOf - if oneOf, ok := schema["oneOf"].([]interface{}); ok { - for _, item := range oneOf { - if itemMap, ok := item.(map[string]interface{}); ok { - if itemType, ok := itemMap["type"].(string); ok { - // Skip null type, return the first non-null type found - if itemType != "null" { - return itemType - } + for _, key := range []string{"oneOf", "anyOf"} { + variants, ok := schema[key].([]interface{}) + if !ok { + continue + } + for _, variant := range variants { + if variantMap, ok := variant.(map[string]interface{}); ok { + if variantType := extractTypeFromSchema(variantMap); variantType != "" && variantType != "null" { + return variantType + } + } + } + } + + if _, ok := schema["$ref"]; ok { + return "object" + } + + return "" +} + +// extractItemsType extracts type of array items from schema, including arrays declared inside oneOf/anyOf +func extractItemsType(schema map[string]interface{}) string { + if items, ok := schema["items"].(map[string]interface{}); ok { + return extractTypeFromSchema(items) + } + + for _, key := range []string{"oneOf", "anyOf"} { + variants, ok := schema[key].([]interface{}) + if !ok { + continue + } + for _, variant := range variants { + if variantMap, ok := variant.(map[string]interface{}); ok && extractTypeFromSchema(variantMap) == "array" { + if items := extractItemsType(variantMap); items != "" { + return items } } } @@ -34,150 +79,84 @@ func extractTypeFromSchema(schema map[string]interface{}) string { return "" } -func getParameters(input map[string]interface{}) []interface{} { - var emptySlice []interface{} - methods := []string{"get", "post", "put", "delete", "patch", "head", "options"} - - var methodData interface{} - found := false - - for _, method := range methods { - if data, ok := input[method]; ok { - methodData = data - found = true - break +// getParameters returns request parameters of all methods, content types and query/path parameters of a path +func getParameters(input map[string]interface{}) []platformParam { + params := make(map[string]platformParam) + add := func(name string, schema map[string]interface{}, required bool) { + p := platformParam{Name: name, Required: required} + if schema != nil { + p.Type = extractTypeFromSchema(schema) + if p.Type == "array" { + p.Items = extractItemsType(schema) + } + p.Default = schema["default"] } + if prev, ok := params[name]; ok { + p.Required = p.Required || prev.Required + if p.Type == "" { + p.Type, p.Items = prev.Type, prev.Items + } + if p.Default == nil { + p.Default = prev.Default + } + } + params[name] = p } - if !found { - return emptySlice - } + for _, method := range []string{"get", "post", "put", "delete", "patch", "head", "options"} { + operation, ok := input[method].(map[string]interface{}) + if !ok { + continue + } - parameters, ok := methodData.(map[string]interface{}) - if !ok { - return emptySlice - } - - var result []interface{} - - if requestBody, ok := parameters["requestBody"].(map[string]interface{}); ok { - if content, ok := requestBody["content"].(map[string]interface{}); ok { - // Check for application/x-www-form-urlencoded, application/json, or multipart/form-data - var schemaData map[string]interface{} - var found bool - - if formData, ok := content["application/x-www-form-urlencoded"].(map[string]interface{}); ok { - if schema, ok := formData["schema"].(map[string]interface{}); ok { - schemaData = schema - found = true + if requestBody, ok := operation["requestBody"].(map[string]interface{}); ok { + content, _ := requestBody["content"].(map[string]interface{}) + for _, media := range content { + mediaMap, _ := media.(map[string]interface{}) + schema, _ := mediaMap["schema"].(map[string]interface{}) + if schema == nil { + continue } - } else if jsonData, ok := content["application/json"].(map[string]interface{}); ok { - if schema, ok := jsonData["schema"].(map[string]interface{}); ok { - schemaData = schema - found = true + requiredFields := make(map[string]bool) + if req, ok := schema["required"].([]interface{}); ok { + for _, r := range req { + if reqStr, ok := r.(string); ok { + requiredFields[reqStr] = true + } + } } - } else if multipartData, ok := content["multipart/form-data"].(map[string]interface{}); ok { - if schema, ok := multipartData["schema"].(map[string]interface{}); ok { - schemaData = schema - found = true + properties, _ := schema["properties"].(map[string]interface{}) + for name, prop := range properties { + propMap, _ := prop.(map[string]interface{}) + add(name, propMap, requiredFields[name]) } } + } - if found && schemaData != nil { - if properties, ok := schemaData["properties"].(map[string]interface{}); ok { - requiredFields := make(map[string]bool) - if req, ok := schemaData["required"].([]interface{}); ok { - for _, r := range req { - if reqStr, ok := r.(string); ok { - requiredFields[reqStr] = true - } - } - } - for name, prop := range properties { - propMap, ok := prop.(map[string]interface{}) - if !ok { - continue - } - newParam := make(map[string]interface{}) - newParam["name"] = name - propType := extractTypeFromSchema(propMap) - if propType != "" { - newParam["type"] = propType - } - newParam["required"] = requiredFields[name] - if propType == "array" { - // Try to get items from propMap first - if items, ok := propMap["items"].(map[string]interface{}); ok { - newParam["items"] = items - } else if oneOf, ok := propMap["oneOf"].([]interface{}); ok { - // If items not in propMap, try to get from oneOf array element - for _, item := range oneOf { - if itemMap, ok := item.(map[string]interface{}); ok { - if itemType, ok := itemMap["type"].(string); ok && itemType == "array" { - if items, ok := itemMap["items"].(map[string]interface{}); ok { - newParam["items"] = items - break - } - } - } - } - } - } - result = append(result, newParam) - } + if parameters, ok := operation["parameters"].([]interface{}); ok { + for _, p := range parameters { + param, ok := p.(map[string]interface{}) + if !ok { + continue } + name, ok := param["name"].(string) + if !ok { + continue + } + schema, _ := param["schema"].(map[string]interface{}) + required, _ := param["required"].(bool) + add(name, schema, required) } } } - if params, ok := parameters["parameters"].([]interface{}); ok { - for _, p := range params { - param, ok := p.(map[string]interface{}) - if !ok { - continue - } - - newParam := make(map[string]interface{}) - if name, ok := param["name"].(string); ok { - newParam["name"] = name - } - if schema, ok := param["schema"].(map[string]interface{}); ok { - paramType := extractTypeFromSchema(schema) - if paramType != "" { - newParam["type"] = paramType - } - if paramType == "array" { - if items, ok := schema["items"].(map[string]interface{}); ok { - newParam["items"] = items - } else if oneOf, ok := schema["oneOf"].([]interface{}); ok { - for _, item := range oneOf { - if itemMap, ok := item.(map[string]interface{}); ok { - if itemType, ok := itemMap["type"].(string); ok && itemType == "array" { - if items, ok := itemMap["items"].(map[string]interface{}); ok { - newParam["items"] = items - break - } - } - } - } - } - } - } - // Handle required field - if required, ok := param["required"].(bool); ok { - newParam["required"] = required - } else { - newParam["required"] = false - } - result = append(result, newParam) - } + result := make([]platformParam, 0, len(params)) + for _, p := range params { + result = append(result, p) } + sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name }) - if len(result) > 0 { - return result - } - - return emptySlice + return result } func getBytesFromJSON(fileName string, t *testing.T) []byte { @@ -195,6 +174,7 @@ func getBytesFromJSON(fileName string, t *testing.T) []byte { return bytes } +// getErrorsFromJSON compares platform requests from swagger with the values SDK passes to DecortApiCall* func getErrorsFromJSON(bytes []byte, t *testing.T, cloud string) { var requests map[string]interface{} @@ -209,227 +189,272 @@ func getErrorsFromJSON(bytes []byte, t *testing.T, cloud string) { t.Fatalf("Wrong cloud provided, expected `cloudapi`, `cloudbroker` or `sdn`, got %s", cloud) } - var dataLogs []string + index, err := getSDKIndex() + if err != nil { + t.Fatalf("can not parse SDK sources: %v", err) + } paths, err := getMapFromFile(bytes) if err != nil { - t.Error(err) + t.Fatal(err) } - i := 0 - for k, v := range paths { + + requestPaths := make([]string, 0, len(requests)) + for k := range requests { + requestPaths = append(requestPaths, k) + } + sort.Strings(requestPaths) + + var absentOnPlatform, invalidKeys []string + for _, k := range requestPaths { + if !strings.HasPrefix(k, "/restmachine/"+cloud+"/") { + invalidKeys = append(invalidKeys, k) + continue + } // exclude deprecated urls from analysis if !validateUrlFromJson(k) { continue } - - params := getParameters(v.(map[string]interface{})) - structure, ok := requests[k] + v, ok := paths[k] if !ok { + absentOnPlatform = append(absentOnPlatform, k) continue } - i++ - typStruct := reflect.TypeOf(structure) - - var errs []string - // empty request case - if len(params) == 0 && structure == nil { - continue - } - if len(params) != typStruct.NumField() { - errs = append(errs, fmt.Sprintf("Platform (%d) and golang structure (%d) have different amount of fields.", len(params), typStruct.NumField())) - } - paramMap := make(map[string]bool) - paramRequiredMap := make(map[string]bool) - for _, p := range params { - param, ok := p.(map[string]interface{}) - if !ok { - continue - } - name, ok := param["name"].(string) - if ok { - paramMap[name] = true - required, ok := param["required"].(bool) - if ok { - paramRequiredMap[name] = required - } else { - paramRequiredMap[name] = false - } - } - } - - for _, p := range params { - param, ok := p.(map[string]interface{}) - if !ok { - continue - } - - name, ok := param["name"].(string) - if !ok { - name = "" - } - required, ok := param["required"].(bool) - if !ok { - required = false - } - typ, ok := p.(map[string]interface{})["type"].(string) - if !ok { - typ = "" - } - - var items string - if p.(map[string]interface{})["items"] != nil { - itemsTemp := p.(map[string]interface{})["items"] - if itemsTemp != nil { - itemsType := itemsTemp.(map[string]interface{})["type"] - if itemsType != nil { - items, ok = itemsType.(string) - if !ok { - items = "" - } - } - } - } - - var found bool - for i := 0; i < typStruct.NumField(); i++ { - jsonTag := typStruct.Field(i).Tag.Get("json") - validation, _ := typStruct.Field(i).Tag.Lookup("validate") - - if checkName(name, jsonTag) { - if !checkRequired(required, validation, typ) { - errs = append(errs, fmt.Sprintf("Field %s has different required parameters on the platform and in golang structure", name)) - } - if !checkKind(typ, items, typStruct.Field(i).Type) { - errs = append(errs, fmt.Sprintf("Field %s has different type parameters on the platform and in golang structure", name)) - } - found = true - break - } - } - - if !found { - errs = append(errs, fmt.Sprintf("Platform has field %s that golang structure doesn't", name)) - } - } - - // Check if required fields in Go structure are missing from platform JSON - // or if they exist but are not required on platform - for i := 0; i < typStruct.NumField(); i++ { - jsonTag := typStruct.Field(i).Tag.Get("json") - validation, _ := typStruct.Field(i).Tag.Lookup("validate") - - fieldName := strings.Split(jsonTag, ",")[0] - if fieldName == "" || fieldName == "-" { - continue - } - - if strings.Contains(validation, "required") { - if !paramMap[fieldName] { - errs = append(errs, fmt.Sprintf("Golang structure has required field %s that platform doesn't", fieldName)) - } else { - platformRequired := paramRequiredMap[fieldName] - if !platformRequired { - fieldType := typStruct.Field(i).Type - if fieldType.Kind() == reflect.Bool { - errs = append(errs, fmt.Sprintf("Golang structure has required field %s that platform doesn't", fieldName)) - } - } - } - } - } + errs := checkPath(k, getParameters(v.(map[string]interface{})), index) if len(errs) > 0 { - msg := fmt.Sprintf("Path %s has following errors: %v", k, errs) - t.Error(msg) - dataLogs = append(dataLogs, msg) + t.Errorf("Path %s has following errors: %v", k, errs) } } - if len(requests) != i { - msg := fmt.Sprintf("Amount of structure checked (%d) is not the same as amount of platform requests available (%d), please check getRequests func in code.", - i, len(requests)) - t.Error(msg) - dataLogs = append(dataLogs, msg) + if len(invalidKeys) > 0 { + t.Errorf("Request map for %s has keys that don't start with /restmachine/%s/ and are never checked: %v", cloud, cloud, invalidKeys) + } + if len(absentOnPlatform) > 0 { + t.Errorf("Request map has paths that platform doesn't (remove them from request map): %v", absentOnPlatform) } } -// checkName checks if name field from platform has the same value as json tag in golang structure (maybe including omitempty) -func checkName(name, tag string) bool { - return strings.Contains(tag, name) +// requestVariant is one distinct request SDK sends to a path (e.g. sync and async methods send different wrappers) +type requestVariant struct { + FuncName string + Transport string + Fields []sentField } -// checkRequired checks if required field from platform has the same value as validate tag in golang structure -func checkRequired(required bool, validation, fieldType string) bool { - if required && strings.Contains(validation, "required") { - return true - } - if required && (!strings.Contains(validation, "omitempty") && validation != "") { - return true - } - if !required && (validation == "" || strings.Contains(validation, "omitempty")) { - return true +// checkPath compares platform parameters of a path with every distinct request SDK sends to this path +func checkPath(path string, params []platformParam, index *sdkIndex) []string { + calls := index.callsForPath(path) + if len(calls) == 0 { + return []string{"SDK call for this path is not found in sources, can not check it"} } - if fieldType == "boolean" { - return true // otherwise we have issues with setting false/true + type variantKey struct{ dir, typeName, transport string } + seen := make(map[variantKey]bool) + var variants []requestVariant + var errs []string + + for _, call := range calls { + key := variantKey{call.Dir, call.TypeName, call.Transport} + if seen[key] { + continue + } + seen[key] = true + + fields, ok := index.sentFields(call) + if !ok { + errs = append(errs, fmt.Sprintf("can not resolve type %s sent by %s", call.TypeName, call.FuncName)) + continue + } + variants = append(variants, requestVariant{FuncName: call.FuncName, Transport: call.Transport, Fields: fields}) } + return append(errs, compareVariants(params, variants)...) +} + +// compareVariants returns differences between platform parameters and all requests SDK sends to the path +func compareVariants(params []platformParam, variants []requestVariant) []string { + var errs []string + seenErr := make(map[string]bool) + add := func(msg string) { + if !seenErr[msg] { + seenErr[msg] = true + errs = append(errs, msg) + } + } + + byName := make([]map[string]sentField, len(variants)) + for i, v := range variants { + byName[i] = make(map[string]sentField, len(v.Fields)) + for _, f := range v.Fields { + if _, ok := byName[i][f.Name]; ok && v.Transport == transportForm { + add(fmt.Sprintf("Golang structure sends field %s more than once", f.Name)) + } + byName[i][f.Name] = f + } + } + + paramsByName := make(map[string]bool, len(params)) + for _, p := range params { + paramsByName[p.Name] = true + + var sentBy, notSentBy []string + for i, v := range variants { + f, ok := byName[i][p.Name] + if !ok { + notSentBy = append(notSentBy, v.FuncName) + continue + } + sentBy = append(sentBy, v.FuncName) + + if !checkKind(p.Type, p.Items, f, v.Transport) { + add(fmt.Sprintf("Field %s has different type parameters on the platform (%s) and in golang structure (%s)", + p.Name, platformTypeString(p), f.Type)) + } + if msg := checkRequired(p, f); msg != "" { + add(msg) + } + } + + if len(sentBy) == 0 { + add(fmt.Sprintf("Platform has field %s that golang structure doesn't", p.Name)) + continue + } + for _, fn := range notSentBy { + if p.Required { + add(fmt.Sprintf("Required field %s is not sent by %s", p.Name, fn)) + } else if p.Type == "boolean" && p.Default == true { + add(fmt.Sprintf("Field %s is not sent by %s, platform default is true", p.Name, fn)) + } + } + } + + for _, v := range variants { + for _, f := range v.Fields { + if !paramsByName[f.Name] { + add(fmt.Sprintf("Golang structure has field %q (%s) that platform doesn't", f.Name, f.GoName)) + } + } + } + + return errs +} + +func platformTypeString(p platformParam) string { + if p.Type == "array" && p.Items != "" { + return "array of " + p.Items + } + return p.Type +} + +// hasValidateTag checks if `validate` tag contains exactly the given tag +func hasValidateTag(validate, tag string) bool { + for _, v := range strings.Split(validate, ",") { + if strings.TrimSpace(v) == tag { + return true + } + } return false } -// checkKind checks if type field from platform has the same value as field type in golang structure -func checkKind(platformType, items string, typ reflect.Type) bool { - //nolint - switch typ.Kind() { - case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int8, reflect.Int16, reflect.Uint, reflect.Uint64, reflect.Uint16, reflect.Uint32, reflect.Uint8: - if platformType == "integer" { - return true - } - return false +func isIntKind(k reflect.Kind) bool { + switch k { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return true + } + return false +} - case reflect.String: +// checkRequired returns description of required mismatch between platform and golang field or empty string +func checkRequired(p platformParam, f sentField) string { + // validators without omitempty fail on zero value, so such field can not be omitted either + goRequired := hasValidateTag(f.Validate, "required") || + (f.Validate != "" && !hasValidateTag(f.Validate, "omitempty")) + + // numbers and booleans without omitempty are always sent and zero is a meaningful value for them + alwaysSent := false + if f.Type != nil && !f.OmitEmpty { + kind := f.Type.Kind() + alwaysSent = isIntKind(kind) || kind == reflect.Bool || kind == reflect.Float32 || kind == reflect.Float64 + } + + if p.Required && !goRequired && !alwaysSent { + return fmt.Sprintf("Field %s is required on the platform but is optional in golang structure", p.Name) + } + if !p.Required && hasValidateTag(f.Validate, "required") { + return fmt.Sprintf("Field %s is optional on the platform but is required in golang structure", p.Name) + } + return "" +} + +// checkKind checks if type field from platform corresponds to field type in golang structure +func checkKind(platformType, items string, f sentField, transport string) bool { + typ := f.Type + // type can not be resolved on one of the sides + if typ == nil || platformType == "" { + return true + } + for typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + + kind := typ.Kind() + switch { + case isIntKind(kind): + return platformType == "integer" + + case kind == reflect.String: if platformType == "string" { return true } - return false + // json-encoded object or array of objects + return transport == transportForm && + (platformType == "object" || (platformType == "array" && items == "object")) - case reflect.Bool: - if platformType == "boolean" { - return true + case kind == reflect.Bool: + return platformType == "boolean" + + case kind == reflect.Float32 || kind == reflect.Float64: + return platformType == "number" + + case kind == reflect.Interface: + if hasValidateTag(f.Validate, "isBool") { + return platformType == "boolean" } - return false + return true - case reflect.Float32, reflect.Float64: - if platformType == "number" { - return true - } - return false + case kind == reflect.Map || kind == reflect.Struct: + return platformType == "object" - case reflect.Array, reflect.Slice: + case kind == reflect.Array || kind == reflect.Slice: if platformType != "array" { return false } - elem := typ.Elem() - switch elem.Kind() { - case reflect.String: - if items == "string" { - return true - } - return false - - case reflect.Int, reflect.Int32, reflect.Int64, reflect.Int8, reflect.Int16, reflect.Uint, reflect.Uint64, reflect.Uint16, reflect.Uint32, reflect.Uint8: - if items == "integer" { - return true - } - return false - - default: // for cases like dataDisks etc. - return true - + for elem.Kind() == reflect.Ptr { + elem = elem.Elem() + } + // items type is not declared on the platform + if items == "" { + return true + } + switch { + case elem.Kind() == reflect.String: + // list of json-encoded objects + return items == "string" || (transport == transportForm && items == "object") + case isIntKind(elem.Kind()): + return items == "integer" + case elem.Kind() == reflect.Bool: + return items == "boolean" + case elem.Kind() == reflect.Float32 || elem.Kind() == reflect.Float64: + return items == "number" + case elem.Kind() == reflect.Struct || elem.Kind() == reflect.Map: + return items == "object" + default: + return true } - - default: // for cases like drivers etc - return true } + + return true }