diff --git a/internal/provider/hypercore_vm_resource.go b/internal/provider/hypercore_vm_resource.go index 13fc534..4b0a332 100644 --- a/internal/provider/hypercore_vm_resource.go +++ b/internal/provider/hypercore_vm_resource.go @@ -39,12 +39,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"` 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"` @@ -95,6 +105,19 @@ func (r *HypercoreVMResource) Schema(ctx context.Context, req resource.SchemaReq MarkdownDescription: "UUID of the snapshot schedule to create automatic snapshots", Optional: true, }, + "import": schema.ObjectAttribute{ + 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. Paramaters path and file_name are always **required**", + Optional: true, + AttributeTypes: map[string]attr.Type{ + "http_uri": types.StringType, + "server": types.StringType, + "username": types.StringType, + "password": types.StringType, + "path": types.StringType, + "file_name": types.StringType, + }, + }, "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,
" + @@ -203,6 +226,7 @@ func (r *HypercoreVMResource) Create(ctx context.Context, req resource.CreateReq tflog.Info(ctx, fmt.Sprintf("TTRT Create: name=%s, source_uuid=%s", data.Name.ValueString(), data.Clone.SourceVMUUID.ValueString())) + // Import or clone vmClone, _ := utils.NewVM( data.Name.ValueString(), data.Clone.SourceVMUUID.ValueString(), @@ -218,13 +242,48 @@ func (r *HypercoreVMResource) Create(ctx context.Context, req resource.CreateReq 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)) + // http import + httpUri := data.Import.HTTPUri.ValueString() + + // smb import + smbServer := data.Import.Server.ValueString() + smbUsername := data.Import.Username.ValueString() + smbPassword := data.Import.Password.ValueString() + + // location + path := data.Import.Path.ValueString() + fileName := data.Import.FileName.ValueString() + + isSMBImport := smbServer != "" || smbUsername != "" || smbPassword != "" + isHTTPImport := httpUri != "" + + if isHTTPImport && !isSMBImport { + nameDiag := utils.ValidateHTTP(httpUri, path) + if nameDiag != nil { + resp.Diagnostics.AddError(nameDiag.Summary(), nameDiag.Detail()) + return + } + httpSource := utils.BuildHTTPImportSource(httpUri, path, fileName) + vmClone.ImportVM(*r.client, httpSource, ctx) + } else if isSMBImport && !isHTTPImport { + nameDiag := utils.ValidateSMB(smbServer, smbUsername, smbPassword, path) + if nameDiag != nil { + resp.Diagnostics.AddError(nameDiag.Summary(), nameDiag.Detail()) + return + } + smbSource := utils.BuildSMBImportSource(smbUsername, smbPassword, smbServer, path, fileName) + vmClone.ImportVM(*r.client, smbSource, ctx) + } else { + // Cloning + 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)) + } // save into the Terraform state. data.Id = types.StringValue(vmClone.UUID) diff --git a/internal/utils/helper.go b/internal/utils/helper.go index 25f3de5..9235319 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,48 @@ func GetFileSize(sourceFilePath string) int64 { } return fileInfo.Size() } + +func ValidateSMB(server string, username string, password string, path 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", + ) + } + if path == "" { + return diag.NewErrorDiagnostic( + "Missing 'path' parameter", + "For using SMB, you must specify the 'path' 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..1f636c7 100644 --- a/internal/utils/vm.go +++ b/internal/utils/vm.go @@ -175,6 +175,33 @@ 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) ImportVM( + restClient RestClient, + source map[string]any, + ctx context.Context, +) map[string]any { + 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, + ) + taskTag.WaitTask(restClient, ctx) + vmUUID := taskTag.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,6 +463,40 @@ 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 + } + + 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 (vc *VM) GetChangedParams(ctx context.Context, vmFromClient map[string]any) (bool, map[string]bool) { changedParams := map[string]bool{} @@ -478,6 +539,32 @@ func (vc *VM) GetChangedParams(ctx context.Context, vmFromClient map[string]any) return false, changedParams } +func BuildSMBImportSource(username string, password string, server string, path string, fileName string) map[string]any { + pathURI := fmt.Sprintf("smb://%s:%s@%s%s", username, password, server, path) + + source := map[string]any{ + "pathURI": pathURI, + } + if fileName != "" { + source["definitionFileName"] = fileName + } + + return source +} + +func BuildHTTPImportSource(httpUri string, path string, fileName string) map[string]any { + pathURI := fmt.Sprintf("%s%s", httpUri, path) + source := map[string]any{ + "pathURI": pathURI, + } + + if fileName != "" { + source["definitionFileName"] = fileName + } + + return source +} + func GetOneVM(uuid string, restClient RestClient) map[string]any { url := "/rest/v1/VirDomain/" + uuid records := restClient.ListRecords( @@ -503,7 +590,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 +605,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, ) } diff --git a/local/main.tf b/local/main.tf index 5c6bdc9..139802c 100644 --- a/local/main.tf +++ b/local/main.tf @@ -11,16 +11,49 @@ terraform { provider "hypercore" {} -data "hypercore_remote_cluster_connection" "all_clusters" {} +locals { + vm_meta_data_tmpl = "./assets/meta-data.ubuntu-22.04.yml.tftpl" + vm_user_data_tmpl = "./assets/user-data.ubuntu-22.04.yml.tftpl" + vm_name = "my-vm" +} + +data "hypercore_vm" "clone_source_vm" { + name = "source_vm" +} + +# This is what the updated resource will look like with import +resource "hypercore_vm" "import-from-http" { + group = "my-group" + name = local.vm_name + description = "some description" + + vcpu = 4 + memory = 4096 # MiB -data "hypercore_remote_cluster_connection" "cluster-a" { - remote_cluster_name = "cluster-a" + import = { + http_path = "http://someurl/my-vm" + } } -output "all_remote_clusters" { - value = jsonencode(data.hypercore_remote_cluster_connection.all_clusters) +resource "hypercore_vm" "import-from-smb" { + group = "my-group" + name = local.vm_name + description = "some description" + + vcpu = 4 + memory = 4096 # MiB + + import = { + server = "server" + username = "username" + password = "password" + path = "path" + file_name = "file_name" + } } -output "filtered_remote_cluster" { - value = jsonencode(data.hypercore_remote_cluster_connection.cluster-a) +# NOTE: 'clone' parameter can still be defined along with 'import', but in THIS case, 'import' will be the one taking effect, not clone + +output "vm_uuid" { + value = hypercore_vm.myvm.id }