diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f57d63e..4fdadaf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,9 +4,6 @@ name: Tests # This GitHub action runs your tests for each pull request and push. # Optionally, you can turn it on using a schedule for regular testing. on: - pull_request: - paths-ignore: - - 'README.md' push: paths-ignore: - 'README.md' diff --git a/docs/resources/vm.md b/docs/resources/vm.md index 53528ba..a9bbf3f 100644 --- a/docs/resources/vm.md +++ b/docs/resources/vm.md @@ -45,6 +45,23 @@ resource "hypercore_vm" "myvm" { } } +resource "hypercore_vm" "import-from-smb" { + group = "my-group" + name = "imported-vm" + description = "some description" + + vcpu = 4 + memory = 4096 # MiB + + import = { + server = "10.5.11.39" + username = ";administrator" + password = "***" + path = "/cidata" + file_name = "example-template.xml" + } +} + output "vm_uuid" { value = hypercore_vm.myvm.id } @@ -63,6 +80,7 @@ output "vm_uuid" { - `clone` (Object) Clone options if the VM is being created as a clone. The `source_vm_uuid` is the UUID of the VM used for cloning,
`user_data` and `meta_data` are used for the cloud init data. (see [below for nested schema](#nestedatt--clone)) - `description` (String) Description of this VM - `group` (String) Group/tag to create this VM in +- `import` (Attributes) Options for importing a VM through a SMB server or some other HTTP location.
Use server, username, password for SMB or http_uri for some other HTTP location. Parameters path and file_name are always **required** (see [below for nested schema](#nestedatt--import)) - `memory` (Number) Memory (RAM) size in `MiB`: If the cloned VM was already created
and it's memory was modified, the cloned VM will be rebooted (either gracefully or forcefully) - `snapshot_schedule_uuid` (String) UUID of the snapshot schedule to create automatic snapshots - `vcpu` (Number) Number of CPUs on this VM. If the cloned VM was already created and it's
`VCPU` was modified, the cloned VM will be rebooted (either gracefully or forcefully) @@ -89,3 +107,19 @@ Optional: - `meta_data` (String) - `source_vm_uuid` (String) - `user_data` (String) + + + +### Nested Schema for `import` + +Required: + +- `file_name` (String) +- `path` (String) + +Optional: + +- `http_uri` (String) +- `password` (String, Sensitive) +- `server` (String) +- `username` (String) diff --git a/examples/resources/hypercore_vm/resource.tf b/examples/resources/hypercore_vm/resource.tf index 422596d..5656b86 100644 --- a/examples/resources/hypercore_vm/resource.tf +++ b/examples/resources/hypercore_vm/resource.tf @@ -30,6 +30,23 @@ resource "hypercore_vm" "myvm" { } } +resource "hypercore_vm" "import-from-smb" { + group = "my-group" + name = "imported-vm" + description = "some description" + + vcpu = 4 + memory = 4096 # MiB + + import = { + server = "10.5.11.39" + username = ";administrator" + password = "***" + path = "/cidata" + file_name = "example-template.xml" + } +} + output "vm_uuid" { value = hypercore_vm.myvm.id } diff --git a/internal/provider/hypercore_vm_resource.go b/internal/provider/hypercore_vm_resource.go index 05c5a4a..b2db316 100644 --- a/internal/provider/hypercore_vm_resource.go +++ b/internal/provider/hypercore_vm_resource.go @@ -43,12 +43,22 @@ type HypercoreVMResourceModel struct { Description types.String `tfsdk:"description"` VCPU types.Int32 `tfsdk:"vcpu"` Memory types.Int64 `tfsdk:"memory"` + Import *ImportModel `tfsdk:"import"` SnapshotScheduleUUID types.String `tfsdk:"snapshot_schedule_uuid"` - Clone CloneModel `tfsdk:"clone"` + Clone *CloneModel `tfsdk:"clone"` AffinityStrategy AffinityStrategyModel `tfsdk:"affinity_strategy"` Id types.String `tfsdk:"id"` } +type ImportModel struct { + HTTPUri types.String `tfsdk:"http_uri"` + Server types.String `tfsdk:"server"` + Username types.String `tfsdk:"username"` + Password types.String `tfsdk:"password"` + Path types.String `tfsdk:"path"` + FileName types.String `tfsdk:"file_name"` +} + type CloneModel struct { SourceVMUUID types.String `tfsdk:"source_vm_uuid"` UserData types.String `tfsdk:"user_data"` @@ -99,6 +109,32 @@ func (r *HypercoreVMResource) Schema(ctx context.Context, req resource.SchemaReq MarkdownDescription: "UUID of the snapshot schedule to create automatic snapshots", Optional: true, }, + "import": schema.SingleNestedAttribute{ + MarkdownDescription: "Options for importing a VM through a SMB server or some other HTTP location.
" + + "Use server, username, password for SMB or http_uri for some other HTTP location. Parameters path and file_name are always **required**", + Optional: true, + Attributes: map[string]schema.Attribute{ + "http_uri": schema.StringAttribute{ + Optional: true, + }, + "server": schema.StringAttribute{ + Optional: true, + }, + "username": schema.StringAttribute{ + Optional: true, + }, + "password": schema.StringAttribute{ + Optional: true, + Sensitive: true, + }, + "path": schema.StringAttribute{ + Required: true, + }, + "file_name": schema.StringAttribute{ + Required: true, + }, + }, + }, "clone": schema.ObjectAttribute{ MarkdownDescription: "" + "Clone options if the VM is being created as a clone. The `source_vm_uuid` is the UUID of the VM used for cloning,
" + @@ -166,16 +202,121 @@ func (r *HypercoreVMResource) Configure(ctx context.Context, req resource.Config r.client = restClient } +func getVMStruct(data *HypercoreVMResourceModel, vmDescription *string, vmTags *[]string) *utils.VM { + // Gets VM structure from Utils.VM, sends parameters based on which VM create logic is being called + sourceVMUUID, userData, metaData := "", "", "" + if data.Clone != nil { + sourceVMUUID = data.Clone.SourceVMUUID.ValueString() + userData = data.Clone.UserData.ValueString() + metaData = data.Clone.MetaData.ValueString() + } + vmStruct := utils.GetVMStruct( + data.Name.ValueString(), + sourceVMUUID, + userData, + metaData, + vmDescription, + vmTags, + data.VCPU.ValueInt32Pointer(), + data.Memory.ValueInt64Pointer(), + data.SnapshotScheduleUUID.ValueStringPointer(), + nil, + data.AffinityStrategy.StrictAffinity.ValueBool(), + data.AffinityStrategy.PreferredNodeUUID.ValueString(), + data.AffinityStrategy.BackupNodeUUID.ValueString(), + ) + return vmStruct +} + +func validateParameters(data *HypercoreVMResourceModel) (*string, *[]string) { + var tags *[]string + var description *string + + if data.Group.ValueString() == "" { + tags = nil + } else { + t := []string{data.Group.ValueString()} + tags = &t + } + + if data.Description.ValueString() == "" { + description = nil + } else { + description = data.Description.ValueStringPointer() + } + return description, tags +} +func isHTTPImport(data *HypercoreVMResourceModel) bool { + // Check if HTTP URI is being used for VM import + httpUri := data.Import.HTTPUri.ValueString() + return httpUri != "" +} +func isSMBImport(data *HypercoreVMResourceModel) bool { + smbServer := data.Import.Server.ValueString() + smbUsername := data.Import.Username.ValueString() + smbPassword := data.Import.Password.ValueString() + + return smbServer != "" || smbUsername != "" || smbPassword != "" +} + +func (r *HypercoreVMResource) handleCloneLogic(data *HypercoreVMResourceModel, ctx context.Context, vmNew *utils.VM) { + changed, msg := vmNew.Clone(*r.client, ctx) + tflog.Info(ctx, fmt.Sprintf("Changed: %t, Message: %s\n", changed, msg)) + // Clone will retain setting from original VM so we call SetVMParams to change those settings based on user input before we save state + changed, vmWasRebooted, vmDiff := vmNew.SetVMParams(*r.client, ctx) + data.Id = types.StringValue(vmNew.UUID) + tflog.Info(ctx, fmt.Sprintf("Changed: %t, Was VM Rebooted: %t, Diff: %v", changed, vmWasRebooted, vmDiff)) +} +func (r *HypercoreVMResource) handleImportFromSMBLogic(data *HypercoreVMResourceModel, ctx context.Context, resp *resource.CreateResponse, vmNew *utils.VM, path string, fileName string) { + smbServer, smbUsername, smbPassword := data.Import.Server.ValueString(), data.Import.Username.ValueString(), data.Import.Password.ValueString() + errorDiagnostic := utils.ValidateSMB(smbServer, smbUsername, smbPassword) + if errorDiagnostic != nil { + resp.Diagnostics.AddError(errorDiagnostic.Summary(), errorDiagnostic.Detail()) + return + } + smbSource := utils.BuildImportSource(smbUsername, smbPassword, smbServer, path, fileName, "", true) + vmNew.Import(*r.client, smbSource, ctx) + // Import will retain setting from original VM so we call SetVMParams to change those settings based on user input before we save state + changed, vmWasRebooted, vmDiff := vmNew.SetVMParams(*r.client, ctx) + data.Id = types.StringValue(vmNew.UUID) + tflog.Info(ctx, fmt.Sprintf("Changed: %t, Was VM Rebooted: %t, Diff: %v", changed, vmWasRebooted, vmDiff)) +} +func (r *HypercoreVMResource) handleImportFromURILogic(data *HypercoreVMResourceModel, ctx context.Context, resp *resource.CreateResponse, vmNew *utils.VM, path string, fileName string) { + httpUri := data.Import.HTTPUri.ValueString() + errorDiagnostic := utils.ValidateHTTP(httpUri, path) + if errorDiagnostic != nil { + resp.Diagnostics.AddError(errorDiagnostic.Summary(), errorDiagnostic.Detail()) + return + } + httpSource := utils.BuildImportSource("", "", "", path, fileName, httpUri, false) + vmNew.Import(*r.client, httpSource, ctx) + // Import will retain setting from original VM so we call SetVMParams to change those settings based on user input before we save state + changed, vmWasRebooted, vmDiff := vmNew.SetVMParams(*r.client, ctx) + data.Id = types.StringValue(vmNew.UUID) + tflog.Info(ctx, fmt.Sprintf("Changed: %t, Was VM Rebooted: %t, Diff: %v", changed, vmWasRebooted, vmDiff)) +} +func (r *HypercoreVMResource) doCreateLogic(data *HypercoreVMResourceModel, ctx context.Context, resp *resource.CreateResponse, description *string, tags *[]string) { + vmNew := getVMStruct(data, description, tags) + // Chose which VM create logic we're going with (clone or import) + if data.Clone != nil { + r.handleCloneLogic(data, ctx, vmNew) + } else if data.Import != nil { + path := data.Import.Path.ValueString() + fileName := data.Import.FileName.ValueString() + if isHTTPImport(data) && !isSMBImport(data) { + r.handleImportFromURILogic(data, ctx, resp, vmNew, path, fileName) + } else if isSMBImport(data) && !isHTTPImport(data) { + r.handleImportFromSMBLogic(data, ctx, resp, vmNew, path, fileName) + } + } +} + func (r *HypercoreVMResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { tflog.Info(ctx, "TTRT HypercoreVMResource CREATE") var data HypercoreVMResourceModel - // var readData HypercoreVMResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - // resp.State.Get(ctx, &readData) - // - // tflog.Debug(ctx, fmt.Sprintf("STATE IS: %v\n", readData.Disks)) if r.client == nil { resp.Diagnostics.AddError( @@ -190,48 +331,11 @@ func (r *HypercoreVMResource) Create(ctx context.Context, req resource.CreateReq return } - var tags *[]string - var description *string - - if data.Group.ValueString() == "" { - tags = nil - } else { - tags = &[]string{data.Group.ValueString()} - } - - if data.Description.ValueString() == "" { - description = nil - } else { - description = data.Description.ValueStringPointer() - } - - tflog.Info(ctx, fmt.Sprintf("TTRT Create: name=%s, source_uuid=%s", data.Name.ValueString(), data.Clone.SourceVMUUID.ValueString())) - - vmClone, _ := utils.NewVM( - data.Name.ValueString(), - data.Clone.SourceVMUUID.ValueString(), - data.Clone.UserData.ValueString(), - data.Clone.MetaData.ValueString(), - description, - tags, - data.VCPU.ValueInt32Pointer(), - data.Memory.ValueInt64Pointer(), - data.SnapshotScheduleUUID.ValueStringPointer(), - nil, - data.AffinityStrategy.StrictAffinity.ValueBool(), - data.AffinityStrategy.PreferredNodeUUID.ValueString(), - data.AffinityStrategy.BackupNodeUUID.ValueString(), - ) - changed, msg := vmClone.Create(*r.client, ctx) - tflog.Info(ctx, fmt.Sprintf("Changed: %t, Message: %s\n", changed, msg)) - - // General parametrization - // set: description, group, vcpu, memory, power_state - changed, vmWasRebooted, vmDiff := vmClone.SetVMParams(*r.client, ctx) - tflog.Info(ctx, fmt.Sprintf("Changed: %t, Was VM Rebooted: %t, Diff: %v", changed, vmWasRebooted, vmDiff)) + // Validate parameters TODO: Add other inputs here from schema if validation is needed + description, tags := validateParameters(&data) - // save into the Terraform state. - data.Id = types.StringValue(vmClone.UUID) + // Right now handles import or clone TODO: Add other VM create options here + r.doCreateLogic(&data, ctx, resp, description, tags) // Write logs using the tflog package // Documentation: https://terraform.io/plugin/log diff --git a/internal/provider/tests/acceptance/hypercore_vm_resource_acc_test.go b/internal/provider/tests/acceptance/hypercore_vm_resource_clone_acc_test.go similarity index 90% rename from internal/provider/tests/acceptance/hypercore_vm_resource_acc_test.go rename to internal/provider/tests/acceptance/hypercore_vm_resource_clone_acc_test.go index b776ae9..6f09351 100644 --- a/internal/provider/tests/acceptance/hypercore_vm_resource_acc_test.go +++ b/internal/provider/tests/acceptance/hypercore_vm_resource_clone_acc_test.go @@ -10,19 +10,18 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) -var requested_power_state string = "stop" // "started" // UUID of VM with name "testtf_src" // var testtf_src_uuid string = "27af8248-88ee-4420-85d7-78b735415064" // https://172.31.6.11 var testtf_src_uuid string = "ff36479e-06bb-4141-bad5-0097c8c1a4a6" // https://10.5.11.205 -func TestAccHypercoreVMResource(t *testing.T) { +func TestAccHypercoreVMResourceClone(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and Read testing { - Config: testAccHypercoreVMResourceConfig("testtf-vm", testtf_src_uuid), + Config: testAccHypercoreVMResourceCloneConfig("testtf-vm", testtf_src_uuid), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("hypercore_vm.test", "description", "testtf-vm-description"), resource.TestCheckResourceAttr("hypercore_vm.test", "memory", "4096"), @@ -64,7 +63,7 @@ func TestAccHypercoreVMResource(t *testing.T) { */ // Update and Read testing { - Config: testAccHypercoreVMResourceConfig("testtf-vm", testtf_src_uuid), + Config: testAccHypercoreVMResourceCloneConfig("testtf-vm", testtf_src_uuid), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("hypercore_vm.test", "name", "testtf-vm"), resource.TestCheckResourceAttr("hypercore_vm.test", "description", "testtf-vm-description"), @@ -79,7 +78,7 @@ func TestAccHypercoreVMResource(t *testing.T) { }) } -func testAccHypercoreVMResourceConfig(vm_name string, source_vm_uuid string) string { +func testAccHypercoreVMResourceCloneConfig(vm_name string, source_vm_uuid string) string { return fmt.Sprintf(` resource "hypercore_vm" "test" { name = %[1]q diff --git a/internal/provider/tests/acceptance/hypercore_vm_resource_import_acc_test.go b/internal/provider/tests/acceptance/hypercore_vm_resource_import_acc_test.go new file mode 100644 index 0000000..517a051 --- /dev/null +++ b/internal/provider/tests/acceptance/hypercore_vm_resource_import_acc_test.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package acceptance + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccHypercoreVMResourceImport(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccHypercoreVMResourceImportConfig("imported_vm_integration"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("hypercore_vm.test", "description", "imported-vm"), + resource.TestCheckResourceAttr("hypercore_vm.test", "memory", "4096"), + resource.TestCheckResourceAttr("hypercore_vm.test", "group", "Xlabintegrationtest"), + resource.TestCheckResourceAttr("hypercore_vm.test", "name", "imported_vm_integration"), + resource.TestCheckResourceAttr("hypercore_vm.test", "vcpu", "4"), + ), + }, + }, + }) +} + +func testAccHypercoreVMResourceImportConfig(vm_name string) string { + return fmt.Sprintf(` +resource "hypercore_vm" "test" { + name = %[1]q + group = "Xlabintegrationtest" + vcpu = 4 + memory = 4096 + description = "imported-vm" + snapshot_schedule_uuid = "" + // power_state = %[3]q + import = { + server = %[2]q + username = %[3]q + password = %[4]q + path = %[5]q + file_name = %[6]q + } +} +`, vm_name, smb_server, smb_username, smb_password, smb_path, smb_filename, requested_power_state) +} diff --git a/internal/provider/tests/acceptance/provider_acc_test.go b/internal/provider/tests/acceptance/provider_acc_test.go index 00f4887..dc05ec6 100644 --- a/internal/provider/tests/acceptance/provider_acc_test.go +++ b/internal/provider/tests/acceptance/provider_acc_test.go @@ -16,6 +16,13 @@ var source_vm_name = os.Getenv("SOURCE_VM_NAME") var existing_vdisk_uuid = os.Getenv("EXISTING_VDISK_UUID") var source_nic_uuid = os.Getenv("SOURCE_NIC_UUID") var source_disk_uuid = os.Getenv("SOURCE_DISK_UUID") +var smb_server = os.Getenv("SMB_SERVER") +var smb_username = os.Getenv("SMB_USERNAME") +var smb_password = os.Getenv("SMB_PASSWORD") +var smb_path = os.Getenv("SMB_PATH") +var smb_filename = os.Getenv("SMB_FILENAME") + +var requested_power_state string = "stop" // "started" // testAccProtoV6ProviderFactories are used to instantiate a provider during // acceptance testing. The factory function will be invoked for every Terraform diff --git a/internal/utils/helper.go b/internal/utils/helper.go index 25f3de5..58ffc06 100644 --- a/internal/utils/helper.go +++ b/internal/utils/helper.go @@ -12,6 +12,9 @@ import ( "net/http" "os" "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/diag" ) func isSuperset(superset map[string]any, candidate map[string]any) bool { @@ -83,21 +86,6 @@ func filterResultsRecursive(results []map[string]any, filterData map[string]any) return filtered } -// nolint:unused -func filterMap(input map[string]any, fieldNames ...string) map[string]any { - output := map[string]any{} - - for _, fieldName := range fieldNames { - if value, ok := input[fieldName]; ok { - if value != nil || value != "" { - output[fieldName] = value - } - } - } - - return output -} - func jsonObjectToTaskTag(jsonObj any) *TaskTag { var taskTag *TaskTag @@ -247,7 +235,7 @@ func AnyToListOfStrings(list any) []string { func ReadLocalFileBinary(filePath string) ([]byte, error) { file, err := os.Open(filePath) if err != nil { - return nil, fmt.Errorf("Error opening file '%s': %s", filePath, err) + return nil, fmt.Errorf("error opening file '%s': %s", filePath, err) } defer file.Close() @@ -303,3 +291,42 @@ func GetFileSize(sourceFilePath string) int64 { } return fileInfo.Size() } + +func ValidateSMB(server string, username string, password string) diag.Diagnostic { + if server == "" { + return diag.NewErrorDiagnostic( + "Missing 'server' parameter", + "For using SMB, you must specify the 'server' parameter", + ) + } + if username == "" { + return diag.NewErrorDiagnostic( + "Missing 'username' parameter", + "For using SMB, you must specify the 'username' parameter", + ) + } + if password == "" { + return diag.NewErrorDiagnostic( + "Missing 'password' parameter", + "For using SMB, you must specify the 'password' parameter", + ) + } + return nil +} + +func ValidateHTTP(httpUri string, path string) diag.Diagnostic { + if !strings.HasPrefix(httpUri, "http://") && !strings.HasPrefix(httpUri, "https://") { + return diag.NewErrorDiagnostic( + "Invalid HTTP uri", + "Invalid HTTP uri. Uri must start with 'http://' or 'https://'", + ) + } + if path == "" { + return diag.NewErrorDiagnostic( + "Invalid path", + "Invalid path. Path parameter must be defined and start with '/'", + ) + } + + return nil +} diff --git a/internal/utils/rest_client.go b/internal/utils/rest_client.go index f9e45e2..07faeac 100644 --- a/internal/utils/rest_client.go +++ b/internal/utils/rest_client.go @@ -60,7 +60,7 @@ func (rc *RestClient) GetAuthHeader() map[string]string { func (rc *RestClient) ToJson(response *http.Response) any { respBytes, err := io.ReadAll(response.Body) if err != nil { - panic(fmt.Errorf("Failed to read response body: %s", err.Error())) + panic(fmt.Errorf("failed to read response body: %s", err.Error())) } var respJson any @@ -80,18 +80,18 @@ func (rc *RestClient) ToJsonObjectList(response *http.Response) []map[string]any if obj, ok := item.(map[string]any); ok { result = append(result, obj) } else { - panic(fmt.Errorf("Unexpected item in response list: %v", item)) + panic(fmt.Errorf("unexpected item in response list: %v", item)) } } return result } - panic(fmt.Errorf("Expected a JSON list of objects, go: %v", respJson)) + panic(fmt.Errorf("expected a JSON list of objects, go: %v", respJson)) } func (rc *RestClient) ToString(response *http.Response) string { respBytes, err := io.ReadAll(response.Body) if err != nil { - panic(fmt.Errorf("Failed to read response body: %s", err.Error())) + panic(fmt.Errorf("failed to read response body: %s", err.Error())) } return string(respBytes) } @@ -103,7 +103,7 @@ func (rc *RestClient) Request(method string, endpoint string, body map[string]an if body != nil { jsonBody, err = json.Marshal(body) if err != nil { - panic(fmt.Errorf("Failed to marshal JSON body: %s", err.Error())) + panic(fmt.Errorf("failed to marshal JSON body: %s", err.Error())) } } @@ -113,7 +113,7 @@ func (rc *RestClient) Request(method string, endpoint string, body map[string]an bytes.NewBuffer(jsonBody), ) if err != nil { - panic(fmt.Errorf("Invalid request: %s", err.Error())) + panic(fmt.Errorf("invalid request: %s", err.Error())) } req.Header.Set("Accept", "application/json") @@ -141,7 +141,7 @@ func (rc *RestClient) RequestBinary( bytes.NewBuffer(binaryData), ) if err != nil { - panic(fmt.Errorf("Invalid request: %s", err.Error())) + panic(fmt.Errorf("invalid request: %s", err.Error())) } req.Header.Set("Accept", "application/json") @@ -162,7 +162,7 @@ func (rc *RestClient) RequestWithList(method string, endpoint string, body []map if body != nil { jsonBody, err = json.Marshal(body) if err != nil { - panic(fmt.Errorf("Failed to marshal JSON body: %s", err.Error())) + panic(fmt.Errorf("failed to marshal JSON body: %s", err.Error())) } } @@ -172,7 +172,7 @@ func (rc *RestClient) RequestWithList(method string, endpoint string, body []map bytes.NewBuffer(jsonBody), ) if err != nil { - panic(fmt.Errorf("Invalid request: %s", err.Error())) + panic(fmt.Errorf("invalid request: %s", err.Error())) } req.Header.Set("Accept", "application/json") @@ -199,12 +199,12 @@ func (rc *RestClient) Login() { resp, err := rc.HttpClient.Do(req) if err != nil { - panic(fmt.Errorf("Couldn't authenticate: %s", err.Error())) + panic(fmt.Errorf("couldn't authenticate: %s", err.Error())) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - panic(fmt.Errorf("Authentication failed with status code: %d", resp.StatusCode)) + panic(fmt.Errorf("authentication failed with status code: %d", resp.StatusCode)) } if respJson, ok := rc.ToJson(resp).(map[string]any); ok { @@ -212,7 +212,7 @@ func (rc *RestClient) Login() { "Cookie": fmt.Sprintf("sessionID=%s", respJson["sessionID"]), } } else { - panic(fmt.Errorf("Session ID not found in response")) + panic(fmt.Errorf("session ID not found in response")) } } @@ -233,12 +233,12 @@ func (rc *RestClient) ListRecords(endpoint string, query map[string]any, timeout resp, err := client.Do(req) if err != nil { - panic(fmt.Errorf("Error making a request: %s", err.Error())) + panic(fmt.Errorf("error making a request: %s", err.Error())) } defer resp.Body.Close() if !(resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNoContent) { - panic(fmt.Errorf("Unexpected response: %d - %v", resp.StatusCode, rc.ToString(resp))) + panic(fmt.Errorf("unexpected response: %d - %v", resp.StatusCode, rc.ToString(resp))) } records := rc.ToJsonObjectList(resp) @@ -302,9 +302,9 @@ func (rc *RestClient) CreateRecord(endpoint string, payload map[string]any, time if respErr, ok := respJsonMap["error"]; ok { return nil, resp.StatusCode, fmt.Errorf("%s", AnyToString(respErr)) } - panic(fmt.Errorf("Unexpected response body: %v", respJson)) + panic(fmt.Errorf("unexpected response body: %v", respJson)) } - panic(fmt.Errorf("Error making a request: Maybe the arguments passed to were incorrectly formatted: %v - response: %v", payload, string(respByte))) + panic(fmt.Errorf("error making a request: Maybe the arguments passed to were incorrectly formatted: %v - response: %v", payload, string(respByte))) } if _, ok := AnyToMap(respJson)["taskTag"]; !ok { @@ -340,9 +340,9 @@ func (rc *RestClient) CreateRecordWithList(endpoint string, payload []map[string if resp.StatusCode == 400 { respByte, ok := respJson.([]byte) if !ok { // this check is needed because of conversion from any to []byte - panic(fmt.Errorf("Unexpected response body: %v", respJson)) + panic(fmt.Errorf("unexpected response body: %v", respJson)) } - panic(fmt.Errorf("Error making a request: Maybe the arguments passed were incorrectly formatted: %v - response: %v", payload, string(respByte))) + panic(fmt.Errorf("error making a request: Maybe the arguments passed were incorrectly formatted: %v - response: %v", payload, string(respByte))) } if _, ok := AnyToMap(respJson)["taskTag"]; !ok { @@ -370,7 +370,7 @@ func (rc *RestClient) UpdateRecord(endpoint string, payload map[string]any, time resp, err := client.Do(req) if err != nil { - panic(fmt.Errorf("Error making a request: %s", err.Error())) + panic(fmt.Errorf("error making a request: %s", err.Error())) } defer resp.Body.Close() @@ -378,9 +378,9 @@ func (rc *RestClient) UpdateRecord(endpoint string, payload map[string]any, time if resp.StatusCode == 400 { respByte, ok := respJson.([]byte) if !ok { // this check is needed because of conversion from any to []byte - panic(fmt.Errorf("Unexpected response body: %v", respJson)) + panic(fmt.Errorf("unexpected response body: %v", respJson)) } - panic(fmt.Errorf("Error making a request: Maybe the arguments passed were incorrectly formatted: %v - response: %v", payload, string(respByte))) + panic(fmt.Errorf("error making a request: Maybe the arguments passed were incorrectly formatted: %v - response: %v", payload, string(respByte))) } if _, ok := AnyToMap(respJson)["taskTag"]; !ok { @@ -408,7 +408,7 @@ func (rc *RestClient) PutRecord(endpoint string, payload map[string]any, timeout resp, err := client.Do(req) if err != nil { - panic(fmt.Errorf("Error making a request: %s", err.Error())) + panic(fmt.Errorf("error making a request: %s", err.Error())) } defer resp.Body.Close() @@ -416,9 +416,9 @@ func (rc *RestClient) PutRecord(endpoint string, payload map[string]any, timeout if resp.StatusCode == 400 { respByte, ok := respJson.([]byte) if !ok { // this check is needed because of conversion from any to []byte - panic(fmt.Errorf("Unexpected response body: %v", respJson)) + panic(fmt.Errorf("unexpected response body: %v", respJson)) } - panic(fmt.Errorf("Error making a request: Maybe the arguments passed were incorrectly formatted: %v - response: %v", payload, string(respByte))) + panic(fmt.Errorf("error making a request: Maybe the arguments passed were incorrectly formatted: %v - response: %v", payload, string(respByte))) } if _, ok := AnyToMap(respJson)["taskTag"]; !ok { @@ -447,7 +447,7 @@ func (rc *RestClient) PutBinaryRecord(endpoint string, binaryData []byte, conten resp, err := client.Do(req) if err != nil { - panic(fmt.Errorf("Error making a request: %s", err.Error())) + panic(fmt.Errorf("error making a request: %s", err.Error())) } defer resp.Body.Close() @@ -455,9 +455,9 @@ func (rc *RestClient) PutBinaryRecord(endpoint string, binaryData []byte, conten if resp.StatusCode == 400 { respByte, ok := respJson.([]byte) if !ok { // this check is needed because of conversion from any to []byte - panic(fmt.Errorf("Unexpected response body: %v", respJson)) + panic(fmt.Errorf("unexpected response body: %v", respJson)) } - panic(fmt.Errorf("Error making a request: Maybe the arguments passed were incorrectly formatted: %v - response: %v", binaryData, string(respByte))) + panic(fmt.Errorf("error making a request: Maybe the arguments passed were incorrectly formatted: %v - response: %v", binaryData, string(respByte))) } if _, ok := AnyToMap(respJson)["taskTag"]; !ok { @@ -486,12 +486,12 @@ func (rc *RestClient) PutBinaryRecordWithoutTaskTag(endpoint string, binaryData resp, err := client.Do(req) if err != nil { - panic(fmt.Errorf("Error making a request: %s", err.Error())) + panic(fmt.Errorf("error making a request: %s", err.Error())) } defer resp.Body.Close() if resp.StatusCode != 200 { - panic(fmt.Errorf("Error making a request: got response status code %v", resp.StatusCode)) + panic(fmt.Errorf("error making a request: got response status code %v", resp.StatusCode)) } return resp.StatusCode, nil @@ -514,7 +514,7 @@ func (rc *RestClient) DeleteRecord(endpoint string, timeout float64, ctx context resp, err := client.Do(req) if err != nil { - panic(fmt.Errorf("Error making a request: %s", err.Error())) + panic(fmt.Errorf("error making a request: %s", err.Error())) } defer resp.Body.Close() diff --git a/internal/utils/vm.go b/internal/utils/vm.go index 619ebfa..25e9ea2 100644 --- a/internal/utils/vm.go +++ b/internal/utils/vm.go @@ -79,7 +79,7 @@ type VM struct { _wasResetTried bool } -func NewVM( +func GetVMStruct( _VMName string, _sourceVMUUID string, userData string, @@ -93,11 +93,11 @@ func NewVM( _strictAffinity bool, _preferredNodeUUID string, _backupNodeUUID string, -) (*VM, error) { +) *VM { userDataB64 := base64.StdEncoding.EncodeToString([]byte(userData)) metaDataB64 := base64.StdEncoding.EncodeToString([]byte(metaData)) - vmClone := &VM{ + vmNew := &VM{ UUID: "", VMName: _VMName, sourceVMUUID: _sourceVMUUID, @@ -125,10 +125,10 @@ func NewVM( _wasResetTried: false, } - return vmClone, nil + return vmNew } -func (vc *VM) Clone(restClient RestClient, sourceVM map[string]any) *TaskTag { +func (vc *VM) SendCloneRequest(restClient RestClient, sourceVM map[string]any) *TaskTag { // Clone payload clonePayload := map[string]any{ "template": map[string]any{ @@ -145,8 +145,7 @@ func (vc *VM) Clone(restClient RestClient, sourceVM map[string]any) *TaskTag { return taskTag } - -func (vc *VM) Create(restClient RestClient, ctx context.Context) (bool, string) { +func (vc *VM) Clone(restClient RestClient, ctx context.Context) (bool, string) { vm := GetVM(map[string]any{"name": vc.VMName}, restClient) if len(vm) > 0 { @@ -161,7 +160,7 @@ func (vc *VM) Create(restClient RestClient, ctx context.Context) (bool, string) sourceVMName, _ := sourceVM["name"].(string) // Clone payload - task := vc.Clone(restClient, sourceVM) + task := vc.SendCloneRequest(restClient, sourceVM) task.WaitTask(restClient, ctx) taskStatus := task.GetStatus(restClient) @@ -175,6 +174,34 @@ func (vc *VM) Create(restClient RestClient, ctx context.Context) (bool, string) panic(fmt.Sprintf("There was a problem during cloning of %s %s, cloning failed.", sourceVMName, vc.sourceVMUUID)) } +func (vc *VM) SendImportRequest(restClient RestClient, source map[string]any) *TaskTag { + payload := map[string]any{ + "source": source, + } + + importTemplate := vc.BuildImportTemplate() + if len(importTemplate) > 0 { + payload["template"] = importTemplate + } + + taskTag, _, _ := restClient.CreateRecord( + "/rest/v1/VirDomain/import", + payload, + -1, + ) + //panic(fmt.Sprintf("neki neki: %d, %v", statusCode, err)) + return taskTag +} +func (vc *VM) Import(restClient RestClient, source map[string]any, ctx context.Context) map[string]any { + task := vc.SendImportRequest(restClient, source) + task.WaitTask(restClient, ctx) + vmUUID := task.CreatedUUID + vm := GetOneVM(vmUUID, restClient) + + vc.UUID = vmUUID + return vm +} + func (vc *VM) SetVMParams(restClient RestClient, ctx context.Context) (bool, bool, map[string]any) { vm := GetVMByName(vc.VMName, restClient, true) changed, changedParams := vc.GetChangedParams(ctx, *vm) @@ -436,9 +463,62 @@ func (vc *VM) BuildUpdatePayload(changedParams map[string]bool) map[string]any { return updatePayload } +func (vc *VM) BuildImportTemplate() map[string]any { + importTemplate := map[string]any{} + + if vc.description != nil { + importTemplate["description"] = *vc.description + } + if vc.tags != nil { + importTemplate["tags"] = tagsListToCommaString(*vc.tags) + } + if vc.memory != nil { + vcMemoryBytes := *vc.memory * 1024 * 1024 // MB to B + importTemplate["mem"] = vcMemoryBytes + } + if vc.vcpu != nil { + importTemplate["numVCPU"] = *vc.vcpu + } + if vc.VMName != "" { + importTemplate["name"] = vc.VMName + } + + affinityStrategy := map[string]any{ + "strictAffinity": vc.strictAffinity, + } + + if vc.preferredNodeUUID != "" { + affinityStrategy["preferredNodeUUID"] = vc.preferredNodeUUID + } + + if vc.backupNodeUUID != "" { + affinityStrategy["backupNodeUUID"] = vc.backupNodeUUID + } + + importTemplate["affinityStrategy"] = affinityStrategy + + return importTemplate +} +func BuildImportSource(username string, password string, server string, path string, fileName string, httpUri string, isSMB bool) map[string]any { + pathURI := "" + if isSMB { + pathURI = fmt.Sprintf("smb://%s:%s@%s%s", username, password, server, path) + } else { + pathURI = fmt.Sprintf("%s%s", httpUri, path) + } + + source := map[string]any{ + "pathURI": pathURI, + } + if fileName != "" { + source["definitionFileName"] = fileName + } + + return source +} + func (vc *VM) GetChangedParams(ctx context.Context, vmFromClient map[string]any) (bool, map[string]bool) { changedParams := map[string]bool{} - if vc.description != nil { changedParams["description"] = *vc.description != vmFromClient["description"] } @@ -450,7 +530,7 @@ func (vc *VM) GetChangedParams(ctx context.Context, vmFromClient map[string]any) changedParams["memory"] = vcMemoryBytes != vmFromClient["mem"] } if vc.vcpu != nil { - changedParams["vcpu"] = *vc.memory != vmFromClient["numVCPU"] + changedParams["vcpu"] = *vc.vcpu != vmFromClient["numVCPU"] } if vc.powerState != nil { requestedPowerAction := *vc.powerState @@ -503,7 +583,7 @@ func GetOneVMWithError(uuid string, restClient RestClient) (*map[string]any, err ) if record == nil { - return nil, fmt.Errorf("VM not found - vmUUID=%s.\n", uuid) + return nil, fmt.Errorf("vm not found - vmUUID=%s", uuid) } return record, nil @@ -518,7 +598,7 @@ func GetVMOrFail(query map[string]any, restClient RestClient) []map[string]any { ) if len(records) == 0 { - panic(fmt.Errorf("No VM found: %v", query)) + panic(fmt.Errorf("no VM found: %v", query)) } return records diff --git a/internal/utils/vm_disk.go b/internal/utils/vm_disk.go index f37d320..f8d49ad 100644 --- a/internal/utils/vm_disk.go +++ b/internal/utils/vm_disk.go @@ -36,7 +36,7 @@ func NewVMDisk( _size *float64, ) (*VMDisk, error) { if !ALLOWED_DISK_TYPES[_type] { - return nil, fmt.Errorf("Disk type '%s' not allowed. Allowed types are: IDE_DISK, SCSI_DISK, VIRTIO_DISK, IDE_FLOPPY, NVRAM, VTPM\n", _type) + return nil, fmt.Errorf("disk type '%s' not allowed. Allowed types are: IDE_DISK, SCSI_DISK, VIRTIO_DISK, IDE_FLOPPY, NVRAM, VTPM", _type) } var byteSize *float64 @@ -108,7 +108,7 @@ func (vd *VMDisk) CreateOrUpdate( desiredDiskSize := AnyToFloat64(desiredDisk["capacity"]) / 1000 / 1000 / 1000 if existingDiskSize > desiredDiskSize { return false, false, "", fmt.Errorf( - "Disk of type '%s' on slot %d can only be expanded. Use a different slot or use a larger size. %v GB > %v GB\n", + "disk of type '%s' on slot %d can only be expanded. Use a different slot or use a larger size. %v GB > %v GB", existingDiskType, existingDiskSlot, existingDiskSize, desiredDiskSize, ) }