Skip to content

Commit

Permalink
Add Virtual Machine data source and resource
Browse files Browse the repository at this point in the history
  • Loading branch information
TobiPeterG committed Sep 5, 2024
1 parent fa7ddd4 commit c94a0e0
Show file tree
Hide file tree
Showing 8 changed files with 957 additions and 13 deletions.
259 changes: 259 additions & 0 deletions internal/provider/data_source_virtual_machine.go
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 9 additions & 0 deletions internal/provider/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
16 changes: 9 additions & 7 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
},
}

Expand Down
17 changes: 12 additions & 5 deletions internal/provider/resource_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -95,9 +91,20 @@ 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.ClusterType = nb.BulkWritableCableRequestStatus{
Id: &nb.BulkWritableCableRequestStatusId{
String: stringPtr(d.Get("cluster_type_id").(string)),
Expand Down
13 changes: 12 additions & 1 deletion internal/provider/resource_cluster_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,20 @@ 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)

if v, ok := d.GetOk("description"); ok {
description := v.(string)
Expand Down
Loading

0 comments on commit c94a0e0

Please sign in to comment.