diff --git a/docs/resources/virtualization_vm.md b/docs/resources/virtualization_vm.md index 53460765b..df971318b 100644 --- a/docs/resources/virtualization_vm.md +++ b/docs/resources/virtualization_vm.md @@ -16,6 +16,15 @@ resource "netbox_virtualization_vm" "vm_test" { name = "tag1" slug = "tag1" } + + custom_fields = { + cf_boolean = "true" + cf_date = "2020-12-25" + cf_integer = "10" + cf_selection = "1" + cf_text = "Some text" + cf_url = "https://github.com" + } } ``` @@ -36,6 +45,13 @@ The following arguments are supported: The ``tag`` block supports: * ``name`` - (Required) Name of the existing tag to associate with this resource. * ``slug`` - (Required) Slug of the existing tag to associate with this resource. +* ``custom_fields`` - (Optional) Custom Field Keys and Values for this object +** For boolean, use the string value "true" or "false" +** For data, use the string format "YYYY-MM-DD" +** For integer, use the value between double quote "10" +** For selection, use the level id +** For text, use the string value +** For URL, use the URL as string ## Attributes Reference diff --git a/netbox/resource_netbox_virtualization_vm.go b/netbox/resource_netbox_virtualization_vm.go index 74369dd50..de5ef7ee7 100644 --- a/netbox/resource_netbox_virtualization_vm.go +++ b/netbox/resource_netbox_virtualization_vm.go @@ -90,6 +90,24 @@ func resourceNetboxVirtualizationVM() *schema.Resource { Optional: true, Default: 0, }, + "custom_fields": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + // terraform default behavior sees a difference between null and an empty string + // therefore we override the default, because null in terraform results in empty string in netbox + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // function is called for each member of map + // including additional call on the amount of entries + // we ignore the count, because the actual state always returns the amount of existing custom_fields and all are optional in terraform + if k == "custom_fields.%" { + return true + } + return old == new + }, + }, }, } } @@ -110,6 +128,8 @@ func resourceNetboxVirtualizationVMCreate(d *schema.ResourceData, tags := d.Get("tag").(*schema.Set).List() tenantID := int64(d.Get("tenant_id").(int)) vcpus := int64(d.Get("vcpus").(int)) + resourceCustomFields := d.Get("custom_fields").(map[string]interface{}) + customFields := convertCustomFieldsFromTerraformToAPICreate(resourceCustomFields) newResource := &models.WritableVirtualMachineWithConfigContext{ Cluster: &clusterID, @@ -144,6 +164,8 @@ func resourceNetboxVirtualizationVMCreate(d *schema.ResourceData, newResource.Vcpus = &vcpus } + newResource.CustomFields = &customFields + resource := virtualization.NewVirtualizationVirtualMachinesCreateParams().WithData(newResource) resourceCreated, err := client.Virtualization.VirtualizationVirtualMachinesCreate(resource, nil) @@ -247,6 +269,12 @@ func resourceNetboxVirtualizationVMRead(d *schema.ResourceData, return err } + customFields := convertCustomFieldsFromAPIToTerraform(resource.CustomFields) + + if err = d.Set("custom_fields", customFields); err != nil { + return err + } + return nil } } @@ -314,6 +342,13 @@ func resourceNetboxVirtualizationVMUpdate(d *schema.ResourceData, params.Vcpus = &vcpus } + // + if d.HasChange("custom_fields") { + stateCustomFields, resourceCustomFields := d.GetChange("custom_fields") + customFields := convertCustomFieldsFromTerraformToAPIUpdate(stateCustomFields, resourceCustomFields) + params.CustomFields = &customFields + } + resource := virtualization.NewVirtualizationVirtualMachinesPartialUpdateParams().WithData(params) resourceID, err := strconv.ParseInt(d.Id(), 10, 64) diff --git a/netbox/util.go b/netbox/util.go index db9343a28..607813fa8 100644 --- a/netbox/util.go +++ b/netbox/util.go @@ -230,3 +230,66 @@ func updatePrimaryStatus(m interface{}, info InfosForPrimary, id int64) error { * return customFieldsAPI * } */ + +// custom_fields have multiple data type returns based on field type +// but terraform only supports map[string]string, so we convert all to strings +func convertCustomFieldsFromAPIToTerraform(customFields interface{}) map[string]string { + toReturn := make(map[string]string) + switch t := customFields.(type) { + case map[string]interface{}: + for key, value := range t { + var strValue string + if value != nil { + switch v := value.(type) { + default: + strValue = fmt.Sprintf("%v", v) + case map[string]interface{}: + strValue = fmt.Sprintf("%v", v["value"]) + } + } + toReturn[key] = strValue + + } + } + + return toReturn +} + +func convertCustomFieldsFromTerraformToAPICreate(customFields map[string]interface{}) map[string]interface{} { + toReturn := make(map[string]interface{}) + for key, value := range customFields { + toReturn[key] = value + + // special handling for booleans, as they are the only parameter not supplied as string to netbox + if value == "true" { + toReturn[key] = 1 + } else if value == "false" { + toReturn[key] = 0 + } + } + + return toReturn +} + +// all custom fields need to be submitted to the netbox API for updates +func convertCustomFieldsFromTerraformToAPIUpdate(stateCustomFields, resourceCustomFields interface{}) map[string]interface{} { + toReturn := make(map[string]interface{}) + + // netbox needs explicit empty string to remove old values + // first we fill all existing fields from the state with an empty string + for key := range stateCustomFields.(map[string]interface{}) { + toReturn[key] = nil + } + // then we override the values that still exist in the terraform code with their respective value + for key, value := range resourceCustomFields.(map[string]interface{}) { + toReturn[key] = value + + // special handling for booleans, as they are the only parameter not supplied as string to netbox + if value == "true" { + toReturn[key] = 1 + } else if value == "false" { + toReturn[key] = 0 + } + } + return toReturn +}