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,
)
}