From 487c82711a55e7d284d19ae00bd86277effa523c Mon Sep 17 00:00:00 2001 From: TobiPeterG Date: Thu, 5 Sep 2024 15:43:03 +0200 Subject: [PATCH] Add Virtual Machine data source and resource --- .../provider/data_source_virtual_machine.go | 259 ++++++++ internal/provider/patch.go | 9 + internal/provider/provider.go | 16 +- internal/provider/resource_cluster.go | 18 +- internal/provider/resource_cluster_type.go | 14 +- internal/provider/resource_virtual_machine.go | 620 ++++++++++++++++++ test/main.tf | 24 + test/output.tf | 12 + 8 files changed, 959 insertions(+), 13 deletions(-) create mode 100644 internal/provider/data_source_virtual_machine.go create mode 100644 internal/provider/resource_virtual_machine.go diff --git a/internal/provider/data_source_virtual_machine.go b/internal/provider/data_source_virtual_machine.go new file mode 100644 index 0000000..b4c58fd --- /dev/null +++ b/internal/provider/data_source_virtual_machine.go @@ -0,0 +1,259 @@ +package provider + +import ( + "context" + "fmt" + "strconv" + "time" + + nb "github.com/TobiPeterG/go-nautobot" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceVirtualMachine() *schema.Resource { + return &schema.Resource{ + Description: "Retrieves information about virtual machines in Nautobot.", + + ReadContext: dataSourceVirtualMachineRead, + + Schema: map[string]*schema.Schema{ + "virtual_machines": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Description: "The UUID of the virtual machine.", + Type: schema.TypeString, + Computed: true, + }, + "name": { + Description: "The name of the virtual machine.", + Type: schema.TypeString, + Computed: true, + }, + "cluster_id": { + Description: "The ID of the cluster associated with the virtual machine.", + Type: schema.TypeString, + Computed: true, + }, + "status": { + Description: "The name of the status of the virtual machine.", + Type: schema.TypeString, + Computed: true, + }, + "tenant_id": { + Description: "The ID of the tenant associated with the virtual machine.", + Type: schema.TypeString, + Computed: true, + }, + "platform_id": { + Description: "The ID of the platform associated with the virtual machine.", + Type: schema.TypeString, + Computed: true, + }, + "role_id": { + Description: "The ID of the role associated with the virtual machine.", + Type: schema.TypeString, + Computed: true, + }, + "primary_ip4_id": { + Description: "The ID of the primary IPv4 address.", + Type: schema.TypeString, + Computed: true, + }, + "primary_ip6_id": { + Description: "The ID of the primary IPv6 address.", + Type: schema.TypeString, + Computed: true, + }, + "vcpus": { + Description: "The number of virtual CPUs.", + Type: schema.TypeInt, + Computed: true, + }, + "memory": { + Description: "The amount of memory in MB.", + Type: schema.TypeInt, + Computed: true, + }, + "disk": { + Description: "The disk size in GB.", + Type: schema.TypeInt, + Computed: true, + }, + "comments": { + Description: "Comments or notes about the virtual machine.", + Type: schema.TypeString, + Computed: true, + }, + "tags_ids": { + Description: "The IDs of the tags associated with the virtual machine.", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "custom_fields": { + Description: "Custom fields associated with the virtual machine.", + Type: schema.TypeMap, + Computed: true, + }, + "created": { + Description: "The creation date of the virtual machine.", + Type: schema.TypeString, + Computed: true, + }, + "last_updated": { + Description: "The last update date of the virtual machine.", + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func getStatusName(ctx context.Context, c *nb.APIClient, token string, statusID string) (string, error) { + auth := context.WithValue( + ctx, + nb.ContextAPIKeys, + map[string]nb.APIKey{ + "tokenAuth": { + Key: token, + Prefix: "Token", + }, + }, + ) + + // Fetch the status using the status ID + status, _, err := c.ExtrasAPI.ExtrasStatusesRetrieve(auth, statusID).Execute() + if err != nil { + return "", err + } + + // No need to dereference, just check if the string is empty + if status.Name != "" { + return status.Name, nil + } + + return "", fmt.Errorf("status name not found for ID %s", statusID) +} + +func dataSourceVirtualMachineRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + c := meta.(*apiClient).Client + s := meta.(*apiClient).Server + t := meta.(*apiClient).Token.token + + // Auth context + auth := context.WithValue( + ctx, + nb.ContextAPIKeys, + map[string]nb.APIKey{ + "tokenAuth": { + Key: t, + Prefix: "Token", + }, + }, + ) + + // Fetch virtual machines list + rsp, _, err := c.VirtualizationAPI.VirtualizationVirtualMachinesList(auth).Execute() + if err != nil { + return diag.Errorf("failed to get virtual machines list from %s: %s", s, err.Error()) + } + + results := rsp.Results + list := make([]map[string]interface{}, 0) + + for _, vm := range results { + createdStr := "" + if vm.Created.IsSet() && vm.Created.Get() != nil { + createdStr = vm.Created.Get().Format(time.RFC3339) + } + + lastUpdatedStr := "" + if vm.LastUpdated.IsSet() && vm.LastUpdated.Get() != nil { + lastUpdatedStr = vm.LastUpdated.Get().Format(time.RFC3339) + } + + itemMap := map[string]interface{}{ + "id": vm.Id, + "name": vm.Name, + "vcpus": vm.Vcpus.Get(), + "memory": vm.Memory.Get(), + "disk": vm.Disk.Get(), + "comments": vm.Comments, + "created": createdStr, + "last_updated": lastUpdatedStr, + "custom_fields": vm.CustomFields, + } + + // Extract cluster_id, status, and other fields + if vm.Cluster.Id != nil && vm.Cluster.Id.String != nil { + itemMap["cluster_id"] = *vm.Cluster.Id.String + } + + if vm.Status.Id != nil && vm.Status.Id.String != nil { + statusID := *vm.Status.Id.String + statusName, err := getStatusName(ctx, c, t, statusID) + if err != nil { + return diag.Errorf("failed to get status name for ID %s: %s", statusID, err.Error()) + } + itemMap["status"] = statusName + } + + // Handle nullable fields (tenant, platform, role, etc.) + if vm.Tenant.IsSet() { + tenant := vm.Tenant.Get() + if tenant != nil && tenant.Id != nil { + itemMap["tenant_id"] = *tenant.Id.String + } + } + + if vm.Platform.IsSet() { + platform := vm.Platform.Get() + if platform != nil && platform.Id != nil { + itemMap["platform_id"] = *platform.Id.String + } + } + + if vm.Role.IsSet() { + role := vm.Role.Get() + if role != nil && role.Id != nil { + itemMap["role_id"] = *role.Id.String + } + } + + if vm.PrimaryIp4.IsSet() { + primaryIp4 := vm.PrimaryIp4.Get() + if primaryIp4 != nil && primaryIp4.Id != nil { + itemMap["primary_ip4_id"] = *primaryIp4.Id.String + } + } + + if vm.PrimaryIp6.IsSet() { + primaryIp6 := vm.PrimaryIp6.Get() + if primaryIp6 != nil && primaryIp6.Id != nil { + itemMap["primary_ip6_id"] = *primaryIp6.Id.String + } + } + + list = append(list, itemMap) + } + + if err := d.Set("virtual_machines", list); err != nil { + return diag.FromErr(err) + } + + // Set ID for the data source + d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) + + return diags +} diff --git a/internal/provider/patch.go b/internal/provider/patch.go index 44257c8..d51eeac 100644 --- a/internal/provider/patch.go +++ b/internal/provider/patch.go @@ -20,3 +20,12 @@ func (s *SecurityProviderNautobotToken) Intercept(ctx context.Context, req *http req.Header.Set("Authorization", fmt.Sprintf("Token %s", s.token)) return nil } + +func stringPtr(s string) *string { + return &s +} + +func int32Ptr(i int) *int32 { + val := int32(i) + return &val +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 918b243..608e053 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -52,15 +52,17 @@ func New(version string) func() *schema.Provider { }, }, DataSourcesMap: map[string]*schema.Resource{ - "nautobot_manufacturers": dataSourceManufacturers(), - "nautobot_graphql": dataSourceGraphQL(), - "nautobot_cluster_type": dataSourceClusterType(), - "nautobot_cluster": dataSourceCluster(), + "nautobot_manufacturers": dataSourceManufacturers(), + "nautobot_graphql": dataSourceGraphQL(), + "nautobot_cluster_type": dataSourceClusterType(), + "nautobot_cluster": dataSourceCluster(), + "nautobot_virtual_machine": dataSourceVirtualMachine(), }, ResourcesMap: map[string]*schema.Resource{ - "nautobot_manufacturer": resourceManufacturer(), - "nautobot_cluster_type": resourceClusterType(), - "nautobot_cluster": resourceCluster(), + "nautobot_manufacturer": resourceManufacturer(), + "nautobot_cluster_type": resourceClusterType(), + "nautobot_cluster": resourceCluster(), + "nautobot_virtual_machine": resourceVirtualMachine(), }, } diff --git a/internal/provider/resource_cluster.go b/internal/provider/resource_cluster.go index c216c44..0527017 100644 --- a/internal/provider/resource_cluster.go +++ b/internal/provider/resource_cluster.go @@ -75,10 +75,6 @@ func resourceCluster() *schema.Resource { } } -func stringPtr(s string) *string { - return &s -} - func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { c := meta.(*apiClient).Client t := meta.(*apiClient).Token.token @@ -95,9 +91,21 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta int }, ) + clusterName := d.Get("name").(string) + existingClusters, _, err := c.VirtualizationAPI.VirtualizationClustersList(auth).Name([]string{clusterName}).Execute() + if err != nil { + return diag.Errorf("failed to list clusters: %s", err.Error()) + } + + // If a cluster with the same name exists, use its ID and skip creation + if len(existingClusters.Results) > 0 { + d.SetId(existingClusters.Results[0].Id) + return resourceClusterRead(ctx, d, meta) + } + // Prepare ClusterRequest var cluster nb.ClusterRequest - cluster.Name = d.Get("name").(string) + cluster.Name = clusterName cluster.ClusterType = nb.BulkWritableCableRequestStatus{ Id: &nb.BulkWritableCableRequestStatusId{ String: stringPtr(d.Get("cluster_type_id").(string)), diff --git a/internal/provider/resource_cluster_type.go b/internal/provider/resource_cluster_type.go index e1514ee..0c84fda 100644 --- a/internal/provider/resource_cluster_type.go +++ b/internal/provider/resource_cluster_type.go @@ -98,9 +98,21 @@ func resourceClusterTypeCreate(ctx context.Context, d *schema.ResourceData, meta }, ) + clusterTypeName := d.Get("name").(string) + existingClusterTypes, _, err := c.VirtualizationAPI.VirtualizationClusterTypesList(auth).Name([]string{clusterTypeName}).Execute() + if err != nil { + return diag.Errorf("failed to list cluster types: %s", err.Error()) + } + + // If a cluster type with the same name exists, use its ID and skip creation + if len(existingClusterTypes.Results) > 0 { + d.SetId(existingClusterTypes.Results[0].Id) + return resourceClusterTypeRead(ctx, d, meta) + } + // Prepare ClusterTypeRequest var clusterType nb.ClusterTypeRequest - clusterType.Name = d.Get("name").(string) + clusterType.Name = clusterTypeName if v, ok := d.GetOk("description"); ok { description := v.(string) diff --git a/internal/provider/resource_virtual_machine.go b/internal/provider/resource_virtual_machine.go new file mode 100644 index 0000000..03a891f --- /dev/null +++ b/internal/provider/resource_virtual_machine.go @@ -0,0 +1,620 @@ +package provider + +import ( + "context" + "fmt" + + nb "github.com/TobiPeterG/go-nautobot" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceVirtualMachine() *schema.Resource { + return &schema.Resource{ + Description: "This object manages a virtual machine in Nautobot", + + CreateContext: resourceVirtualMachineCreate, + ReadContext: resourceVirtualMachineRead, + UpdateContext: resourceVirtualMachineUpdate, + DeleteContext: resourceVirtualMachineDelete, + + Schema: map[string]*schema.Schema{ + "name": { + Description: "Virtual Machine's name.", + Type: schema.TypeString, + Required: true, + }, + "cluster_id": { + Description: "Cluster where the virtual machine belongs.", + Type: schema.TypeString, + Required: true, + }, + "status": { + Description: "Status of the virtual machine.", + Type: schema.TypeString, + Required: true, + }, + "vcpus": { + Description: "Number of virtual CPUs.", + Type: schema.TypeInt, + Optional: true, + }, + "memory": { + Description: "Amount of memory in MB.", + Type: schema.TypeInt, + Optional: true, + }, + "disk": { + Description: "Disk size in GB.", + Type: schema.TypeInt, + Optional: true, + }, + "comments": { + Description: "Comments or notes about the virtual machine.", + Type: schema.TypeString, + Optional: true, + }, + "tenant_id": { + Description: "Tenant associated with the virtual machine.", + Type: schema.TypeString, + Optional: true, + }, + "platform_id": { + Description: "Platform or OS installed on the virtual machine.", + Type: schema.TypeString, + Optional: true, + }, + "role_id": { + Description: "Role of the virtual machine.", + Type: schema.TypeString, + Optional: true, + }, + "primary_ip4_id": { + Description: "Primary IPv4 address.", + Type: schema.TypeString, + Optional: true, + }, + "primary_ip6_id": { + Description: "Primary IPv6 address.", + Type: schema.TypeString, + Optional: true, + }, + "software_version_id": { + Description: "Software version installed on the virtual machine.", + Type: schema.TypeString, + Optional: true, + }, + "software_image_files": { + Description: "Software image files associated with the software version.", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "custom_fields": { + Description: "Custom fields associated with the virtual machine.", + Type: schema.TypeMap, + Optional: true, + }, + "tags_ids": { + Description: "Tags associated with the virtual machine.", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "created": { + Description: "Creation date of the virtual machine.", + Type: schema.TypeString, + Computed: true, + }, + "last_updated": { + Description: "Last update date of the virtual machine.", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func getStatusID(ctx context.Context, c *nb.APIClient, token string, statusName string) (string, error) { + auth := context.WithValue( + ctx, + nb.ContextAPIKeys, + map[string]nb.APIKey{ + "tokenAuth": { + Key: token, + Prefix: "Token", + }, + }, + ) + + statuses, _, err := c.ExtrasAPI.ExtrasStatusesList(auth).Name([]string{statusName}).Execute() + if err != nil { + return "", err + } + + if len(statuses.Results) == 0 { + return "", fmt.Errorf("status %s not found", statusName) + } + + return statuses.Results[0].Id, nil +} + +func resourceVirtualMachineCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + c := meta.(*apiClient).Client + t := meta.(*apiClient).Token.token + + // Auth context + auth := context.WithValue( + ctx, + nb.ContextAPIKeys, + map[string]nb.APIKey{ + "tokenAuth": { + Key: t, + Prefix: "Token", + }, + }, + ) + + // Check if the VM with the same name exists + vmName := d.Get("name").(string) + existingVMs, _, err := c.VirtualizationAPI.VirtualizationVirtualMachinesList(auth).Name([]string{vmName}).Execute() + if err != nil { + return diag.Errorf("failed to list virtual machines: %s", err.Error()) + } + + // If a VM with the same name exists, use its ID and skip creation + if len(existingVMs.Results) > 0 { + d.SetId(existingVMs.Results[0].Id) + return resourceVirtualMachineRead(ctx, d, meta) + } + + // Convert status name to ID + statusName := d.Get("status").(string) + statusID, err := getStatusID(ctx, c, t, statusName) + if err != nil { + return diag.Errorf("failed to get status ID for %s: %s", statusName, err.Error()) + } + + // Prepare the VirtualMachineRequest + var vm nb.VirtualMachineRequest + vm.Name = d.Get("name").(string) + vm.Cluster = nb.BulkWritableCableRequestStatus{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(d.Get("cluster_id").(string)), + }, + } + vm.Status = nb.BulkWritableCableRequestStatus{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(statusID), + }, + } + + // Optional fields + if v, ok := d.GetOk("vcpus"); ok { + vm.Vcpus.Set(int32Ptr(v.(int))) + } + if v, ok := d.GetOk("memory"); ok { + vm.Memory.Set(int32Ptr(v.(int))) + } + if v, ok := d.GetOk("disk"); ok { + vm.Disk.Set(int32Ptr(v.(int))) + } + if v, ok := d.GetOk("comments"); ok { + comments := v.(string) + vm.Comments = &comments + } + if v, ok := d.GetOk("tenant_id"); ok { + tenant := v.(string) + var nullableTenant nb.NullableBulkWritableCircuitRequestTenant + nullableTenant.Set(&nb.BulkWritableCircuitRequestTenant{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(tenant), + }, + }) + vm.Tenant = nullableTenant + } + if v, ok := d.GetOk("platform_id"); ok { + platform := v.(string) + var nullablePlatform nb.NullableBulkWritableCircuitRequestTenant + nullablePlatform.Set(&nb.BulkWritableCircuitRequestTenant{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(platform), + }, + }) + vm.Platform = nullablePlatform + } + + if v, ok := d.GetOk("role_id"); ok { + role := v.(string) + var nullableRole nb.NullableBulkWritableCircuitRequestTenant + nullableRole.Set(&nb.BulkWritableCircuitRequestTenant{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(role), + }, + }) + vm.Role = nullableRole + } + + if v, ok := d.GetOk("primary_ip4_id"); ok { + ip4 := v.(string) + var nullableIP4 nb.NullablePrimaryIPv4 + primaryIPv4 := &nb.PrimaryIPv4{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(ip4), + }, + } + nullableIP4.Set(primaryIPv4) + vm.PrimaryIp4 = nullableIP4 + } + + if v, ok := d.GetOk("primary_ip6_id"); ok { + ip6 := v.(string) + var nullableIP6 nb.NullablePrimaryIPv6 + primaryIPv6 := &nb.PrimaryIPv6{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(ip6), + }, + } + nullableIP6.Set(primaryIPv6) + vm.PrimaryIp6 = nullableIP6 + } + + if v, ok := d.GetOk("software_version_id"); ok { + softwareVersion := v.(string) + var nullableSoftwareVersion nb.NullableBulkWritableVirtualMachineRequestSoftwareVersion + softwareVersionStruct := &nb.BulkWritableVirtualMachineRequestSoftwareVersion{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(softwareVersion), + }, + } + nullableSoftwareVersion.Set(softwareVersionStruct) + vm.SoftwareVersion = nullableSoftwareVersion + } + + if v, ok := d.GetOk("software_image_files"); ok { + var files []nb.SoftwareImageFiles + for _, file := range v.([]interface{}) { + fileData := file.(map[string]interface{}) + files = append(files, nb.SoftwareImageFiles{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(fileData["id"].(string)), + }, + }) + } + vm.SoftwareImageFiles = files + } + if v, ok := d.GetOk("custom_fields"); ok { + vm.CustomFields = v.(map[string]interface{}) + } + if v, ok := d.GetOk("tags_ids"); ok { + var tags []nb.BulkWritableCableRequestStatus + for _, tag := range v.([]interface{}) { + tags = append(tags, nb.BulkWritableCableRequestStatus{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(tag.(string)), + }, + }) + } + vm.Tags = tags + } + + // Create the virtual machine + rsp, _, err := c.VirtualizationAPI.VirtualizationVirtualMachinesCreate(auth).VirtualMachineRequest(vm).Execute() + if err != nil { + return diag.Errorf("failed to create virtual machine: %s", err.Error()) + } + + // Set resource ID + d.SetId(rsp.Id) + + return resourceVirtualMachineRead(ctx, d, meta) +} + +func resourceVirtualMachineRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + c := meta.(*apiClient).Client + t := meta.(*apiClient).Token.token + + // Auth context + auth := context.WithValue( + ctx, + nb.ContextAPIKeys, + map[string]nb.APIKey{ + "tokenAuth": { + Key: t, + Prefix: "Token", + }, + }, + ) + + // Fetch virtual machine by ID + vmId := d.Id() + vm, _, err := c.VirtualizationAPI.VirtualizationVirtualMachinesRetrieve(auth, vmId).Execute() + if err != nil { + return diag.Errorf("failed to read virtual machine: %s", err.Error()) + } + + // Map the retrieved data back to Terraform state + d.Set("name", vm.Name) + d.Set("cluster_id", vm.Cluster.Id) + d.Set("status", vm.Status.Id) + d.Set("vcpus", vm.Vcpus.Get()) + d.Set("memory", vm.Memory.Get()) + d.Set("disk", vm.Disk.Get()) + d.Set("comments", vm.Comments) + + // Handle nullable fields using IsSet and Get methods + if vm.Tenant.IsSet() { + tenant := vm.Tenant.Get() + if tenant != nil && tenant.Id != nil { + d.Set("tenant_id", *tenant.Id.String) + } + } + + if vm.Platform.IsSet() { + platform := vm.Platform.Get() + if platform != nil && platform.Id != nil { + d.Set("platform_id", *platform.Id.String) + } + } + + if vm.Role.IsSet() { + role := vm.Role.Get() + if role != nil && role.Id != nil { + d.Set("role_id", *role.Id.String) + } + } + + if vm.PrimaryIp4.IsSet() { + primaryIp4 := vm.PrimaryIp4.Get() + if primaryIp4 != nil && primaryIp4.Id != nil { + d.Set("primary_ip4_id", *primaryIp4.Id.String) + } + } + + if vm.PrimaryIp6.IsSet() { + primaryIp6 := vm.PrimaryIp6.Get() + if primaryIp6 != nil && primaryIp6.Id != nil { + d.Set("primary_ip6_id", *primaryIp6.Id.String) + } + } + + if vm.SoftwareVersion.IsSet() { + softwareVersion := vm.SoftwareVersion.Get() + if softwareVersion != nil && softwareVersion.Id != nil { + d.Set("software_version_id", *softwareVersion.Id.String) + } + } + + var imageFiles []map[string]string + for _, file := range vm.SoftwareImageFiles { + if file.Id != nil && file.Id.String != nil { + imageFiles = append(imageFiles, map[string]string{ + "id": *file.Id.String, + }) + } + } + + d.Set("software_image_files", imageFiles) + + d.Set("custom_fields", vm.CustomFields) + + var tags []string + for _, tag := range vm.Tags { + if tag.Id != nil { + tags = append(tags, *tag.Id.String) + } + } + d.Set("tags_ids", tags) + + d.Set("created", vm.Created) + d.Set("last_updated", vm.LastUpdated) + + return nil +} + +func resourceVirtualMachineUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + c := meta.(*apiClient).Client + t := meta.(*apiClient).Token.token + + vmId := d.Id() + + var vm nb.PatchedVirtualMachineRequest + + // Auth context + auth := context.WithValue( + ctx, + nb.ContextAPIKeys, + map[string]nb.APIKey{ + "tokenAuth": { + Key: t, + Prefix: "Token", + }, + }, + ) + + // Update the fields that have changed + if d.HasChange("name") { + name := d.Get("name").(string) + vm.Name = &name + } + + if d.HasChange("cluster_id") { + clusterID := d.Get("cluster_id").(string) + vm.Cluster = &nb.BulkWritableCableRequestStatus{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: &clusterID, + }, + } + } + + if d.HasChange("status") { + statusName := d.Get("status").(string) + statusID, err := getStatusID(ctx, c, t, statusName) + if err != nil { + return diag.Errorf("failed to get status ID for %s: %s", statusName, err.Error()) + } + vm.Status = &nb.BulkWritableCableRequestStatus{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(statusID), + }, + } + } + + // Optional fields + if d.HasChange("vcpus") { + vm.Vcpus.Set(int32Ptr(d.Get("vcpus").(int))) + } + if d.HasChange("memory") { + vm.Memory.Set(int32Ptr(d.Get("memory").(int))) + } + if d.HasChange("disk") { + vm.Disk.Set(int32Ptr(d.Get("disk").(int))) + } + if d.HasChange("comments") { + comments := d.Get("comments").(string) + vm.Comments = &comments + } + if d.HasChange("tenant_id") { + tenant := d.Get("tenant_id").(string) + var nullableTenant nb.NullableBulkWritableCircuitRequestTenant + nullableTenant.Set(&nb.BulkWritableCircuitRequestTenant{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(tenant), + }, + }) + vm.Tenant = nullableTenant + } + if d.HasChange("platform_id") { + platform := d.Get("platform_id").(string) + var nullablePlatform nb.NullableBulkWritableCircuitRequestTenant + nullablePlatform.Set(&nb.BulkWritableCircuitRequestTenant{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(platform), + }, + }) + vm.Platform = nullablePlatform + } + + if d.HasChange("role_id") { + role := d.Get("role_id").(string) + var nullableRole nb.NullableBulkWritableCircuitRequestTenant + nullableRole.Set(&nb.BulkWritableCircuitRequestTenant{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(role), + }, + }) + vm.Role = nullableRole + } + + if d.HasChange("primary_ip4_id") { + ip4 := d.Get("primary_ip4_id").(string) + var nullableIP4 nb.NullablePrimaryIPv4 + primaryIPv4 := &nb.PrimaryIPv4{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(ip4), + }, + } + nullableIP4.Set(primaryIPv4) + vm.PrimaryIp4 = nullableIP4 + } + + if d.HasChange("primary_ip6_id") { + ip6 := d.Get("primary_ip6_id").(string) + var nullableIP6 nb.NullablePrimaryIPv6 + primaryIPv6 := &nb.PrimaryIPv6{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(ip6), + }, + } + nullableIP6.Set(primaryIPv6) + vm.PrimaryIp6 = nullableIP6 + } + + if d.HasChange("software_version_id") { + softwareVersion := d.Get("software_version_id").(string) + var nullableSoftwareVersion nb.NullableBulkWritableVirtualMachineRequestSoftwareVersion + softwareVersionStruct := &nb.BulkWritableVirtualMachineRequestSoftwareVersion{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(softwareVersion), + }, + } + nullableSoftwareVersion.Set(softwareVersionStruct) + vm.SoftwareVersion = nullableSoftwareVersion + } + + if d.HasChange("software_image_files") { + var files []nb.SoftwareImageFiles + for _, file := range d.Get("software_image_files").([]interface{}) { + fileData := file.(map[string]interface{}) + files = append(files, nb.SoftwareImageFiles{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: stringPtr(fileData["id"].(string)), + }, + }) + } + vm.SoftwareImageFiles = files + } + + if d.HasChange("custom_fields") { + vm.CustomFields = d.Get("custom_fields").(map[string]interface{}) + } + + if d.HasChange("tags_ids") { + var tags []nb.BulkWritableCableRequestStatus + for _, tag := range d.Get("tags_ids").([]interface{}) { + tagID := tag.(string) + tags = append(tags, nb.BulkWritableCableRequestStatus{ + Id: &nb.BulkWritableCableRequestStatusId{ + String: &tagID, + }, + }) + } + vm.Tags = tags + } + + // Call the API to update the virtual machine + _, _, err := c.VirtualizationAPI.VirtualizationVirtualMachinesPartialUpdate(auth, vmId).PatchedVirtualMachineRequest(vm).Execute() + if err != nil { + return diag.Errorf("failed to update virtual machine: %s", err.Error()) + } + + return resourceVirtualMachineRead(ctx, d, meta) +} + +func resourceVirtualMachineDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + c := meta.(*apiClient).Client + t := meta.(*apiClient).Token.token + + // Auth context + auth := context.WithValue( + ctx, + nb.ContextAPIKeys, + map[string]nb.APIKey{ + "tokenAuth": { + Key: t, + Prefix: "Token", + }, + }, + ) + + // Delete the virtual machine by ID + vmId := d.Id() + _, err := c.VirtualizationAPI.VirtualizationVirtualMachinesDestroy(auth, vmId).Execute() + if err != nil { + return diag.Errorf("failed to delete virtual machine: %s", err.Error()) + } + + // Clear the ID + d.SetId("") + + return nil +} diff --git a/test/main.tf b/test/main.tf index 99c5a87..a4cf36d 100644 --- a/test/main.tf +++ b/test/main.tf @@ -37,4 +37,28 @@ resource "nautobot_cluster" "new" { "custom_field_1" = "value1" "custom_field_2" = "value2" } +} + +# Example virtual machine resource +resource "nautobot_virtual_machine" "new" { + name = "Example VM" + cluster_id = nautobot_cluster.new.id + status = "Active" + vcpus = 4 + memory = 8192 # Memory in MB (8GB) + disk = 100 # Disk size in GB + comments = "This virtual machine was created using Terraform." +# tenant_id = "some-tenant-id" # Optional +# platform_id = "Linux" # Optional +# role_id = "Web Server" # Optional +# primary_ip4_id = "192.168.0.100" # Optional +# primary_ip6_id = "2001:db8::100" # Optional +# software_version_id = "v1.0" # Optional + + custom_fields = { + custom_field_1 = "Custom value 1" + custom_field_2 = "Custom value 2" + } + +# tags_ids = ["tag1", "tag2"] # Optional tags } \ No newline at end of file diff --git a/test/output.tf b/test/output.tf index 70ddbe3..fba291a 100644 --- a/test/output.tf +++ b/test/output.tf @@ -59,4 +59,16 @@ output "cluster_details" { output "cluster_id" { value = data.nautobot_cluster.example.clusters[0].id +} + +data "nautobot_virtual_machine" "example" { + depends_on = [nautobot_virtual_machine.new] +} + +output "vm_details" { + value = data.nautobot_virtual_machine.example.virtual_machines[0] +} + +output "vm_id" { + value = data.nautobot_virtual_machine.example.virtual_machines[0].id } \ No newline at end of file