Skip to content

Commit 05d5994

Browse files
committed
Add VM import (based on HC3 API) to hypercore_vm_resource
1 parent bcd3170 commit 05d5994

File tree

4 files changed

+228
-13
lines changed

4 files changed

+228
-13
lines changed

internal/provider/hypercore_vm_resource.go

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,22 @@ type HypercoreVMResourceModel struct {
3939
Description types.String `tfsdk:"description"`
4040
VCPU types.Int32 `tfsdk:"vcpu"`
4141
Memory types.Int64 `tfsdk:"memory"`
42+
Import ImportModel `tfsdk:"import"`
43+
HTTPImportPath types.String `tfsdk:"http_import_path"`
4244
Clone CloneModel `tfsdk:"clone"`
4345
AffinityStrategy AffinityStrategyModel `tfsdk:"affinity_strategy"`
4446
Id types.String `tfsdk:"id"`
4547
}
4648

49+
type ImportModel struct {
50+
HTTPPath types.String `tfsdk:"http_path"`
51+
Server types.String `tfsdk:"server"`
52+
Username types.String `tfsdk:"username"`
53+
Password types.String `tfsdk:"password"`
54+
Path types.String `tfsdk:"path"`
55+
FileName types.String `tfsdk:"file_name"`
56+
}
57+
4758
type CloneModel struct {
4859
SourceVMUUID types.String `tfsdk:"source_vm_uuid"`
4960
UserData types.String `tfsdk:"user_data"`
@@ -90,6 +101,19 @@ func (r *HypercoreVMResource) Schema(ctx context.Context, req resource.SchemaReq
90101
"and it's memory was modified, the cloned VM will be rebooted (either gracefully or forcefully)",
91102
Optional: true,
92103
},
104+
"import": schema.ObjectAttribute{
105+
MarkdownDescription: "Options for importing a VM through an SMB server. <br>" +
106+
"All parameters except `file_path` are **required**",
107+
Optional: true,
108+
AttributeTypes: map[string]attr.Type{
109+
"http_path": types.StringType,
110+
"server": types.StringType,
111+
"username": types.StringType,
112+
"password": types.StringType,
113+
"path": types.StringType,
114+
"file_name": types.StringType,
115+
},
116+
},
93117
"clone": schema.ObjectAttribute{
94118
MarkdownDescription: "" +
95119
"Clone options if the VM is being created as a clone. The `source_vm_uuid` is the UUID of the VM used for cloning, <br>" +
@@ -198,6 +222,7 @@ func (r *HypercoreVMResource) Create(ctx context.Context, req resource.CreateReq
198222

199223
tflog.Info(ctx, fmt.Sprintf("TTRT Create: name=%s, source_uuid=%s", data.Name.ValueString(), data.Clone.SourceVMUUID.ValueString()))
200224

225+
// Import or clone
201226
vmClone, _ := utils.NewVM(
202227
data.Name.ValueString(),
203228
data.Clone.SourceVMUUID.ValueString(),
@@ -212,13 +237,42 @@ func (r *HypercoreVMResource) Create(ctx context.Context, req resource.CreateReq
212237
data.AffinityStrategy.PreferredNodeUUID.ValueString(),
213238
data.AffinityStrategy.BackupNodeUUID.ValueString(),
214239
)
215-
changed, msg := vmClone.Create(*r.client, ctx)
216-
tflog.Info(ctx, fmt.Sprintf("Changed: %t, Message: %s\n", changed, msg))
217240

218-
// General parametrization
219-
// set: description, group, vcpu, memory, power_state
220-
changed, vmWasRebooted, vmDiff := vmClone.SetVMParams(*r.client, ctx)
221-
tflog.Info(ctx, fmt.Sprintf("Changed: %t, Was VM Rebooted: %t, Diff: %v", changed, vmWasRebooted, vmDiff))
241+
// http import
242+
httpPath := data.Import.HTTPPath.ValueString()
243+
244+
// smb import
245+
smbServer := data.Import.Server.ValueString()
246+
smbUsername := data.Import.Username.ValueString()
247+
smbPassword := data.Import.Password.ValueString()
248+
smbPath := data.Import.Path.ValueString()
249+
fileName := data.Import.FileName.ValueString()
250+
251+
isSMBImport := smbServer != "" || smbUsername != "" || smbPassword != "" || smbPath != ""
252+
isHTTPImport := httpPath != ""
253+
254+
importedVM := map[string]any{}
255+
if isHTTPImport && !isSMBImport {
256+
utils.ValidateHTTP(httpPath)
257+
httpSource := utils.BuildHTTPImportSource(httpPath, fileName)
258+
importedVM = vmClone.ImportVM(*r.client, httpSource, ctx)
259+
tflog.Debug(ctx, fmt.Sprintf("TTRT Create - with HTTP VM import: vm_uuid=%s, vm=%v", vmClone.UUID, importedVM))
260+
} else if isSMBImport && !isHTTPImport {
261+
utils.ValidateSMB(smbServer, smbUsername, smbPassword, smbPath)
262+
smbSource := utils.BuildSMBImportSource(smbUsername, smbPassword, smbServer, smbPath, fileName)
263+
importedVM = vmClone.ImportVM(*r.client, smbSource, ctx)
264+
tflog.Debug(ctx, fmt.Sprintf("TTRT Create - with SMB VM import: vm_uuid=%s, vm=%v", vmClone.UUID, importedVM))
265+
} else {
266+
// Cloning
267+
changed, msg := vmClone.Create(*r.client, ctx)
268+
tflog.Info(ctx, fmt.Sprintf("Changed: %t, Message: %s\n", changed, msg))
269+
270+
// General parametrization
271+
// set: description, group, vcpu, memory, power_state
272+
changed, vmWasRebooted, vmDiff := vmClone.SetVMParams(*r.client, ctx)
273+
tflog.Info(ctx, fmt.Sprintf("Changed: %t, Was VM Rebooted: %t, Diff: %v", changed, vmWasRebooted, vmDiff))
274+
275+
}
222276

223277
// save into the Terraform state.
224278
data.Id = types.StringValue(vmClone.UUID)

internal/utils/helper.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import (
1212
"net/http"
1313
"os"
1414
"strconv"
15+
"strings"
16+
17+
"github.com/hashicorp/terraform-plugin-framework/diag"
1518
)
1619

1720
func isSuperset(superset map[string]any, candidate map[string]any) bool {
@@ -303,3 +306,42 @@ func GetFileSize(sourceFilePath string) int64 {
303306
}
304307
return fileInfo.Size()
305308
}
309+
310+
func ValidateSMB(server string, username string, password string, path string) diag.Diagnostic {
311+
if server == "" {
312+
return diag.NewErrorDiagnostic(
313+
"Missing 'server' parameter",
314+
"For using SMB, you must specify the 'server' parameter",
315+
)
316+
}
317+
if username == "" {
318+
return diag.NewErrorDiagnostic(
319+
"Missing 'username' parameter",
320+
"For using SMB, you must specify the 'username' parameter",
321+
)
322+
}
323+
if password == "" {
324+
return diag.NewErrorDiagnostic(
325+
"Missing 'password' parameter",
326+
"For using SMB, you must specify the 'password' parameter",
327+
)
328+
}
329+
if path == "" {
330+
return diag.NewErrorDiagnostic(
331+
"Missing 'path' parameter",
332+
"For using SMB, you must specify the 'path' parameter",
333+
)
334+
}
335+
return nil
336+
}
337+
338+
func ValidateHTTP(httpPath string) diag.Diagnostic {
339+
if strings.HasPrefix(httpPath, "http://") || strings.HasPrefix(httpPath, "https://") {
340+
return diag.NewErrorDiagnostic(
341+
"Invalid HTTP path",
342+
"Invalid HTTP path. Path must start with 'http://' or 'https://'",
343+
)
344+
}
345+
346+
return nil
347+
}

internal/utils/vm.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,33 @@ func (vc *VM) Create(restClient RestClient, ctx context.Context) (bool, string)
172172
panic(fmt.Sprintf("There was a problem during cloning of %s %s, cloning failed.", sourceVMName, vc.sourceVMUUID))
173173
}
174174

175+
func (vc *VM) ImportVM(
176+
restClient RestClient,
177+
source map[string]any,
178+
ctx context.Context,
179+
) map[string]any {
180+
payload := map[string]any{
181+
"source": source,
182+
}
183+
184+
importTemplate := vc.BuildImportTemplate()
185+
if len(importTemplate) > 0 {
186+
payload["template"] = importTemplate
187+
}
188+
189+
taskTag, _, _ := restClient.CreateRecord(
190+
"/rest/v1/VirDomain/import",
191+
payload,
192+
-1,
193+
)
194+
taskTag.WaitTask(restClient, ctx)
195+
vmUUID := taskTag.CreatedUUID
196+
vm := GetOneVM(vmUUID, restClient)
197+
198+
vc.UUID = vmUUID
199+
return vm
200+
}
201+
175202
func (vc *VM) SetVMParams(restClient RestClient, ctx context.Context) (bool, bool, map[string]any) {
176203
vm := GetVMByName(vc.VMName, restClient, true)
177204
changed, changedParams := vc.GetChangedParams(ctx, *vm)
@@ -430,6 +457,40 @@ func (vc *VM) BuildUpdatePayload(changedParams map[string]bool) map[string]any {
430457
return updatePayload
431458
}
432459

460+
func (vc *VM) BuildImportTemplate() map[string]any {
461+
importTemplate := map[string]any{}
462+
463+
if vc.description != nil {
464+
importTemplate["description"] = *vc.description
465+
}
466+
if vc.tags != nil {
467+
importTemplate["tags"] = tagsListToCommaString(*vc.tags)
468+
}
469+
if vc.memory != nil {
470+
vcMemoryBytes := *vc.memory * 1024 * 1024 // MB to B
471+
importTemplate["mem"] = vcMemoryBytes
472+
}
473+
if vc.vcpu != nil {
474+
importTemplate["numVCPU"] = *vc.vcpu
475+
}
476+
477+
affinityStrategy := map[string]any{
478+
"strictAffinity": vc.strictAffinity,
479+
}
480+
481+
if vc.preferredNodeUUID != "" {
482+
affinityStrategy["preferredNodeUUID"] = vc.preferredNodeUUID
483+
}
484+
485+
if vc.backupNodeUUID != "" {
486+
affinityStrategy["backupNodeUUID"] = vc.backupNodeUUID
487+
}
488+
489+
importTemplate["affinityStrategy"] = affinityStrategy
490+
491+
return importTemplate
492+
}
493+
433494
func (vc *VM) GetChangedParams(ctx context.Context, vmFromClient map[string]any) (bool, map[string]bool) {
434495
changedParams := map[string]bool{}
435496

@@ -469,6 +530,31 @@ func (vc *VM) GetChangedParams(ctx context.Context, vmFromClient map[string]any)
469530
return false, changedParams
470531
}
471532

533+
func BuildSMBImportSource(username string, password string, server string, path string, fileName string) map[string]any {
534+
pathURI := fmt.Sprintf("smb://%s:%s@%s%s", username, password, server, path)
535+
536+
source := map[string]any{
537+
"pathURI": pathURI,
538+
}
539+
if fileName != "" {
540+
source["definitionFileName"] = fileName
541+
}
542+
543+
return source
544+
}
545+
546+
func BuildHTTPImportSource(httpPath string, fileName string) map[string]any {
547+
source := map[string]any{
548+
"pathURI": httpPath,
549+
}
550+
551+
if fileName != "" {
552+
source["definitionFileName"] = fileName
553+
}
554+
555+
return source
556+
}
557+
472558
func GetOneVM(uuid string, restClient RestClient) map[string]any {
473559
url := "/rest/v1/VirDomain/" + uuid
474560
records := restClient.ListRecords(

local/main.tf

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,49 @@ terraform {
1111

1212
provider "hypercore" {}
1313

14-
data "hypercore_remote_cluster_connection" "all_clusters" {}
14+
locals {
15+
vm_meta_data_tmpl = "./assets/meta-data.ubuntu-22.04.yml.tftpl"
16+
vm_user_data_tmpl = "./assets/user-data.ubuntu-22.04.yml.tftpl"
17+
vm_name = "my-vm"
18+
}
19+
20+
data "hypercore_vm" "clone_source_vm" {
21+
name = "source_vm"
22+
}
23+
24+
# This is what the updated resource will look like with import
25+
resource "hypercore_vm" "import-from-http" {
26+
group = "my-group"
27+
name = local.vm_name
28+
description = "some description"
29+
30+
vcpu = 4
31+
memory = 4096 # MiB
1532

16-
data "hypercore_remote_cluster_connection" "cluster-a" {
17-
remote_cluster_name = "cluster-a"
33+
import = {
34+
http_path = "http://someurl/my-vm"
35+
}
1836
}
1937

20-
output "all_remote_clusters" {
21-
value = jsonencode(data.hypercore_remote_cluster_connection.all_clusters)
38+
resource "hypercore_vm" "import-from-smb" {
39+
group = "my-group"
40+
name = local.vm_name
41+
description = "some description"
42+
43+
vcpu = 4
44+
memory = 4096 # MiB
45+
46+
import = {
47+
server = "server"
48+
username = "username"
49+
password = "password"
50+
path = "path"
51+
file_name = "file_name"
52+
}
2253
}
2354

24-
output "filtered_remote_cluster" {
25-
value = jsonencode(data.hypercore_remote_cluster_connection.cluster-a)
55+
# NOTE: 'clone' parameter can still be defined along with 'import', but in THIS case, 'import' will be the one taking effect, not clone
56+
57+
output "vm_uuid" {
58+
value = hypercore_vm.myvm.id
2659
}

0 commit comments

Comments
 (0)