767 lines
26 KiB
Go
767 lines
26 KiB
Go
package provider
|
|
|
|
// [НЕ ИЗМЕНЯТЬ !!!!]
|
|
// Данный ресурс реализует паттерн "Nubes Flow" для Virtual Data Center (VDC).
|
|
// ВАЖНО: VDC является фундаментальным ресурсом инфраструктуры.
|
|
//
|
|
// ЛОГИКА УДАЛЕНИЯ (Delete):
|
|
// Автоматическое удаление через API ОТКЛЮЧЕНО для защиты от случайной потери данных.
|
|
// При вызове 'terraform destroy' ресурс просто УДАЛЯЕТСЯ ИЗ СТЕЙТА Terraform, но остается в облаке.
|
|
// Для реального удаления юзер должен вручную перевести инстанс в 'suspend' через UI и дождаться удаления (14 дней).
|
|
//
|
|
// ЛОГИКА ИЗМЕНЕНИЯ (Modify):
|
|
// Если ресурс находится в стейте, выполнение 'terraform apply' вызовет метод Update,
|
|
// который запустит операцию 'modify' для обновления параметров (квот и т.д.).
|
|
// ВНИМАНИЕ: Если вы уже выполнили 'destroy' (удалили из стейта), но ресурс остался в облаке,
|
|
// то для его изменения через Terraform вам придется сначала выполнить 'terraform import'.
|
|
//
|
|
// TODO: Протестировать VDC 'modify' позже.
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
|
|
)
|
|
|
|
var _ resource.Resource = &VDCResource{}
|
|
var _ resource.ResourceWithImportState = &VDCResource{}
|
|
|
|
func NewVDCResource() resource.Resource {
|
|
return &VDCResource{}
|
|
}
|
|
|
|
type VDCResource struct {
|
|
client *NubesClient
|
|
}
|
|
|
|
type VDCResourceModel struct {
|
|
ID types.String `tfsdk:"id"`
|
|
DisplayName types.String `tfsdk:"display_name"`
|
|
Description types.String `tfsdk:"description"`
|
|
OrganizationUID types.String `tfsdk:"organization_uid"`
|
|
ProviderVDC types.String `tfsdk:"provider_vdc"`
|
|
StorageProfiles types.String `tfsdk:"storage_profiles"`
|
|
NetworkPool types.String `tfsdk:"network_pool"`
|
|
CpuAllocationPct types.Int64 `tfsdk:"cpu_allocation_pct"`
|
|
RamAllocationPct types.Int64 `tfsdk:"ram_allocation_pct"`
|
|
CpuQuota types.Int64 `tfsdk:"cpu_quota"`
|
|
RamQuota types.Int64 `tfsdk:"ram_quota"`
|
|
DeletionProtection types.Bool `tfsdk:"deletion_protection"`
|
|
Status types.String `tfsdk:"status"`
|
|
Timeouts timeouts.Value `tfsdk:"timeouts"`
|
|
}
|
|
|
|
func (r *VDCResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
resp.TypeName = req.ProviderTypeName + "_vdc"
|
|
}
|
|
|
|
func (r *VDCResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
|
resp.Schema = schema.Schema{
|
|
MarkdownDescription: "Nubes VDC (Virtual Data Center) resource",
|
|
Blocks: map[string]schema.Block{
|
|
"timeouts": timeouts.Block(ctx, timeouts.Opts{
|
|
Create: true,
|
|
Update: true,
|
|
Delete: true,
|
|
}),
|
|
},
|
|
|
|
Attributes: map[string]schema.Attribute{
|
|
"id": schema.StringAttribute{
|
|
Computed: true,
|
|
MarkdownDescription: "VDC identifier (UUID)",
|
|
PlanModifiers: []planmodifier.String{
|
|
stringplanmodifier.UseStateForUnknown(),
|
|
},
|
|
},
|
|
"display_name": schema.StringAttribute{
|
|
MarkdownDescription: "VDC display name",
|
|
Required: true,
|
|
},
|
|
"description": schema.StringAttribute{
|
|
MarkdownDescription: "VDC description",
|
|
Optional: true,
|
|
},
|
|
"organization_uid": schema.StringAttribute{
|
|
MarkdownDescription: "Organization UUID (parameter ID 30)",
|
|
Required: true,
|
|
},
|
|
"provider_vdc": schema.StringAttribute{
|
|
MarkdownDescription: "Provider VDC name in Cloud Director (parameter ID 335)",
|
|
Required: true,
|
|
},
|
|
"storage_profiles": schema.StringAttribute{
|
|
MarkdownDescription: "Storage profiles JSON array, e.g. [{\"name\":\"vsan\",\"size\":\"100\"}] (parameter ID 361)",
|
|
Required: true,
|
|
},
|
|
"network_pool": schema.StringAttribute{
|
|
MarkdownDescription: "Network Pool name in Cloud Director (parameter ID 366)",
|
|
Required: true,
|
|
},
|
|
"cpu_allocation_pct": schema.Int64Attribute{
|
|
MarkdownDescription: "CPU allocation percentage (parameter ID 397)",
|
|
Required: true,
|
|
},
|
|
"ram_allocation_pct": schema.Int64Attribute{
|
|
MarkdownDescription: "RAM allocation percentage (parameter ID 398)",
|
|
Required: true,
|
|
},
|
|
"cpu_quota": schema.Int64Attribute{
|
|
MarkdownDescription: "CPU quota (parameter ID 557)",
|
|
Optional: true,
|
|
},
|
|
"ram_quota": schema.Int64Attribute{
|
|
MarkdownDescription: "RAM quota (parameter ID 558)",
|
|
Optional: true,
|
|
},
|
|
"deletion_protection": schema.BoolAttribute{
|
|
MarkdownDescription: "If true, the resource will only be removed from Terraform state upon destroy, but will remain in the cloud. If false, destroy will trigger 'suspend' in Nubes.",
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: booldefault.StaticBool(true),
|
|
},
|
|
"status": schema.StringAttribute{
|
|
MarkdownDescription: "Current status of the VDC",
|
|
Computed: true,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (r *VDCResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
|
if req.ProviderData == nil {
|
|
return
|
|
}
|
|
|
|
client, ok := req.ProviderData.(*NubesClient)
|
|
if !ok {
|
|
resp.Diagnostics.AddError(
|
|
"Unexpected Resource Configure Type",
|
|
fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData),
|
|
)
|
|
return
|
|
}
|
|
|
|
r.client = client
|
|
}
|
|
|
|
func (r *VDCResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
|
// [ЛОГИКА VDC FLOW]
|
|
// Ресурс VDC создается по стандартному 7-шаговому алгоритму Nubes.
|
|
// Особое внимание уделяется параметрам квот (CPU/RAM) и сетевым пулам.
|
|
|
|
var data VDCResourceModel
|
|
|
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// Apply timeout
|
|
createTimeout, diags := data.Timeouts.Create(ctx, 3*time.Minute)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, createTimeout)
|
|
defer cancel()
|
|
|
|
// Step 1: Create instance
|
|
createReq := CreateInstanceRequest{
|
|
ServiceId: 21, // VDC service ID
|
|
DisplayName: data.DisplayName.ValueString(),
|
|
Descr: data.Description.ValueString(),
|
|
}
|
|
|
|
jsonData, err := json.Marshal(createReq)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal request: %s", err))
|
|
return
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instances", bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create request: %s", err))
|
|
return
|
|
}
|
|
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
if r.client.ApiToken != "" {
|
|
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
|
|
}
|
|
|
|
httpResp, err := r.client.HttpClient.Do(httpReq)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create instance: %s", err))
|
|
return
|
|
}
|
|
defer httpResp.Body.Close()
|
|
|
|
if httpResp.StatusCode != http.StatusCreated {
|
|
body, _ := io.ReadAll(httpResp.Body)
|
|
resp.Diagnostics.AddError(
|
|
"API Error",
|
|
fmt.Sprintf("Create instance failed with status %d: %s", httpResp.StatusCode, string(body)),
|
|
)
|
|
return
|
|
}
|
|
|
|
location := httpResp.Header.Get("Location")
|
|
if location == "" {
|
|
resp.Diagnostics.AddError("API Error", "No Location header in response")
|
|
return
|
|
}
|
|
instanceId := location[2:]
|
|
|
|
data.ID = types.StringValue(instanceId)
|
|
|
|
// Step 2: Create operation
|
|
// Deck API требует явного создания операции 'create' для инстанса.
|
|
// Это переводит инстанс в состояние "wizard", где можно настраивать параметры.
|
|
operationReq := CreateOperationRequest{
|
|
InstanceUid: instanceId,
|
|
Operation: "create",
|
|
}
|
|
|
|
jsonData, err = json.Marshal(operationReq)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
|
|
return
|
|
}
|
|
|
|
httpReq, err = http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
|
|
return
|
|
}
|
|
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
if r.client.ApiToken != "" {
|
|
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
|
|
}
|
|
|
|
httpResp, err = r.client.HttpClient.Do(httpReq)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation: %s", err))
|
|
return
|
|
}
|
|
defer httpResp.Body.Close()
|
|
|
|
if httpResp.StatusCode != http.StatusCreated {
|
|
body, _ := io.ReadAll(httpResp.Body)
|
|
resp.Diagnostics.AddError(
|
|
"API Error",
|
|
fmt.Sprintf("Create operation failed with status %d: %s", httpResp.StatusCode, string(body)),
|
|
)
|
|
return
|
|
}
|
|
|
|
location = httpResp.Header.Get("Location")
|
|
if location == "" {
|
|
resp.Diagnostics.AddError("API Error", "No Location header in operation response")
|
|
return
|
|
}
|
|
operationId := location[2:]
|
|
|
|
// Step 3: Submit operation parameters and run
|
|
if err := r.submitOperationParams(ctx, operationId, data); err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit operation parameters: %s", err))
|
|
return
|
|
}
|
|
|
|
// Step 4: Wait for operation to complete, then check instance status
|
|
if err := r.waitForOperationAndInstanceStatus(ctx, operationId, instanceId, 10*time.Minute); err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Instance failed to reach running status: %s", err))
|
|
return
|
|
}
|
|
|
|
readData, err := r.readInstance(ctx, instanceId)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after creation: %s", err))
|
|
return
|
|
}
|
|
|
|
data.Status = types.StringValue(readData.Status)
|
|
|
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
}
|
|
|
|
func (r *VDCResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
|
var data VDCResourceModel
|
|
|
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
instanceResp, err := r.readInstance(ctx, data.ID.ValueString())
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance: %s", err))
|
|
return
|
|
}
|
|
|
|
data.Status = types.StringValue(instanceResp.Status)
|
|
|
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
}
|
|
|
|
func (r *VDCResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
|
// [НЕ ИЗМЕНЯТЬ !!!!]
|
|
// VDC поддерживает изменение параметров через операцию 'modify'.
|
|
// Это позволяет обновлять квоты CPU, RAM и другие параметры без пересоздания ресурса.
|
|
|
|
var data VDCResourceModel
|
|
|
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
operationReq := CreateOperationRequest{
|
|
InstanceUid: data.ID.ValueString(),
|
|
Operation: "modify",
|
|
}
|
|
|
|
jsonData, err := json.Marshal(operationReq)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
|
|
return
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
|
|
return
|
|
}
|
|
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
if r.client.ApiToken != "" {
|
|
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
|
|
}
|
|
|
|
httpResp, err := r.client.HttpClient.Do(httpReq)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update instance: %s", err))
|
|
return
|
|
}
|
|
defer httpResp.Body.Close()
|
|
|
|
if httpResp.StatusCode != http.StatusCreated {
|
|
body, _ := io.ReadAll(httpResp.Body)
|
|
resp.Diagnostics.AddError(
|
|
"API Error",
|
|
fmt.Sprintf("Modify operation failed with status %d: %s", httpResp.StatusCode, string(body)),
|
|
)
|
|
return
|
|
}
|
|
|
|
location := httpResp.Header.Get("Location")
|
|
if location != "" {
|
|
operationId := location[2:]
|
|
if err := r.submitOperationParams(ctx, operationId, data); err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit operation parameters: %s", err))
|
|
return
|
|
}
|
|
}
|
|
|
|
time.Sleep(5 * time.Second)
|
|
readData, err := r.readInstance(ctx, data.ID.ValueString())
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after update: %s", err))
|
|
return
|
|
}
|
|
|
|
data.Status = types.StringValue(readData.Status)
|
|
|
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
|
}
|
|
|
|
func (r *VDCResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
|
// ЛОГИКА УДАЛЕНИЯ VDC:
|
|
// В зависимости от флага deletion_protection:
|
|
// 1. Если true (по умолчанию): Только удаляем из стейта. VDC остается работать.
|
|
// 2. Если false: Вызываем операцию 'suspend' через API.
|
|
// Доступа к VDC больше не будет, данные сохраняются 14 дней, затем авто-удаление.
|
|
|
|
var data VDCResourceModel
|
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
if data.DeletionProtection.ValueBool() {
|
|
tflog.Warn(ctx, "Deletion Protection is ENABLED. VDC will remain active in Nubes Cloud. Manual cleanup required.")
|
|
return
|
|
}
|
|
|
|
tflog.Info(ctx, "Deletion Protection is DISABLED. Triggering 'suspend' for VDC...")
|
|
|
|
instanceId := data.ID.ValueString()
|
|
|
|
// Выполняем операцию suspend
|
|
err := r.triggerOperation(ctx, instanceId, "suspend")
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to suspend VDC: %s", err))
|
|
return
|
|
}
|
|
|
|
tflog.Info(ctx, "VDC suspended successfully. It will be permanently deleted from the cloud in 14 days.")
|
|
}
|
|
|
|
func (r *VDCResource) triggerOperation(ctx context.Context, instanceId string, operationName string) error {
|
|
opReq := InstanceOperationRequest{
|
|
Action: operationName,
|
|
Params: struct{}{},
|
|
}
|
|
|
|
jsonData, err := json.Marshal(opReq)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to marshal request: %s", err)
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instances/"+instanceId+"/run", bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create request: %s", err)
|
|
}
|
|
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
if r.client.ApiToken != "" {
|
|
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
|
|
}
|
|
|
|
httpResp, err := r.client.HttpClient.Do(httpReq)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to trigger operation: %s", err)
|
|
}
|
|
defer httpResp.Body.Close()
|
|
|
|
if httpResp.StatusCode != http.StatusOK && httpResp.StatusCode != http.StatusAccepted {
|
|
body, _ := io.ReadAll(httpResp.Body)
|
|
return fmt.Errorf("operation failed with status %d: %s", httpResp.StatusCode, string(body))
|
|
}
|
|
|
|
// Ждем пока статус изменится на целевой (например, suspended)
|
|
targetStatus := "running"
|
|
if operationName == "suspend" {
|
|
targetStatus = "suspended"
|
|
}
|
|
|
|
return r.client.WaitForInstanceStatus(ctx, instanceId, targetStatus)
|
|
}
|
|
|
|
func (r *VDCResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
|
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
|
|
}
|
|
|
|
func (r *VDCResource) readInstance(ctx context.Context, id string) (*InstanceResponse, error) {
|
|
httpReq, err := http.NewRequestWithContext(ctx, "GET", r.client.ApiEndpoint+"/instances/"+id, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if r.client.ApiToken != "" {
|
|
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
|
|
}
|
|
|
|
httpResp, err := r.client.HttpClient.Do(httpReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer httpResp.Body.Close()
|
|
|
|
body, err := io.ReadAll(httpResp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if httpResp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("read request failed with status %d: %s", httpResp.StatusCode, string(body))
|
|
}
|
|
|
|
var instanceResp InstanceResponse
|
|
if err := json.Unmarshal(body, &instanceResp); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &instanceResp, nil
|
|
}
|
|
|
|
func (r *VDCResource) submitOperationParams(ctx context.Context, operationUid string, data VDCResourceModel) error {
|
|
// [ЛОГИКА СИНХРОНИЗАЦИИ ПАРАМЕТРОВ]
|
|
// 1. Получаем список параметров операции (GET /instanceOperations/{uid}?fields=cfsParams).
|
|
// 2. Для каждого параметра из списка находим значение в Terraform или используем дефолт.
|
|
// 3. Отправляем значение обратно в API (POST /instanceOperationCfsParams).
|
|
// 4. После синхронизации всех параметров вызываем RUN.
|
|
|
|
// Get operation details
|
|
httpReq, err := http.NewRequestWithContext(ctx, "GET",
|
|
r.client.ApiEndpoint+"/instanceOperations/"+operationUid+"?fields=cfsParams", nil)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create request: %s", err)
|
|
}
|
|
|
|
if r.client.ApiToken != "" {
|
|
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
|
|
}
|
|
|
|
httpResp, err := r.client.HttpClient.Do(httpReq)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to get operation: %s", err)
|
|
}
|
|
defer httpResp.Body.Close()
|
|
|
|
body, err := io.ReadAll(httpResp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to read response: %s", err)
|
|
}
|
|
|
|
if httpResp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("get operation failed with status %d: %s", httpResp.StatusCode, string(body))
|
|
}
|
|
|
|
var getResp GetOperationResponse
|
|
if err := json.Unmarshal(body, &getResp); err != nil {
|
|
return fmt.Errorf("unable to unmarshal response: %s", err)
|
|
}
|
|
operationResp := getResp.InstanceOperation
|
|
|
|
// Submit each parameter
|
|
for _, param := range operationResp.CfsParams {
|
|
valToSend := ""
|
|
|
|
// Map VDC parameters by ID
|
|
switch param.SvcOperationCfsParamId {
|
|
case 30: // organizationUid
|
|
if !data.OrganizationUID.IsNull() && !data.OrganizationUID.IsUnknown() {
|
|
valToSend = data.OrganizationUID.ValueString()
|
|
}
|
|
case 335: // providerVdc
|
|
if !data.ProviderVDC.IsNull() && !data.ProviderVDC.IsUnknown() {
|
|
valToSend = data.ProviderVDC.ValueString()
|
|
}
|
|
case 361: // storageProfiles (JSON)
|
|
if !data.StorageProfiles.IsNull() && !data.StorageProfiles.IsUnknown() {
|
|
valToSend = data.StorageProfiles.ValueString()
|
|
}
|
|
case 366: // networkPool
|
|
if !data.NetworkPool.IsNull() && !data.NetworkPool.IsUnknown() {
|
|
valToSend = data.NetworkPool.ValueString()
|
|
}
|
|
case 397: // cpuAllocationPct
|
|
if !data.CpuAllocationPct.IsNull() && !data.CpuAllocationPct.IsUnknown() {
|
|
valToSend = fmt.Sprintf("%d", data.CpuAllocationPct.ValueInt64())
|
|
}
|
|
case 398: // ramAllocationPct
|
|
if !data.RamAllocationPct.IsNull() && !data.RamAllocationPct.IsUnknown() {
|
|
valToSend = fmt.Sprintf("%d", data.RamAllocationPct.ValueInt64())
|
|
}
|
|
case 557: // cpuQuota
|
|
if !data.CpuQuota.IsNull() && !data.CpuQuota.IsUnknown() {
|
|
valToSend = fmt.Sprintf("%d", data.CpuQuota.ValueInt64())
|
|
}
|
|
case 558: // ramQuota
|
|
if !data.RamQuota.IsNull() && !data.RamQuota.IsUnknown() {
|
|
valToSend = fmt.Sprintf("%d", data.RamQuota.ValueInt64())
|
|
}
|
|
default:
|
|
// Use existing or default value for unknown parameters
|
|
if param.ParamValue != nil {
|
|
valToSend = *param.ParamValue
|
|
} else if param.DefaultValue != nil {
|
|
valToSend = *param.DefaultValue
|
|
}
|
|
}
|
|
|
|
// Fix specific data type formatting
|
|
if valToSend == "" || valToSend == "\"\"" {
|
|
if param.DataType == "map" || param.DataType == "json" {
|
|
valToSend = "{}"
|
|
} else if param.DataType == "array" || param.DataType == "list" {
|
|
valToSend = "[]"
|
|
}
|
|
}
|
|
|
|
paramReq := CreateCfsParamRequest{
|
|
InstanceOperationUid: operationUid,
|
|
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
|
|
ParamValue: valToSend,
|
|
}
|
|
|
|
jsonData, err := json.Marshal(paramReq)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to marshal param request: %s", err)
|
|
}
|
|
|
|
httpReq, err = http.NewRequestWithContext(ctx, "POST",
|
|
r.client.ApiEndpoint+"/instanceOperationCfsParams", bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create param request: %s", err)
|
|
}
|
|
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
if r.client.ApiToken != "" {
|
|
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
|
|
}
|
|
|
|
httpResp, err = r.client.HttpClient.Do(httpReq)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to submit parameter: %s", err)
|
|
}
|
|
|
|
respBody, _ := io.ReadAll(httpResp.Body)
|
|
httpResp.Body.Close()
|
|
|
|
if httpResp.StatusCode != http.StatusCreated &&
|
|
httpResp.StatusCode != http.StatusOK &&
|
|
httpResp.StatusCode != http.StatusNoContent {
|
|
return fmt.Errorf("submit parameter id %d failed with status %d: %s",
|
|
param.SvcOperationCfsParamId, httpResp.StatusCode, string(respBody))
|
|
}
|
|
}
|
|
|
|
// Run the operation
|
|
runReq, err := http.NewRequestWithContext(ctx, "POST",
|
|
r.client.ApiEndpoint+"/instanceOperations/"+operationUid+"/run", bytes.NewBuffer([]byte("{}")))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create run request: %s", err)
|
|
}
|
|
|
|
runReq.Header.Set("Content-Type", "application/json")
|
|
if r.client.ApiToken != "" {
|
|
runReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
|
|
}
|
|
|
|
runResp, err := r.client.HttpClient.Do(runReq)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to run operation: %s", err)
|
|
}
|
|
defer runResp.Body.Close()
|
|
|
|
if runResp.StatusCode != http.StatusOK &&
|
|
runResp.StatusCode != http.StatusNoContent &&
|
|
runResp.StatusCode != http.StatusCreated {
|
|
runBody, _ := io.ReadAll(runResp.Body)
|
|
return fmt.Errorf("run operation failed with status %d: %s",
|
|
runResp.StatusCode, string(runBody))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *VDCResource) waitForOperationAndInstanceStatus(ctx context.Context, operationId string, instanceId string, timeout time.Duration) error {
|
|
deadline := time.Now().Add(timeout)
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
log.Printf("[DEBUG] Starting two-stage polling: operationId=%s, instanceId=%s, timeout=%v", operationId, instanceId, timeout)
|
|
|
|
// Шаг 1: Ждём завершения операции
|
|
operationLoop:
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("context cancelled")
|
|
case <-ticker.C:
|
|
if time.Now().After(deadline) {
|
|
return fmt.Errorf("timeout waiting for operation to complete")
|
|
}
|
|
|
|
log.Printf("[DEBUG] Polling operation status: operationId=%s", operationId)
|
|
|
|
// Проверяем статус операции
|
|
httpReq, err := http.NewRequestWithContext(ctx, "GET",
|
|
r.client.ApiEndpoint+"/instanceOperations/"+operationId, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %s", err)
|
|
}
|
|
|
|
if r.client.ApiToken != "" {
|
|
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
|
|
}
|
|
|
|
httpResp, err := r.client.HttpClient.Do(httpReq)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check operation status: %s", err)
|
|
}
|
|
|
|
body, _ := io.ReadAll(httpResp.Body)
|
|
httpResp.Body.Close()
|
|
|
|
if httpResp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("get operation failed with status %d", httpResp.StatusCode)
|
|
}
|
|
|
|
var opResp struct {
|
|
InstanceOperation struct {
|
|
IsInProgress bool `json:"isInProgress"`
|
|
IsPending bool `json:"isPending"`
|
|
} `json:"instanceOperation"`
|
|
}
|
|
|
|
if err := json.Unmarshal(body, &opResp); err != nil {
|
|
return fmt.Errorf("failed to parse operation response: %s", err)
|
|
}
|
|
|
|
log.Printf("[DEBUG] Operation status: isInProgress=%v, isPending=%v", opResp.InstanceOperation.IsInProgress, opResp.InstanceOperation.IsPending)
|
|
|
|
// Операция завершена когда isInProgress=false И isPending=false
|
|
if !opResp.InstanceOperation.IsInProgress && !opResp.InstanceOperation.IsPending {
|
|
log.Printf("[DEBUG] Operation completed, moving to instance status check")
|
|
break operationLoop
|
|
}
|
|
}
|
|
}
|
|
|
|
// Шаг 2: Проверяем статус instance
|
|
ticker2 := time.NewTicker(5 * time.Second)
|
|
defer ticker2.Stop()
|
|
|
|
log.Printf("[DEBUG] Starting instance status polling")
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("context cancelled")
|
|
case <-ticker2.C:
|
|
if time.Now().After(deadline) {
|
|
return fmt.Errorf("timeout waiting for instance to become running")
|
|
}
|
|
|
|
log.Printf("[DEBUG] Polling instance status: instanceId=%s", instanceId)
|
|
|
|
instance, err := r.readInstance(ctx, instanceId)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check instance status: %s", err)
|
|
}
|
|
|
|
log.Printf("[DEBUG] Instance status: %s", instance.Status)
|
|
|
|
if instance.Status == "running" {
|
|
log.Printf("[DEBUG] Instance is running, success!")
|
|
return nil
|
|
}
|
|
|
|
if instance.Status == "error" || instance.Status == "failed" {
|
|
return fmt.Errorf("instance entered error state: %s", instance.Status)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|